refactor: Adding structure
Huge refactor to add better structure to the project. Separating features with their associated view and model structure. This should be better organization and will allow us to improve the overall architecture in the future. I forsee many more improvements that can follow this change. e.g. MVVM Arch As well as cleaning up duplicate, unused, functionality. Many files have global functions that can also be moved or be renamed. damus/ ├── Features/ │ ├── <Feature>/ │ │ ├── Views/ │ │ └── Models/ ├── Shared/ │ ├── Components/ │ ├── Media/ │ ├── Buttons/ │ ├── Extensions/ │ ├── Empty Views/ │ ├── ErrorHandling/ │ ├── Modifiers/ │ └── Utilities/ ├── Core/ │ ├── Nostr/ │ ├── NIPs/ │ ├── DIPs/ │ ├── Types/ │ ├── Networking/ │ └── Storage/ Signed-off-by: ericholguin <ericholguin@apache.org>
This commit is contained in:
committed by
Daniel D’Aquino
parent
fdbf271432
commit
65a22813a3
59
damus/Core/Storage/DamusCacheManager.swift
Normal file
59
damus/Core/Storage/DamusCacheManager.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// DamusCacheManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-10-04.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Kingfisher
|
||||
|
||||
struct DamusCacheManager {
|
||||
static var shared: DamusCacheManager = DamusCacheManager()
|
||||
|
||||
func clear_cache(damus_state: DamusState, completion: (() -> Void)? = nil) {
|
||||
Log.info("Clearing all caches", for: .storage)
|
||||
clear_kingfisher_cache(completion: {
|
||||
clear_cache_folder(completion: {
|
||||
Log.info("All caches cleared", for: .storage)
|
||||
completion?()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func clear_kingfisher_cache(completion: (() -> Void)? = nil) {
|
||||
Log.info("Clearing Kingfisher cache", for: .storage)
|
||||
KingfisherManager.shared.cache.clearMemoryCache()
|
||||
KingfisherManager.shared.cache.clearDiskCache {
|
||||
Log.info("Kingfisher cache cleared", for: .storage)
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
func clear_cache_folder(completion: (() -> Void)? = nil) {
|
||||
Log.info("Clearing entire cache folder", for: .storage)
|
||||
let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
|
||||
do {
|
||||
let fileNames = try FileManager.default.contentsOfDirectory(atPath: cacheURL.path)
|
||||
|
||||
for fileName in fileNames {
|
||||
let filePath = cacheURL.appendingPathComponent(fileName)
|
||||
|
||||
// Prevent issues by double-checking if files are in use, and do not delete them if they are.
|
||||
// This is not perfect. There is still a small chance for a race condition if a file is opened between this check and the file removal.
|
||||
let isBusy = (!(access(filePath.path, F_OK) == -1 && errno == ETXTBSY))
|
||||
if isBusy {
|
||||
continue
|
||||
}
|
||||
|
||||
try FileManager.default.removeItem(at: filePath)
|
||||
}
|
||||
|
||||
Log.info("Cache folder cleared successfully.", for: .storage)
|
||||
completion?()
|
||||
} catch {
|
||||
Log.error("Could not clear cache folder", for: .storage)
|
||||
}
|
||||
}
|
||||
}
|
||||
230
damus/Core/Storage/DamusState.swift
Normal file
230
damus/Core/Storage/DamusState.swift
Normal file
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// DamusState.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-30.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import LinkPresentation
|
||||
import EmojiPicker
|
||||
|
||||
class DamusState: HeadlessDamusState {
|
||||
let keypair: Keypair
|
||||
let likes: EventCounter
|
||||
let boosts: EventCounter
|
||||
let quote_reposts: EventCounter
|
||||
let contacts: Contacts
|
||||
let mutelist_manager: MutelistManager
|
||||
let profiles: Profiles
|
||||
let dms: DirectMessagesModel
|
||||
let previews: PreviewCache
|
||||
let zaps: Zaps
|
||||
let lnurls: LNUrls
|
||||
let settings: UserSettingsStore
|
||||
let relay_filters: RelayFilters
|
||||
let relay_model_cache: RelayModelCache
|
||||
let drafts: Drafts
|
||||
let events: EventCache
|
||||
let bookmarks: BookmarksManager
|
||||
let replies: ReplyCounter
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
let music: MusicController?
|
||||
let video: DamusVideoCoordinator
|
||||
let ndb: Ndb
|
||||
var purple: DamusPurple
|
||||
var push_notification_client: PushNotificationClient
|
||||
let emoji_provider: EmojiProvider
|
||||
let favicon_cache: FaviconCache
|
||||
private(set) var nostrNetwork: NostrNetworkManager
|
||||
|
||||
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
|
||||
self.keypair = keypair
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
self.contacts = contacts
|
||||
self.mutelist_manager = mutelist_manager
|
||||
self.profiles = profiles
|
||||
self.dms = dms
|
||||
self.previews = previews
|
||||
self.zaps = zaps
|
||||
self.lnurls = lnurls
|
||||
self.settings = settings
|
||||
self.relay_filters = relay_filters
|
||||
self.relay_model_cache = relay_model_cache
|
||||
self.drafts = drafts
|
||||
self.events = events
|
||||
self.bookmarks = bookmarks
|
||||
self.replies = replies
|
||||
self.wallet = wallet
|
||||
self.nav = nav
|
||||
self.music = music
|
||||
self.video = video
|
||||
self.ndb = ndb
|
||||
self.purple = purple ?? DamusPurple(
|
||||
settings: settings,
|
||||
keypair: keypair
|
||||
)
|
||||
self.quote_reposts = quote_reposts
|
||||
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
||||
self.emoji_provider = emoji_provider
|
||||
self.favicon_cache = FaviconCache()
|
||||
|
||||
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
|
||||
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
convenience init?(keypair: Keypair) {
|
||||
// nostrdb
|
||||
var mndb = Ndb()
|
||||
if mndb == nil {
|
||||
// try recovery
|
||||
print("DB ISSUE! RECOVERING")
|
||||
mndb = Ndb.safemode()
|
||||
|
||||
// out of space or something?? maybe we need a in-memory fallback
|
||||
if mndb == nil {
|
||||
logout(nil)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||
let home: HomeModel = HomeModel()
|
||||
let sub_id = UUID().uuidString
|
||||
|
||||
guard let ndb = mndb else { return nil }
|
||||
let pubkey = keypair.pubkey
|
||||
|
||||
let model_cache = RelayModelCache()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
|
||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||
|
||||
self.init(
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
mutelist_manager: MutelistManager(user_keypair: keypair),
|
||||
profiles: Profiles(ndb: ndb),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
lnurls: LNUrls(),
|
||||
settings: settings,
|
||||
relay_filters: relay_filters,
|
||||
relay_model_cache: model_cache,
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: ndb),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: navigationCoordinator,
|
||||
music: MusicController(onChange: { _ in }),
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func add_zap(zap: Zapping) -> Bool {
|
||||
// store generic zap mapping
|
||||
self.zaps.add_zap(zap: zap)
|
||||
let stored = self.events.store_zap(zap: zap)
|
||||
|
||||
// thread zaps
|
||||
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
|
||||
// [nozaps]: thread zaps are only available outside of the app store
|
||||
replies.count_replies(ev, keypair: self.keypair)
|
||||
events.add_replies(ev: ev, keypair: self.keypair)
|
||||
}
|
||||
|
||||
// associate with events as well
|
||||
return stored
|
||||
}
|
||||
|
||||
var pubkey: Pubkey {
|
||||
return keypair.pubkey
|
||||
}
|
||||
|
||||
var is_privkey_user: Bool {
|
||||
keypair.privkey != nil
|
||||
}
|
||||
|
||||
func close() {
|
||||
print("txn: damus close")
|
||||
Task {
|
||||
try await self.push_notification_client.revoke_token()
|
||||
}
|
||||
wallet.disconnect()
|
||||
nostrNetwork.pool.close()
|
||||
ndb.close()
|
||||
}
|
||||
|
||||
static var empty: DamusState {
|
||||
let empty_pub: Pubkey = .empty
|
||||
let empty_sec: Privkey = .empty
|
||||
let kp = Keypair(pubkey: empty_pub, privkey: nil)
|
||||
|
||||
return DamusState.init(
|
||||
keypair: Keypair(pubkey: empty_pub, privkey: empty_sec),
|
||||
likes: EventCounter(our_pubkey: empty_pub),
|
||||
boosts: EventCounter(our_pubkey: empty_pub),
|
||||
contacts: Contacts(our_pubkey: empty_pub),
|
||||
mutelist_manager: MutelistManager(user_keypair: kp),
|
||||
profiles: Profiles(ndb: .empty),
|
||||
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: empty_pub),
|
||||
lnurls: LNUrls(),
|
||||
settings: UserSettingsStore(),
|
||||
relay_filters: RelayFilters(our_pubkey: empty_pub),
|
||||
relay_model_cache: RelayModelCache(),
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: .empty),
|
||||
bookmarks: BookmarksManager(pubkey: empty_pub),
|
||||
replies: ReplyCounter(our_pubkey: empty_pub),
|
||||
wallet: WalletModel(settings: UserSettingsStore()),
|
||||
nav: NavigationCoordinator(),
|
||||
music: nil,
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: .empty,
|
||||
quote_reposts: .init(our_pubkey: empty_pub),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension DamusState {
|
||||
struct NostrNetworkManagerDelegate: NostrNetworkManager.Delegate {
|
||||
let settings: UserSettingsStore
|
||||
let contacts: Contacts
|
||||
|
||||
var ndb: Ndb
|
||||
var keypair: Keypair
|
||||
|
||||
var latestRelayListEventIdHex: String? {
|
||||
get { self.settings.latestRelayListEventIdHex }
|
||||
set { self.settings.latestRelayListEventIdHex = newValue }
|
||||
}
|
||||
|
||||
var latestContactListEvent: NostrEvent? { self.contacts.event }
|
||||
var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() }
|
||||
var developerMode: Bool { self.settings.developer_mode }
|
||||
var relayModelCache: RelayModelCache
|
||||
var relayFilters: RelayFilters
|
||||
|
||||
var nwcWallet: WalletConnectURL? {
|
||||
guard let nwcString = self.settings.nostr_wallet_connect else { return nil }
|
||||
return WalletConnectURL(str: nwcString)
|
||||
}
|
||||
}
|
||||
}
|
||||
133
damus/Core/Storage/DamusUserDefaults.swift
Normal file
133
damus/Core/Storage/DamusUserDefaults.swift
Normal file
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// DamusUserDefaults.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-11-25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// # DamusUserDefaults
|
||||
///
|
||||
/// This struct acts like a UserDefaults object, but is also capable of automatically mirroring values to a separate store.
|
||||
///
|
||||
/// It works by using a specific store container as the main source of truth, and by optionally mirroring values to a different container if needed.
|
||||
///
|
||||
/// This is useful when the data of a UserDefaults object needs to be accessible from another store container,
|
||||
/// as it offers ways to automatically mirror information over a different container (e.g. When using app extensions)
|
||||
///
|
||||
/// Since it mirrors items instead of migrating them, this object can be used in a backwards compatible manner.
|
||||
///
|
||||
/// The easiest way to use this is to use `DamusUserDefaults.standard` as a drop-in replacement for `UserDefaults.standard`
|
||||
/// Or, you can initialize a custom object with customizable stores.
|
||||
struct DamusUserDefaults {
|
||||
|
||||
// MARK: - Helper data structures
|
||||
|
||||
enum Store: Equatable {
|
||||
case standard
|
||||
case shared
|
||||
case custom(UserDefaults)
|
||||
|
||||
func get_user_defaults() -> UserDefaults? {
|
||||
switch self {
|
||||
case .standard:
|
||||
return UserDefaults.standard
|
||||
case .shared:
|
||||
return UserDefaults(suiteName: Constants.DAMUS_APP_GROUP_IDENTIFIER)
|
||||
case .custom(let user_defaults):
|
||||
return user_defaults
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DamusUserDefaultsError: Error {
|
||||
case cannot_initialize_user_defaults
|
||||
case cannot_mirror_main_user_defaults
|
||||
}
|
||||
|
||||
// MARK: - Stored properties
|
||||
|
||||
private let main: UserDefaults
|
||||
private let mirrors: [UserDefaults]
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
init?(main: Store, mirror mirrors: [Store] = []) throws {
|
||||
guard let main_user_defaults = main.get_user_defaults() else { throw DamusUserDefaultsError.cannot_initialize_user_defaults }
|
||||
let mirror_user_defaults: [UserDefaults] = try mirrors.compactMap({ mirror_store in
|
||||
guard let mirror_user_default = mirror_store.get_user_defaults() else {
|
||||
throw DamusUserDefaultsError.cannot_initialize_user_defaults
|
||||
}
|
||||
guard mirror_store != main else {
|
||||
throw DamusUserDefaultsError.cannot_mirror_main_user_defaults
|
||||
}
|
||||
return mirror_user_default
|
||||
})
|
||||
|
||||
self.main = main_user_defaults
|
||||
self.mirrors = mirror_user_defaults
|
||||
}
|
||||
|
||||
// MARK: - Functions for feature parity with UserDefaults
|
||||
|
||||
func string(forKey defaultName: String) -> String? {
|
||||
let value = self.main.string(forKey: defaultName)
|
||||
self.mirror(value, forKey: defaultName)
|
||||
return value
|
||||
}
|
||||
|
||||
func set(_ value: Any?, forKey defaultName: String) {
|
||||
self.main.set(value, forKey: defaultName)
|
||||
self.mirror(value, forKey: defaultName)
|
||||
}
|
||||
|
||||
func removeObject(forKey defaultName: String) {
|
||||
self.main.removeObject(forKey: defaultName)
|
||||
self.mirror_object_removal(forKey: defaultName)
|
||||
}
|
||||
|
||||
func object(forKey defaultName: String) -> Any? {
|
||||
let value = self.main.object(forKey: defaultName)
|
||||
self.mirror(value, forKey: defaultName)
|
||||
return value
|
||||
}
|
||||
|
||||
// MARK: - Mirroring utilities
|
||||
|
||||
private func mirror(_ value: Any?, forKey defaultName: String) {
|
||||
for mirror in self.mirrors {
|
||||
mirror.set(value, forKey: defaultName)
|
||||
}
|
||||
}
|
||||
|
||||
private func mirror_object_removal(forKey defaultName: String) {
|
||||
for mirror in self.mirrors {
|
||||
mirror.removeObject(forKey: defaultName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Default convenience objects
|
||||
|
||||
/// # Convenience objects
|
||||
///
|
||||
/// - `DamusUserDefaults.standard`: will detect the bundle identifier and pick an appropriate object. You should generally use this one.
|
||||
/// - `DamusUserDefaults.app`: stores things on its own container, and mirrors them to the shared container.
|
||||
/// - `DamusUserDefaults.shared`: stores things on the shared container and does no mirroring
|
||||
extension DamusUserDefaults {
|
||||
static let app: DamusUserDefaults = try! DamusUserDefaults(main: .standard, mirror: [.shared])! // Since the underlying behavior is very static, the risk of crashing on force unwrap is low
|
||||
static let shared: DamusUserDefaults = try! DamusUserDefaults(main: .shared)! // Since the underlying behavior is very static, the risk of crashing on force unwrap is low
|
||||
static var standard: DamusUserDefaults {
|
||||
get {
|
||||
switch Bundle.main.bundleIdentifier {
|
||||
case Constants.MAIN_APP_BUNDLE_IDENTIFIER:
|
||||
return Self.app
|
||||
case Constants.NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER:
|
||||
return Self.shared
|
||||
default:
|
||||
return Self.shared
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
damus/Core/Storage/KeychainStorage.swift
Normal file
73
damus/Core/Storage/KeychainStorage.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// KeychainStorage.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Bryan Montz on 5/2/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
@propertyWrapper struct KeychainStorage {
|
||||
let account: String
|
||||
private let service = "damus"
|
||||
|
||||
var wrappedValue: String? {
|
||||
get {
|
||||
let query = [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: account,
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecReturnData: true,
|
||||
kSecMatchLimit: kSecMatchLimitOne
|
||||
] as [CFString: Any] as CFDictionary
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query, &result)
|
||||
|
||||
if status == errSecSuccess, let data = result as? Data {
|
||||
return String(data: data, encoding: .utf8)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
if let newValue {
|
||||
let query = [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: account,
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecValueData: newValue.data(using: .utf8) as Any
|
||||
] as [CFString: Any] as CFDictionary
|
||||
|
||||
var status = SecItemAdd(query, nil)
|
||||
|
||||
if status == errSecDuplicateItem {
|
||||
let query = [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: account,
|
||||
kSecClass: kSecClassGenericPassword
|
||||
] as [CFString: Any] as CFDictionary
|
||||
|
||||
let updates = [
|
||||
kSecValueData: newValue.data(using: .utf8) as Any
|
||||
] as CFDictionary
|
||||
|
||||
status = SecItemUpdate(query, updates)
|
||||
}
|
||||
} else {
|
||||
let query = [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: account,
|
||||
kSecClass: kSecClassGenericPassword
|
||||
] as [CFString: Any] as CFDictionary
|
||||
|
||||
_ = SecItemDelete(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(account: String) {
|
||||
self.account = account
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user