Skip to content

Commit

Permalink
Finish logo shenanegins, add notification settings, refactor usersett…
Browse files Browse the repository at this point in the history
…ingstore
  • Loading branch information
IAmTomahawkx committed Jul 19, 2024
1 parent 38ddcab commit a891529
Show file tree
Hide file tree
Showing 28 changed files with 510 additions and 94 deletions.
10 changes: 9 additions & 1 deletion Revolt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
178E26BC2B1542820015CAC7 /* PageToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178E26BB2B1542820015CAC7 /* PageToolbar.swift */; };
17966B652AE0842A00BAEC58 /* ChannelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17966B642AE0842A00BAEC58 /* ChannelIcon.swift */; };
17A589FA2C44781F00B9D85A /* AnyTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A589F92C44781F00B9D85A /* AnyTransition.swift */; };
17A7FE372C49DE6F0083B22F /* Member.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A7FE362C49DE6F0083B22F /* Member.swift */; };
17A7FE392C49EBD20083B22F /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A7FE382C49EBD20083B22F /* Image.swift */; };
17B375812C41FEC300FC7E6E /* Types.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17DE7DB22BFAEC3F00C99188 /* Types.framework */; };
17B7128D2B03E9D700CFF61C /* FriendsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B7128C2B03E9D700CFF61C /* FriendsList.swift */; };
17BADE012B7019270021BB62 /* EmojiPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BADE002B7019270021BB62 /* EmojiPicker.swift */; };
Expand Down Expand Up @@ -220,6 +222,8 @@
178E26BB2B1542820015CAC7 /* PageToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageToolbar.swift; sourceTree = "<group>"; };
17966B642AE0842A00BAEC58 /* ChannelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelIcon.swift; sourceTree = "<group>"; };
17A589F92C44781F00B9D85A /* AnyTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyTransition.swift; sourceTree = "<group>"; };
17A7FE362C49DE6F0083B22F /* Member.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Member.swift; sourceTree = "<group>"; };
17A7FE382C49EBD20083B22F /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
17B1A0462BFC471C005D4664 /* Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = "<group>"; };
17B7128C2B03E9D700CFF61C /* FriendsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendsList.swift; sourceTree = "<group>"; };
17BADE002B7019270021BB62 /* EmojiPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPicker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -318,6 +322,8 @@
172F2CFE2C22ED3600948C00 /* Bundle.swift */,
170C23D72C224BA40057E399 /* View.swift */,
17A589F92C44781F00B9D85A /* AnyTransition.swift */,
17A7FE362C49DE6F0083B22F /* Member.swift */,
17A7FE382C49EBD20083B22F /* Image.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -557,9 +563,9 @@
D49B705129C4D3FE009494A5 /* Revolt */ = {
isa = PBXGroup;
children = (
173190012B754BB900B6DA49 /* Resources */,
17772C162C30AF6D000D1EDA /* Delegates */,
170C23D82C224BA40057E399 /* Extensions */,
173190012B754BB900B6DA49 /* Resources */,
17DFB4622AE06A3200E1D417 /* Api */,
17DFB4612AE06A0F00E1D417 /* Pages */,
175E01DC2ADAB3EA004F6431 /* Components */,
Expand Down Expand Up @@ -826,6 +832,7 @@
17BF54CD2B1785E200178866 /* HomeWelcome.swift in Sources */,
17F502542B9BFB1000A3022D /* AddFriend.swift in Sources */,
17C9A3502B0580D10043A387 /* Welcome.swift in Sources */,
17A7FE392C49EBD20083B22F /* Image.swift in Sources */,
17E019CF2AF146AD00AB4663 /* About.swift in Sources */,
1773C03D2C07DD1F007B8867 /* MessageableChannel.swift in Sources */,
DAAA4C0B29F2CF1200F41E52 /* Websocket.swift in Sources */,
Expand All @@ -836,6 +843,7 @@
17E28E082AF319D500F6069F /* AddServer.swift in Sources */,
17429D972B4C91170036105A /* ChannelSettings.swift in Sources */,
17BC476B2B9CB51200A593DA /* ShareInviteSheet.swift in Sources */,
17A7FE372C49DE6F0083B22F /* Member.swift in Sources */,
1739DB962B08E53B00D23DAD /* MessageBadge.swift in Sources */,
175E01DE2ADAB3F6004F6431 /* LazyImage.swift in Sources */,
1759C39C2B291E2F006E6BBE /* SystemMessageView.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions Revolt/Api/Http.swift
Original file line number Diff line number Diff line change
Expand Up @@ -342,4 +342,8 @@ struct HTTPClient {
func uploadNotificationToken(token: String) async -> Result<EmptyResponse, RevoltError> {
await req(method: .post, route: "/push/subscribe", parameters: ["endpoint": "apn", "p256dh": "", "auth": token])
}

func revokeNotificationToken() async -> Result<EmptyResponse, RevoltError> {
await req(method: .post, route: "/push/unsubscribe")
}
}
218 changes: 186 additions & 32 deletions Revolt/Api/UserSettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import Types

let logger = Logger(subsystem: "chat.revolt.app", category: "settingsStore")


// MARK: - Discardable caches

struct AccountSettingsMFAStatus: Codable {
var email_otp: Bool
var trusted_handover: Bool
Expand All @@ -32,76 +35,201 @@ struct UserSettingsAccountData: Codable {
}

@Observable
class UserSettingsStore: Codable {
class DiscardableUserStore: Codable {
var user: Types.User?
var accountData: UserSettingsAccountData?

/// This is null when we havent asked for permission yet
var rejectedRemoteNotifications: Bool?

fileprivate func clear() {
user = nil
accountData = nil
rejectedRemoteNotifications = nil
}

enum CodingKeys: String, CodingKey {
case _user = "user"
case _accountData = "accountData"
}
}

// MARK: - Persistent settings

@Observable
class NotificationOptionsData: Codable {
var keyWasSet: () -> Void = {}

var rejectedRemoteNotifications: Bool {
didSet(newSetting) {
keyWasSet()
}
}

var wantsNotificationsWhileAppRunning: Bool {
didSet(newSetting) {
keyWasSet()
}
}

init(keyWasSet: @escaping () -> Void, rejectedRemoteNotifications: Bool, wantsNotificationsWhileAppRunning: Bool) {
self.rejectedRemoteNotifications = rejectedRemoteNotifications
self.wantsNotificationsWhileAppRunning = wantsNotificationsWhileAppRunning

self.keyWasSet = keyWasSet
}

init(keyWasSet: @escaping () -> Void) {
self.rejectedRemoteNotifications = true
self.wantsNotificationsWhileAppRunning = true

self.keyWasSet = keyWasSet
}

init() {
self.rejectedRemoteNotifications = true
self.wantsNotificationsWhileAppRunning = true
}

enum CodingKeys: String, CodingKey {
case _rejectedRemoteNotifications = "rejectedRemoteNotifications"
case _wantsNotificationsWhileAppRunning = "wantsNotificationsWhileAppRunning"
}
}

@Observable
class PersistentUserSettingsStore: Codable {
var notifications: NotificationOptionsData

init(keyWasSet: @escaping () -> Void, notifications: NotificationOptionsData) {
self.notifications = notifications
}

init() {
self.notifications = NotificationOptionsData()
}

fileprivate func updateDecodeWithCallback(keyWasSet: @escaping () -> Void) {
self._notifications.keyWasSet = keyWasSet
}

enum CodingKeys: String, CodingKey {
case _notifications = "notifications"
}
}

class UserSettingsData {
enum SettingsFetchState {
case fetching, failed, cached
}

var viewState: ViewState?
var store: UserSettingsStore
var dataState: SettingsFetchState

var cache: DiscardableUserStore
var cacheState: SettingsFetchState

var store: PersistentUserSettingsStore

static var cacheFile: URL? {
if let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
let revoltDir = caches.appendingPathComponent("RevoltCaches", conformingTo: .directory)
let resp = revoltDir.appendingPathComponent("settingsCache", conformingTo: .json)
if let caches = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
let revoltDir = caches.appendingPathComponent(Bundle.main.bundleIdentifier!, conformingTo: .directory)
let resp = revoltDir.appendingPathComponent("userInfoCache", conformingTo: .json)
return resp
}
return nil
}

static var storeFile: URL? {
if let caches = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
let revoltDir = caches.appendingPathComponent(Bundle.main.bundleIdentifier!, conformingTo: .directory)
let resp = revoltDir.appendingPathComponent("userSettings", conformingTo: .json)
return resp
}
return nil
}

init(viewState: ViewState?, store: UserSettingsStore) {
init(viewState: ViewState?, cache: DiscardableUserStore, store: PersistentUserSettingsStore) {
self.viewState = viewState
self.cache = cache
self.cacheState = .cached
self.store = store
self.dataState = .cached
self.store.updateDecodeWithCallback(keyWasSet: storeKeyWasSet)
}

init(viewState: ViewState?, store: PersistentUserSettingsStore) {
self.viewState = viewState
self.cache = DiscardableUserStore()
self.cacheState = .fetching

self.store = store
self.store.updateDecodeWithCallback(keyWasSet: storeKeyWasSet)

createFetchTask()
}

init(viewState: ViewState?) {
self.viewState = viewState
self.store = UserSettingsStore()
self.dataState = .fetching
self.cache = DiscardableUserStore()
self.cacheState = .fetching

self.store = PersistentUserSettingsStore()
self.store.updateDecodeWithCallback(keyWasSet: storeKeyWasSet)

createFetchTask()
}

class func maybeRead(viewState: ViewState?) -> UserSettingsData {
let filePath = UserSettingsData.cacheFile!
var file = Data()
var cache: DiscardableUserStore? = nil
var store: PersistentUserSettingsStore? = nil

var fileContents: Data?
do {
file = try Data(contentsOf: filePath)
let filePath = UserSettingsData.cacheFile!
fileContents = try Data(contentsOf: filePath)
} catch {
logger.debug("settingsCache file does not exist")
return UserSettingsData(viewState: viewState)
logger.debug("settingsCache file does not exist, will rebuild. \(error.localizedDescription)")
}

do {
if fileContents != nil {
cache = try JSONDecoder().decode(DiscardableUserStore.self, from: fileContents!)
}
} catch {
logger.warning("Failed to parse the existing cache file. Will discard cache and rebuild. \(error.localizedDescription)")
}

var storefileContents: Data? = nil
do {
let data = try JSONDecoder().decode(UserSettingsStore.self, from: file)
return UserSettingsData(viewState: viewState, store: data)
let filePath = UserSettingsData.storeFile!
storefileContents = try Data(contentsOf: filePath)
} catch {
logger.warning("Failed to parse the existing cache file. Discarding file and rebuilding cache")
logger.warning("User settings have been removed. Will rebuild from scratch. \(error.localizedDescription)")
}

do {
if storefileContents != nil {
store = try JSONDecoder().decode(PersistentUserSettingsStore.self, from: storefileContents!)
}
} catch {
logger.warning("Failed to parse the existing settings store file. Settings may have been lost. \(error.localizedDescription)")
}

if store != nil && cache != nil {
return UserSettingsData(viewState: viewState, cache: cache!, store: store!)
} else if store != nil {
return UserSettingsData(viewState: viewState, store: store!)
} else {
return UserSettingsData(viewState: viewState)
}
}

private func storeKeyWasSet() {
DispatchQueue.main.async(qos: .utility) {
self.writeStoreToFile()
}
}

func createFetchTask() {
Task(priority: .medium, operation: self.fetchFromApi)
}


func fetchFromApi() async {
while viewState == nil {
try! await Task.sleep(for: .seconds(1))
Expand All @@ -112,16 +240,16 @@ class UserSettingsData {
}

do {
self.store.user = try await state.http.fetchSelf().get()
self.store.accountData = UserSettingsAccountData(
self.cache.user = try await state.http.fetchSelf().get()
self.cache.accountData = UserSettingsAccountData(
email: try await state.http.fetchAccount().get().email,
mfaStatus: try await state.http.fetchMFAStatus().get()
)

self.dataState = .cached
self.cacheState = .cached
writeCacheToFile()
} catch {
self.dataState = .failed
self.cacheState = .failed
let error = error as! RevoltError
switch error {
case .Alamofire(let afErr):
Expand All @@ -137,24 +265,22 @@ class UserSettingsData {
SentrySDK.capture(error: error)
}
default:
#if DEBUG
logger.error("An error occurred while fetching user settings: \(error.localizedDescription)")
#endif
SentrySDK.capture(error: error)
}
}
}

func writeCacheToFile() {
DispatchQueue.main.async(qos: .utility) {
if let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
let revoltDir = caches.appendingPathComponent("RevoltCaches", conformingTo: .directory)
if let caches = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
let revoltDir = caches.appendingPathComponent(Bundle.main.bundleIdentifier!, conformingTo: .directory)
do {
try FileManager.default.createDirectory(at: revoltDir, withIntermediateDirectories: false)
} catch {} //ignore error if it already exists

do {
let encoded = try JSONEncoder().encode(self.store)
let encoded = try JSONEncoder().encode(self.cache)
let filePath = UserSettingsData.cacheFile!
logger.debug("will write cache to: \(filePath.absoluteString)")
try encoded.write(to: filePath)
Expand All @@ -168,14 +294,42 @@ class UserSettingsData {
}
}

func writeStoreToFile() {
DispatchQueue.main.async(qos: .utility) {
if let caches = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
let revoltDir = caches.appendingPathComponent(Bundle.main.bundleIdentifier!, conformingTo: .directory)
do {
try FileManager.default.createDirectory(at: revoltDir, withIntermediateDirectories: false)
} catch {} //ignore error if it already exists
}
do {
let encoded = try JSONEncoder().encode(self.store)
let filePath = UserSettingsData.storeFile!
logger.debug("will write settings store to: \(filePath.absoluteString)")
try encoded.write(to: filePath)
} catch {
logger.error("Failed to serialize the settings store: \(error.localizedDescription)")
}
}
}

func destroyCache() {
DispatchQueue.main.async(qos: .utility, execute: deleteCacheFile)
self.store.clear()
self.cache.clear()
logger.debug("Queued cache file deletion, evicted from memory")
}

private func deleteCacheFile() {
let file = UserSettingsData.cacheFile!
try? FileManager.default.removeItem(at: file)
}

/// Called when logging out of the app
func isLoggingOut() {
destroyCache()
let file = UserSettingsData.storeFile!
try? FileManager.default.removeItem(at: file)
self.store = .init()
self.store.updateDecodeWithCallback(keyWasSet: storeKeyWasSet)
}
}
Loading

0 comments on commit a891529

Please sign in to comment.