feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish

Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
This commit is contained in:
Rocky
2026-08-13 01:00:51 +08:00
parent fd6e0d3e7e
commit 9f308fadd2
202 changed files with 10897 additions and 5962 deletions
+126 -1
View File
@@ -19,13 +19,17 @@ final class SettingsCloudSyncTests: XCTestCase {
override func setUp() {
super.setUp()
Keychain.resetTestMemoryStore()
suiteName = "group.com.osgkeyboard.shared.tests.settings.\(UUID().uuidString)"
defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
defaults.set(deviceA, forKey: "sync.deviceID.v1")
store = AppGroupStore(defaults: defaults)
kvs = FakeUbiquitousKeyValueStore()
settingsSync = SettingsCloudSync(kvs: kvs) { [unowned self] in store }
settingsSync = SettingsCloudSync(
kvs: kvs,
makeStore: { [unowned self] in store }
)
}
override func tearDown() {
@@ -34,6 +38,8 @@ final class SettingsCloudSyncTests: XCTestCase {
try? Keychain.deleteAPIKey(for: "openai", useICloudSync: true)
try? Keychain.deleteAPIKey(for: "qwen", useICloudSync: false)
try? Keychain.deleteAPIKey(for: "qwen", useICloudSync: true)
try? Keychain.deleteASRAPIKey(for: "openai", useICloudSync: true)
Keychain.resetTestMemoryStore()
super.tearDown()
}
@@ -126,6 +132,81 @@ final class SettingsCloudSyncTests: XCTestCase {
XCTAssertNil(reencodedObject["flowKeepAliveMode"])
}
func testLegacyClipboardFieldsDecodeButAreNotReencoded() throws {
let payload = SyncedAppSettingsV2.seeded(
from: AppGroupConfiguration.load(fromAvailable: defaults),
deviceID: deviceA,
updatedAt: Date(timeIntervalSince1970: 100)
)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
var object = try XCTUnwrap(
JSONSerialization.jsonObject(with: encoder.encode(payload)) as? [String: Any]
)
let legacyField = try XCTUnwrap(
JSONSerialization.jsonObject(
with: encoder.encode(
SyncedField(value: true, updatedAt: Date(timeIntervalSince1970: 200), deviceID: deviceB)
)
) as? [String: Any]
)
object["clipboardHistoryEnabled"] = legacyField
object["clipboardCandidateBarEnabled"] = legacyField
let legacyData = try JSONSerialization.data(withJSONObject: object)
let decoded = try settingsSync.decodeV2(legacyData)
let reencoded = try XCTUnwrap(
JSONSerialization.jsonObject(with: encoder.encode(decoded)) as? [String: Any]
)
XCTAssertNil(reencoded["clipboardHistoryEnabled"])
XCTAssertNil(reencoded["clipboardCandidateBarEnabled"])
}
func testRemoteClipboardTrueDoesNotActivateLocalConsent() async throws {
store.setSettingsICloudSyncEnabled(true)
store.setClipboardHistoryEnabled(false)
store.setClipboardCandidateBarEnabled(false)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let remote = SyncedAppSettingsV2.seeded(
from: AppGroupConfiguration.load(fromAvailable: defaults),
deviceID: deviceB,
updatedAt: Date(timeIntervalSince1970: 100)
)
var object = try XCTUnwrap(
JSONSerialization.jsonObject(with: encoder.encode(remote)) as? [String: Any]
)
let remoteTrue = try XCTUnwrap(
JSONSerialization.jsonObject(
with: encoder.encode(
SyncedField(value: true, updatedAt: Date(timeIntervalSince1970: 900), deviceID: deviceB)
)
) as? [String: Any]
)
object["clipboardHistoryEnabled"] = remoteTrue
object["clipboardCandidateBarEnabled"] = remoteTrue
kvs.set(
try JSONSerialization.data(withJSONObject: object),
forKey: SettingsCloudSync.kvsKey
)
await settingsSync.pullAndMerge(store: store)
XCTAssertFalse(store.clipboardHistoryEnabled)
XCTAssertFalse(store.clipboardCandidateBarEnabled)
}
func testClipboardTogglesRemainSharedThroughLocalAppGroupDefaults() {
store.setClipboardHistoryEnabled(true)
store.setClipboardCandidateBarEnabled(true)
let extensionSideReader = AppGroupStore(defaults: defaults)
XCTAssertTrue(extensionSideReader.clipboardHistoryEnabled)
XCTAssertTrue(extensionSideReader.clipboardCandidateBarEnabled)
}
func testLegacyV1PullDoesNotClearKeychain() async throws {
try Keychain.setAPIKey("sk-local-openai", for: "openai", useICloudSync: false)
store.setSettingsICloudSyncEnabled(true)
@@ -167,6 +248,50 @@ final class SettingsCloudSyncTests: XCTestCase {
XCTAssertEqual(Keychain.apiKey(for: "openai", preferICloudSync: true), "sk-local-openai")
}
func testDisableSyncMigratesKeyLocallyBeforeTurningFlagsOff() throws {
try Keychain.setAPIKey("synced-credential", for: "openai", useICloudSync: true)
store.setSettingsICloudSyncEnabled(true)
store.setPersonalDictionaryICloudSyncEnabled(true)
try settingsSync.disableSync()
XCTAssertFalse(store.settingsICloudSyncEnabled)
XCTAssertFalse(store.personalDictionaryICloudSyncEnabled)
XCTAssertNotNil(Keychain.apiKey(for: "openai", preferICloudSync: false))
}
func testDisableSyncFailureKeepsFlagsEnabled() {
store.setSettingsICloudSyncEnabled(true)
store.setPersonalDictionaryICloudSyncEnabled(true)
ICloudSyncPreferences.pushSettingsEnabled(true, kvs: kvs)
ICloudSyncPreferences.pushDictionaryEnabled(true, kvs: kvs)
let failingSync = SettingsCloudSync(
kvs: kvs,
makeStore: { [unowned self] in store },
migrateLocalKeysToICloud: {},
migrateICloudKeysToLocal: {
throw Keychain.CredentialMigrationError.conflict
}
)
XCTAssertThrowsError(try failingSync.disableSync()) { error in
XCTAssertEqual(
error as? SettingsCloudSyncError,
.credentialMigrationFailed(.conflict)
)
}
XCTAssertTrue(store.settingsICloudSyncEnabled)
XCTAssertTrue(store.personalDictionaryICloudSyncEnabled)
XCTAssertEqual(
kvs.object(forKey: ICloudSyncPreferences.settingsEnabledKey) as? Bool,
true
)
XCTAssertEqual(
kvs.object(forKey: ICloudSyncPreferences.dictionaryEnabledKey) as? Bool,
true
)
}
func testPullAndMergeAppliesRemoteSettingsToAppGroup() async throws {
store.setSettingsICloudSyncEnabled(true)
store.setLocaleId("auto")