Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions desktop/macos/Desktop/Sources/DefaultsKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ enum DefaultsKey: String {
case multiChatEnabled = "multiChatEnabled"
case aiChatWorkingDirectory = "aiChatWorkingDirectory"
case onboardingStep = "onboardingStep"
case onboardingMemoryImportOwnerUserId = "onboardingMemoryImportOwnerUserID"
case homeOmiDeviceAccountHistory = "home-omi-device-account-history"
case chatScreenshotSharingEnabled = "chatScreenshotSharingEnabled"
}

Expand All @@ -50,6 +52,7 @@ extension UserDefaults {
func bool(forKey key: DefaultsKey) -> Bool { bool(forKey: key.rawValue) }
func integer(forKey key: DefaultsKey) -> Int { integer(forKey: key.rawValue) }
func double(forKey key: DefaultsKey) -> Double { double(forKey: key.rawValue) }
func object(forKey key: DefaultsKey) -> Any? { object(forKey: key.rawValue) }

func set(_ value: Any?, forKey key: DefaultsKey) { set(value, forKey: key.rawValue) }
func removeObject(forKey key: DefaultsKey) { removeObject(forKey: key.rawValue) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import AppKit

Check warning on line 1 in desktop/macos/Desktop/Sources/MainWindow/DesktopHomeView.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

desktop/macos/Desktop/Sources/MainWindow/DesktopHomeView.swift is 1183 lines; consider splitting files over 800 lines.
import SwiftUI
import OmiTheme

Expand Down Expand Up @@ -1107,6 +1107,7 @@
case 0:
DashboardPage(
viewModel: viewModelContainer.dashboardViewModel,
homeStatusStore: viewModelContainer.homeStatusStore,
appState: appState,
appProvider: viewModelContainer.appProvider,
chatProvider: viewModelContainer.chatProvider,
Expand All @@ -1132,7 +1133,10 @@
case 7:
RewindPage(appState: appState)
case 8:
AppsPage(appProvider: viewModelContainer.appProvider, appState: appState)
AppsPage(
appProvider: viewModelContainer.appProvider,
appState: appState,
connectorStatusStore: viewModelContainer.homeStatusStore.connectorStatusStore)
case 9:
SettingsPage(
appState: appState,
Expand All @@ -1147,6 +1151,7 @@
default:
DashboardPage(
viewModel: viewModelContainer.dashboardViewModel,
homeStatusStore: viewModelContainer.homeStatusStore,
appState: appState,
appProvider: viewModelContainer.appProvider,
chatProvider: viewModelContainer.chatProvider,
Expand Down
185 changes: 151 additions & 34 deletions desktop/macos/Desktop/Sources/MainWindow/Pages/AppsPage.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit

Check warning on line 1 in desktop/macos/Desktop/Sources/MainWindow/Pages/AppsPage.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

desktop/macos/Desktop/Sources/MainWindow/Pages/AppsPage.swift is 3506 lines; consider splitting files over 800 lines.
import Combine
import GRDB
import SwiftUI
import OmiTheme
Expand Down Expand Up @@ -116,12 +117,12 @@
struct AppsPage: View {
@ObservedObject var appProvider: AppProvider
var appState: AppState? = nil
@ObservedObject var connectorStatusStore: ImportConnectorStatusStore = ImportConnectorStatusStore()
var initialSection: AppsCatalogInitialSection = .imports
var onDismiss: (() -> Void)? = nil
var onSelectApp: ((OmiApp) -> Void)? = nil
var onSelectConnector: ((ImportConnector) -> Void)? = nil
var onSelectDestination: ((MemoryExportDestination) -> Void)? = nil
@StateObject private var connectorStatusStore = ImportConnectorStatusStore()
@State private var searchText = ""
@State private var selectedApp: OmiApp?
@State private var selectedConnector: ImportConnector?
Expand Down Expand Up @@ -739,6 +740,7 @@
}

@Published private var metricsByID: [String: ConnectorMetrics] = [:]
let connectorDidSync = PassthroughSubject<String, Never>()

private let defaults: UserDefaults
private let sourceCountKeyPrefix = "appsImportConnectorSourceCount."
Expand All @@ -750,9 +752,20 @@
private let manualConnectorIDs: Set<String> = ["chatgpt", "claude"]
private let onboardingChatGPTImportedMemoriesKey = "onboardingChatGPTImportedMemoriesCount"
private let onboardingClaudeImportedMemoriesKey = "onboardingClaudeImportedMemoriesCount"
private var sessionUserID: String?

init(defaults: UserDefaults = .standard) {
init(defaults: UserDefaults = .standard, sessionUserID: String? = nil) {
self.defaults = defaults
self.sessionUserID = Self.normalizedUserID(
sessionUserID ?? defaults.string(forKey: .authUserId)
)
load()
}

func setSessionUserID(_ userID: String?) {
let userID = Self.normalizedUserID(userID)
guard userID != sessionUserID else { return }
sessionUserID = userID
load()
}

Expand Down Expand Up @@ -785,63 +798,91 @@
var metrics = metricsByID[connectorID] ?? ConnectorMetrics()
if let sourceCount {
metrics.sourceCount = max(sourceCount, 0)
defaults.set(metrics.sourceCount, forKey: sourceCountKeyPrefix + connectorID)
defaults.set(metrics.sourceCount, forKey: storageKey(prefix: sourceCountKeyPrefix, connectorID: connectorID))
}
if let memoryCount {
metrics.memoryCount = max(memoryCount, 0)
defaults.set(metrics.memoryCount, forKey: memoryCountKeyPrefix + connectorID)
defaults.set(metrics.memoryCount, forKey: storageKey(prefix: memoryCountKeyPrefix, connectorID: connectorID))
}
metrics.lastSyncedAt = syncedAt
defaults.set(syncedAt.timeIntervalSince1970, forKey: lastSyncedAtKeyPrefix + connectorID)
defaults.set(
syncedAt.timeIntervalSince1970,
forKey: storageKey(prefix: lastSyncedAtKeyPrefix, connectorID: connectorID)
)
metrics.lastDeltaCount = lastDeltaCount
defaults.set(lastDeltaCount != nil, forKey: hasLastDeltaKeyPrefix + connectorID)
defaults.set(
lastDeltaCount != nil,
forKey: storageKey(prefix: hasLastDeltaKeyPrefix, connectorID: connectorID)
)
if let lastDeltaCount {
defaults.set(lastDeltaCount, forKey: lastDeltaCountKeyPrefix + connectorID)
defaults.set(
lastDeltaCount,
forKey: storageKey(prefix: lastDeltaCountKeyPrefix, connectorID: connectorID)
)
} else {
defaults.removeObject(forKey: lastDeltaCountKeyPrefix + connectorID)
defaults.removeObject(forKey: storageKey(prefix: lastDeltaCountKeyPrefix, connectorID: connectorID))
}
if let availabilityText {
metrics.availabilityText = availabilityText
defaults.set(availabilityText, forKey: availabilityTextKeyPrefix + connectorID)
defaults.set(
availabilityText,
forKey: storageKey(prefix: availabilityTextKeyPrefix, connectorID: connectorID)
)
}
metricsByID[connectorID] = metrics
connectorDidSync.send(connectorID)
}

private func clearStoredMetrics(for connectorID: String) {
defaults.removeObject(forKey: sourceCountKeyPrefix + connectorID)
defaults.removeObject(forKey: memoryCountKeyPrefix + connectorID)
defaults.removeObject(forKey: lastSyncedAtKeyPrefix + connectorID)
defaults.removeObject(forKey: lastDeltaCountKeyPrefix + connectorID)
defaults.removeObject(forKey: hasLastDeltaKeyPrefix + connectorID)
defaults.removeObject(forKey: availabilityTextKeyPrefix + connectorID)
defaults.removeObject(forKey: storageKey(prefix: sourceCountKeyPrefix, connectorID: connectorID))
defaults.removeObject(forKey: storageKey(prefix: memoryCountKeyPrefix, connectorID: connectorID))
defaults.removeObject(forKey: storageKey(prefix: lastSyncedAtKeyPrefix, connectorID: connectorID))
defaults.removeObject(forKey: storageKey(prefix: lastDeltaCountKeyPrefix, connectorID: connectorID))
defaults.removeObject(forKey: storageKey(prefix: hasLastDeltaKeyPrefix, connectorID: connectorID))
defaults.removeObject(forKey: storageKey(prefix: availabilityTextKeyPrefix, connectorID: connectorID))
metricsByID[connectorID] = ConnectorMetrics()
}

func refresh() async {
refreshPersistedManualImportMetrics()
await refreshLocalFilesMetrics()
await refreshAppleNotesMetrics()
}

func refreshPersistedManualImportMetrics() {
hydrateLegacyManualImports()
}

private func load() {
metricsByID = [:]
guard sessionUserID != nil else { return }
for connector in ImportConnector.all {
migrateLegacyMetricsIfNeeded(connectorID: connector.id)
var metrics = ConnectorMetrics()

if defaults.object(forKey: sourceCountKeyPrefix + connector.id) != nil {
metrics.sourceCount = defaults.integer(forKey: sourceCountKeyPrefix + connector.id)
let sourceCountKey = storageKey(prefix: sourceCountKeyPrefix, connectorID: connector.id)
let memoryCountKey = storageKey(prefix: memoryCountKeyPrefix, connectorID: connector.id)
let lastSyncedAtKey = storageKey(prefix: lastSyncedAtKeyPrefix, connectorID: connector.id)
let hasLastDeltaKey = storageKey(prefix: hasLastDeltaKeyPrefix, connectorID: connector.id)
let lastDeltaCountKey = storageKey(prefix: lastDeltaCountKeyPrefix, connectorID: connector.id)
let availabilityTextKey = storageKey(prefix: availabilityTextKeyPrefix, connectorID: connector.id)

if defaults.object(forKey: sourceCountKey) != nil {
metrics.sourceCount = defaults.integer(forKey: sourceCountKey)
}
if defaults.object(forKey: memoryCountKeyPrefix + connector.id) != nil {
metrics.memoryCount = defaults.integer(forKey: memoryCountKeyPrefix + connector.id)
if defaults.object(forKey: memoryCountKey) != nil {
metrics.memoryCount = defaults.integer(forKey: memoryCountKey)
}
if defaults.object(forKey: lastSyncedAtKeyPrefix + connector.id) != nil {
let timestamp = defaults.double(forKey: lastSyncedAtKeyPrefix + connector.id)
if defaults.object(forKey: lastSyncedAtKey) != nil {
let timestamp = defaults.double(forKey: lastSyncedAtKey)
if timestamp > 0 {
metrics.lastSyncedAt = Date(timeIntervalSince1970: timestamp)
}
}
if defaults.bool(forKey: hasLastDeltaKeyPrefix + connector.id) {
metrics.lastDeltaCount = defaults.integer(forKey: lastDeltaCountKeyPrefix + connector.id)
if defaults.bool(forKey: hasLastDeltaKey) {
metrics.lastDeltaCount = defaults.integer(forKey: lastDeltaCountKey)
}
metrics.availabilityText = defaults.string(forKey: availabilityTextKeyPrefix + connector.id)
metrics.availabilityText = defaults.string(forKey: availabilityTextKey)

metricsByID[connector.id] = metrics
}
Expand All @@ -853,23 +894,79 @@
}

private func hydrateLegacyManualImports() {
guard let sessionUserID else { return }
let ownerUserID = Self.normalizedUserID(defaults.string(forKey: .onboardingMemoryImportOwnerUserId))
let legacyChatGPTCount = defaults.integer(forKey: onboardingChatGPTImportedMemoriesKey)
if legacyChatGPTCount > 0, metricsByID["chatgpt"]?.memoryCount == nil {
let legacyClaudeCount = defaults.integer(forKey: onboardingClaudeImportedMemoriesKey)
if ownerUserID == nil,
(legacyChatGPTCount > 0 || legacyClaudeCount > 0) {
defaults.set(sessionUserID, forKey: .onboardingMemoryImportOwnerUserId)
}

let resolvedOwnerUserID = Self.normalizedUserID(
defaults.string(forKey: .onboardingMemoryImportOwnerUserId)
)
guard resolvedOwnerUserID == sessionUserID else { return }

let chatGPTMemoryCountKey = storageKey(prefix: memoryCountKeyPrefix, connectorID: "chatgpt")
if legacyChatGPTCount > 0,
defaults.object(forKey: chatGPTMemoryCountKey) == nil {
var metrics = metricsByID["chatgpt"] ?? ConnectorMetrics()
metrics.memoryCount = legacyChatGPTCount
metrics.availabilityText = "Imported during onboarding"
metricsByID["chatgpt"] = metrics
defaults.set(legacyChatGPTCount, forKey: chatGPTMemoryCountKey)
defaults.set(
"Imported during onboarding",
forKey: storageKey(prefix: availabilityTextKeyPrefix, connectorID: "chatgpt")
)
}

let legacyClaudeCount = defaults.integer(forKey: onboardingClaudeImportedMemoriesKey)
if legacyClaudeCount > 0, metricsByID["claude"]?.memoryCount == nil {
let claudeMemoryCountKey = storageKey(prefix: memoryCountKeyPrefix, connectorID: "claude")
if legacyClaudeCount > 0,
defaults.object(forKey: claudeMemoryCountKey) == nil {
var metrics = metricsByID["claude"] ?? ConnectorMetrics()
metrics.memoryCount = legacyClaudeCount
metrics.availabilityText = "Imported during onboarding"
metricsByID["claude"] = metrics
defaults.set(legacyClaudeCount, forKey: claudeMemoryCountKey)
defaults.set(
"Imported during onboarding",
forKey: storageKey(prefix: availabilityTextKeyPrefix, connectorID: "claude")
)
}
}

private func storageKey(prefix: String, connectorID: String) -> String {
guard let sessionUserID else { return prefix + connectorID }
return "\(prefix)user.\(sessionUserID).\(connectorID)"
}

private func migrateLegacyMetricsIfNeeded(connectorID: String) {
guard sessionUserID != nil else { return }
for prefix in [
sourceCountKeyPrefix,
memoryCountKeyPrefix,
lastSyncedAtKeyPrefix,
lastDeltaCountKeyPrefix,
hasLastDeltaKeyPrefix,
availabilityTextKeyPrefix,
] {
let legacyKey = prefix + connectorID
let scopedKey = storageKey(prefix: prefix, connectorID: connectorID)
if defaults.object(forKey: scopedKey) == nil,
let legacyValue = defaults.object(forKey: legacyKey) {
defaults.set(legacyValue, forKey: scopedKey)
}
defaults.removeObject(forKey: legacyKey)
}
}

private static func normalizedUserID(_ userID: String?) -> String? {
let trimmed = userID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}

private func refreshLocalFilesMetrics() async {
guard let dbQueue = await RewindDatabase.shared.getDatabaseQueue() else { return }

Expand All @@ -891,17 +988,28 @@

var metrics = metricsByID["local-files"] ?? ConnectorMetrics()
metrics.sourceCount = result.count
defaults.set(result.count, forKey: sourceCountKeyPrefix + "local-files")
defaults.set(
result.count,
forKey: storageKey(prefix: sourceCountKeyPrefix, connectorID: "local-files")
)
if metrics.lastSyncedAt == nil, let lastIndexedAt = result.lastIndexedAt {
metrics.lastSyncedAt = lastIndexedAt
defaults.set(lastIndexedAt.timeIntervalSince1970, forKey: lastSyncedAtKeyPrefix + "local-files")
defaults.set(
lastIndexedAt.timeIntervalSince1970,
forKey: storageKey(prefix: lastSyncedAtKeyPrefix, connectorID: "local-files")
)
}
if metrics.lastSyncedAt != nil || result.count > 0 {
metrics.availabilityText = "On-device index"
defaults.set("On-device index", forKey: availabilityTextKeyPrefix + "local-files")
defaults.set(
"On-device index",
forKey: storageKey(prefix: availabilityTextKeyPrefix, connectorID: "local-files")
)
} else {
metrics.availabilityText = nil
defaults.removeObject(forKey: availabilityTextKeyPrefix + "local-files")
defaults.removeObject(
forKey: storageKey(prefix: availabilityTextKeyPrefix, connectorID: "local-files")
)
}
metricsByID["local-files"] = metrics
} catch {
Expand All @@ -915,14 +1023,23 @@
case .connected(let noteCount, _):
var metrics = metricsByID["apple-notes"] ?? ConnectorMetrics()
metrics.sourceCount = noteCount
defaults.set(noteCount, forKey: sourceCountKeyPrefix + "apple-notes")
defaults.set(
noteCount,
forKey: storageKey(prefix: sourceCountKeyPrefix, connectorID: "apple-notes")
)
if metrics.lastSyncedAt == nil {
let syncedAt = Date()
metrics.lastSyncedAt = syncedAt
defaults.set(syncedAt.timeIntervalSince1970, forKey: lastSyncedAtKeyPrefix + "apple-notes")
defaults.set(
syncedAt.timeIntervalSince1970,
forKey: storageKey(prefix: lastSyncedAtKeyPrefix, connectorID: "apple-notes")
)
}
metrics.availabilityText = "Private notes accessible"
defaults.set("Private notes accessible", forKey: availabilityTextKeyPrefix + "apple-notes")
defaults.set(
"Private notes accessible",
forKey: storageKey(prefix: availabilityTextKeyPrefix, connectorID: "apple-notes")
)
metricsByID["apple-notes"] = metrics
case .needsAccess(_, let reasonCode), .error(_, let reasonCode):
log("ImportConnectorStatusStore: Apple Notes refresh unavailable code=\(reasonCode)")
Expand Down
Loading
Loading