This commit is contained in:
@@ -2,6 +2,43 @@ import XCTest
|
||||
@testable import ClipBored
|
||||
|
||||
final class SettingsModelTests: XCTestCase {
|
||||
func testFreshProfileRequiresOnboardingUntilCompleted() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
XCTAssertFalse(settings.onboardingCompleted)
|
||||
XCTAssertFalse(defaults.bool(forKey: SettingsModel.Keys.onboardingCompleted))
|
||||
|
||||
settings.markOnboardingCompleted()
|
||||
|
||||
XCTAssertTrue(settings.onboardingCompleted)
|
||||
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.onboardingCompleted))
|
||||
XCTAssertEqual(changes, [.other])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertTrue(restored.onboardingCompleted)
|
||||
}
|
||||
|
||||
func testLegacyProfileDoesNotShowOnboardingAfterUpgrade() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
defaults.set(AppConfiguration.defaultHistoryLength, forKey: SettingsModel.Keys.maxHistoryItems)
|
||||
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
|
||||
XCTAssertTrue(settings.onboardingCompleted)
|
||||
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.onboardingCompleted))
|
||||
}
|
||||
|
||||
func testShowDockIconPersistsAndNotifies() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
@@ -26,6 +63,383 @@ final class SettingsModelTests: XCTestCase {
|
||||
XCTAssertTrue(restored.showDockIcon)
|
||||
}
|
||||
|
||||
func testHideFromScreenCapturePersistsAndNotifies() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
XCTAssertFalse(settings.hideFromScreenCapture)
|
||||
|
||||
settings.hideFromScreenCapture = true
|
||||
|
||||
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.hideFromScreenCapture))
|
||||
XCTAssertEqual(changes, [.hideFromScreenCapture])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertTrue(restored.hideFromScreenCapture)
|
||||
}
|
||||
|
||||
func testCompactModePersistsAndNotifies() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
XCTAssertFalse(settings.compactMode)
|
||||
|
||||
settings.compactMode = true
|
||||
|
||||
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.compactMode))
|
||||
XCTAssertEqual(changes, [.compactMode])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertTrue(restored.compactMode)
|
||||
}
|
||||
|
||||
func testPanelSidePersistsAndNotifies() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
XCTAssertEqual(settings.panelLayout, .vertical)
|
||||
XCTAssertEqual(settings.panelSide, .right)
|
||||
|
||||
settings.panelSide = .left
|
||||
|
||||
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.panelLayout), ClipboardPanelLayout.vertical.rawValue)
|
||||
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.panelSide), ClipboardPanelSide.left.rawValue)
|
||||
XCTAssertEqual(changes, [.panelLayout])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertEqual(restored.panelLayout, .vertical)
|
||||
XCTAssertEqual(restored.panelSide, .left)
|
||||
}
|
||||
|
||||
func testImageCacheMinimumAllowsTwoMegabytes() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let twoMegabytes = Int64(2 * 1024 * 1024)
|
||||
defaults.set(twoMegabytes, forKey: SettingsModel.Keys.imageCacheMaxBytes)
|
||||
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
|
||||
XCTAssertEqual(AppConfiguration.minCacheMaxBytes, twoMegabytes)
|
||||
XCTAssertEqual(settings.imageCacheMaxBytes, twoMegabytes)
|
||||
}
|
||||
|
||||
func testICloudSyncPersistsAndNotifies() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
XCTAssertFalse(settings.iCloudSyncEnabled)
|
||||
|
||||
settings.setCloudSyncStatus(message: "Synced 3 clips to iCloud.")
|
||||
XCTAssertEqual(settings.cloudSyncStatusMessage, "Synced 3 clips to iCloud.")
|
||||
changes.removeAll()
|
||||
|
||||
settings.iCloudSyncEnabled = true
|
||||
|
||||
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.iCloudSyncEnabled))
|
||||
XCTAssertEqual(settings.cloudSyncStatusMessage, "")
|
||||
XCTAssertEqual(changes, [.cloudSync])
|
||||
|
||||
settings.setCloudSyncStatus(message: "Synced 3 clips to iCloud.")
|
||||
changes.removeAll()
|
||||
settings.iCloudSyncEnabled = false
|
||||
|
||||
XCTAssertFalse(defaults.bool(forKey: SettingsModel.Keys.iCloudSyncEnabled))
|
||||
XCTAssertEqual(settings.cloudSyncStatusMessage, "")
|
||||
XCTAssertEqual(changes, [.cloudSync])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertFalse(restored.iCloudSyncEnabled)
|
||||
}
|
||||
|
||||
func testPanelShelfHeightPersistsAndNotifies() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
XCTAssertEqual(settings.panelShelfHeight, 0)
|
||||
|
||||
settings.panelShelfHeight = 620
|
||||
|
||||
XCTAssertEqual(defaults.double(forKey: SettingsModel.Keys.panelShelfHeight), 620)
|
||||
XCTAssertEqual(changes, [.panelSizing])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertEqual(restored.panelShelfHeight, 620)
|
||||
}
|
||||
|
||||
func testPauseCaptureUntilPersistsAndCanBeCleared() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
let pauseUntil = Date(timeIntervalSince1970: 1_234_567)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
settings.pauseCapture = true
|
||||
settings.pauseCaptureUntil = pauseUntil
|
||||
|
||||
XCTAssertTrue(defaults.bool(forKey: SettingsModel.Keys.pauseCapture))
|
||||
XCTAssertEqual(defaults.double(forKey: SettingsModel.Keys.pauseCaptureUntil), pauseUntil.timeIntervalSince1970)
|
||||
XCTAssertEqual(changes, [.pauseCapture, .pauseCapture])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertTrue(restored.pauseCapture)
|
||||
XCTAssertEqual(restored.pauseCaptureUntil, pauseUntil)
|
||||
|
||||
restored.pauseCaptureUntil = nil
|
||||
|
||||
XCTAssertNil(defaults.object(forKey: SettingsModel.Keys.pauseCaptureUntil))
|
||||
}
|
||||
|
||||
func testIgnoredAppsPersistsAndNotifiesNarrowChange() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
settings.ignoredApps = ["Safari", "Xcode"]
|
||||
|
||||
XCTAssertEqual(defaults.stringArray(forKey: SettingsModel.Keys.ignoredApps), ["Safari", "Xcode"])
|
||||
XCTAssertEqual(changes, [.ignoredApps])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertEqual(restored.ignoredApps, ["Safari", "Xcode"])
|
||||
}
|
||||
|
||||
func testCommonControlSettingsNotifyNarrowChanges() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
settings.defaultSortMode = .links
|
||||
settings.includeImageTextInSearch = true
|
||||
settings.pruneDuplicates = false
|
||||
settings.ignoredItemKindsRaw = [ClipboardItemKind.image.rawValue]
|
||||
settings.keepFirstImage = false
|
||||
settings.excludeSensitive = true
|
||||
settings.clearHistoryOnQuit = true
|
||||
|
||||
XCTAssertEqual(changes, [
|
||||
.defaultSortMode,
|
||||
.includeImageTextInSearch,
|
||||
.pruneDuplicates,
|
||||
.ignoredItemKinds,
|
||||
.keepFirstImage,
|
||||
.excludeSensitive,
|
||||
.clearHistoryOnQuit
|
||||
])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertEqual(restored.defaultSortMode, .links)
|
||||
XCTAssertTrue(restored.includeImageTextInSearch)
|
||||
XCTAssertFalse(restored.pruneDuplicates)
|
||||
XCTAssertEqual(restored.ignoredItemKindsRaw, [ClipboardItemKind.image.rawValue])
|
||||
XCTAssertFalse(restored.keepFirstImage)
|
||||
XCTAssertTrue(restored.excludeSensitive)
|
||||
XCTAssertTrue(restored.clearHistoryOnQuit)
|
||||
}
|
||||
|
||||
func testIgnoredItemKindsCannotDisableEveryVisibleKind() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
let allVisibleKindRawValues = Self.visibleItemKinds.map(\.rawValue)
|
||||
let expectedIgnoredKindRawValues = allVisibleKindRawValues.filter {
|
||||
$0 != ClipboardItemKind.text.rawValue
|
||||
}
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
settings.ignoredItemKindsRaw = allVisibleKindRawValues
|
||||
|
||||
XCTAssertEqual(settings.ignoredItemKindsRaw, expectedIgnoredKindRawValues)
|
||||
XCTAssertEqual(
|
||||
defaults.object(forKey: SettingsModel.Keys.ignoredItemKinds) as? [Int],
|
||||
expectedIgnoredKindRawValues
|
||||
)
|
||||
XCTAssertEqual(changes, [.ignoredItemKinds])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertEqual(restored.ignoredItemKindsRaw, expectedIgnoredKindRawValues)
|
||||
}
|
||||
|
||||
func testStoredIgnoredItemKindsCannotDisableEveryVisibleKind() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let allVisibleKindRawValues = Self.visibleItemKinds.map(\.rawValue)
|
||||
let expectedIgnoredKindRawValues = allVisibleKindRawValues.filter {
|
||||
$0 != ClipboardItemKind.text.rawValue
|
||||
}
|
||||
defaults.set(allVisibleKindRawValues, forKey: SettingsModel.Keys.ignoredItemKinds)
|
||||
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
|
||||
XCTAssertEqual(settings.ignoredItemKindsRaw, expectedIgnoredKindRawValues)
|
||||
XCTAssertEqual(
|
||||
defaults.object(forKey: SettingsModel.Keys.ignoredItemKinds) as? [Int],
|
||||
expectedIgnoredKindRawValues
|
||||
)
|
||||
}
|
||||
|
||||
func testStoredImageCacheLimitIsClampedToSettingsRange() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let zeroDefaults = UserDefaults(suiteName: "\(suiteName).zero")!
|
||||
let lowDefaults = UserDefaults(suiteName: "\(suiteName).low")!
|
||||
let highDefaults = UserDefaults(suiteName: "\(suiteName).high")!
|
||||
defer {
|
||||
zeroDefaults.removePersistentDomain(forName: "\(suiteName).zero")
|
||||
lowDefaults.removePersistentDomain(forName: "\(suiteName).low")
|
||||
highDefaults.removePersistentDomain(forName: "\(suiteName).high")
|
||||
}
|
||||
|
||||
zeroDefaults.set(0, forKey: SettingsModel.Keys.imageCacheMaxBytes)
|
||||
let zeroSettings = SettingsModel(defaults: zeroDefaults)
|
||||
|
||||
XCTAssertEqual(zeroSettings.imageCacheMaxBytes, AppConfiguration.defaultCacheMaxBytes)
|
||||
XCTAssertEqual(Int64(zeroDefaults.integer(forKey: SettingsModel.Keys.imageCacheMaxBytes)), AppConfiguration.defaultCacheMaxBytes)
|
||||
|
||||
lowDefaults.set(1 * 1024 * 1024, forKey: SettingsModel.Keys.imageCacheMaxBytes)
|
||||
let lowSettings = SettingsModel(defaults: lowDefaults)
|
||||
|
||||
XCTAssertEqual(lowSettings.imageCacheMaxBytes, AppConfiguration.minCacheMaxBytes)
|
||||
XCTAssertEqual(Int64(lowDefaults.integer(forKey: SettingsModel.Keys.imageCacheMaxBytes)), AppConfiguration.minCacheMaxBytes)
|
||||
|
||||
highDefaults.set(2048 * 1024 * 1024, forKey: SettingsModel.Keys.imageCacheMaxBytes)
|
||||
let highSettings = SettingsModel(defaults: highDefaults)
|
||||
|
||||
XCTAssertEqual(highSettings.imageCacheMaxBytes, AppConfiguration.maxCacheMaxBytes)
|
||||
XCTAssertEqual(Int64(highDefaults.integer(forKey: SettingsModel.Keys.imageCacheMaxBytes)), AppConfiguration.maxCacheMaxBytes)
|
||||
}
|
||||
|
||||
func testStoredHistoryLimitIsClampedToSettingsRange() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let zeroDefaults = UserDefaults(suiteName: "\(suiteName).zero")!
|
||||
let lowDefaults = UserDefaults(suiteName: "\(suiteName).low")!
|
||||
let highDefaults = UserDefaults(suiteName: "\(suiteName).high")!
|
||||
defer {
|
||||
zeroDefaults.removePersistentDomain(forName: "\(suiteName).zero")
|
||||
lowDefaults.removePersistentDomain(forName: "\(suiteName).low")
|
||||
highDefaults.removePersistentDomain(forName: "\(suiteName).high")
|
||||
}
|
||||
|
||||
zeroDefaults.set(0, forKey: SettingsModel.Keys.maxHistoryItems)
|
||||
let zeroSettings = SettingsModel(defaults: zeroDefaults)
|
||||
|
||||
XCTAssertEqual(zeroSettings.maxHistoryItems, AppConfiguration.defaultHistoryLength)
|
||||
XCTAssertEqual(zeroDefaults.integer(forKey: SettingsModel.Keys.maxHistoryItems), AppConfiguration.defaultHistoryLength)
|
||||
|
||||
lowDefaults.set(1, forKey: SettingsModel.Keys.maxHistoryItems)
|
||||
let lowSettings = SettingsModel(defaults: lowDefaults)
|
||||
|
||||
XCTAssertEqual(lowSettings.maxHistoryItems, AppConfiguration.minHistoryLength)
|
||||
XCTAssertEqual(lowDefaults.integer(forKey: SettingsModel.Keys.maxHistoryItems), AppConfiguration.minHistoryLength)
|
||||
|
||||
highDefaults.set(100_000, forKey: SettingsModel.Keys.maxHistoryItems)
|
||||
let highSettings = SettingsModel(defaults: highDefaults)
|
||||
|
||||
XCTAssertEqual(highSettings.maxHistoryItems, AppConfiguration.maxHistoryLength)
|
||||
XCTAssertEqual(highDefaults.integer(forKey: SettingsModel.Keys.maxHistoryItems), AppConfiguration.maxHistoryLength)
|
||||
}
|
||||
|
||||
func testLimitAssignmentsAreClampedAndPersisted() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
settings.maxHistoryItems = 1
|
||||
settings.maxHistoryItems = 100_000
|
||||
settings.imageCacheMaxBytes = 1
|
||||
settings.imageCacheMaxBytes = 2048 * 1024 * 1024
|
||||
|
||||
XCTAssertEqual(settings.maxHistoryItems, AppConfiguration.maxHistoryLength)
|
||||
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.maxHistoryItems), AppConfiguration.maxHistoryLength)
|
||||
XCTAssertEqual(settings.imageCacheMaxBytes, AppConfiguration.maxCacheMaxBytes)
|
||||
XCTAssertEqual(Int64(defaults.integer(forKey: SettingsModel.Keys.imageCacheMaxBytes)), AppConfiguration.maxCacheMaxBytes)
|
||||
XCTAssertEqual(changes, [
|
||||
.maxHistoryItems,
|
||||
.maxHistoryItems,
|
||||
.imageCacheMaxBytes,
|
||||
.imageCacheMaxBytes
|
||||
])
|
||||
}
|
||||
|
||||
func testHistoryRetentionDefaultsToOneMonthAndPersists() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
defer {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
let settings = SettingsModel(defaults: defaults)
|
||||
var changes: [SettingsModel.Change] = []
|
||||
settings.observe { changes.append($0) }
|
||||
|
||||
XCTAssertEqual(settings.historyRetention, .oneMonth)
|
||||
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.historyRetention), HistoryRetention.oneMonth.rawValue)
|
||||
|
||||
settings.historyRetention = .oneWeek
|
||||
|
||||
XCTAssertEqual(defaults.integer(forKey: SettingsModel.Keys.historyRetention), HistoryRetention.oneWeek.rawValue)
|
||||
XCTAssertEqual(changes, [.historyRetention])
|
||||
|
||||
let restored = SettingsModel(defaults: defaults)
|
||||
XCTAssertEqual(restored.historyRetention, .oneWeek)
|
||||
}
|
||||
|
||||
func testCustomCollectionsPersistWithNormalizedColors() {
|
||||
let suiteName = "com.clipbored.settingsmodel.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
@@ -77,4 +491,17 @@ final class SettingsModelTests: XCTestCase {
|
||||
XCTAssertEqual(restored.customCollectionNames, ["Product Research"])
|
||||
XCTAssertEqual(restored.collectionColorHex(forCollectionNamed: "Product Research"), "#3366FF")
|
||||
}
|
||||
|
||||
private static let visibleItemKinds: [ClipboardItemKind] = [
|
||||
.text,
|
||||
.code,
|
||||
.url,
|
||||
.image,
|
||||
.color,
|
||||
.audio,
|
||||
.video,
|
||||
.richText,
|
||||
.pdf,
|
||||
.file
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user