ios: process notifications, suspend app, notifications settings UI (#754)

This commit is contained in:
Evgeny Poberezkin
2022-06-27 10:28:30 +01:00
committed by GitHub
parent 463f644bce
commit 6d25991417
13 changed files with 366 additions and 89 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
if useNotifications {
Task {
do {
m.tokenStatus = try await apiRegisterToken(token: token)
m.tokenStatus = try await apiRegisterToken(token: token, notificationMode: .instant)
} catch {
logger.error("apiRegisterToken error: \(responseError(error))")
}
+9
View File
@@ -17,11 +17,14 @@ private let waitForMessages: TimeInterval = 6
private let bgRefreshInterval: TimeInterval = 450
private let maxTimerCount = 9
class BGManager {
static let shared = BGManager()
var chatReceiver: ChatReceiver?
var bgTimer: Timer?
var completed = true
var timerCount = 0
func register() {
logger.debug("BGManager.register")
@@ -60,6 +63,8 @@ class BGManager {
self.chatReceiver = nil
self.bgTimer?.invalidate()
self.bgTimer = nil
self.timerCount = 0
suspendBgRefresh()
complete()
}
}
@@ -82,14 +87,18 @@ class BGManager {
return
}
logger.debug("BGManager.receiveMessages: starting chat")
activateChat(appState: .bgRefresh)
let cr = ChatReceiver()
self.chatReceiver = cr
cr.start()
RunLoop.current.add(Timer(timeInterval: 2, repeats: true) { timer in
logger.debug("BGManager.receiveMessages: timer")
self.bgTimer = timer
self.timerCount += 1
if cr.lastMsgTime.distance(to: Date.now) >= waitForMessages {
completeReceiving("timer (no messages after \(waitForMessages) seconds)")
} else if self.timerCount >= maxTimerCount {
completeReceiving("timer (called \(maxTimerCount) times")
}
}, forMode: .default)
}
+2
View File
@@ -30,6 +30,8 @@ final class ChatModel: ObservableObject {
@Published var appOpenUrl: URL?
@Published var deviceToken: String?
@Published var tokenStatus = NtfTknStatus.new
@Published var notificationMode = NotificationMode.off
@Published var notificationPreview: NotificationPreviewMode? = .message
// current WebRTC call
@Published var callInvitations: Dictionary<ChatId, CallInvitation> = [:]
@Published var activeCall: Call?
+13 -7
View File
@@ -149,10 +149,16 @@ func apiStopChat() async throws {
}
}
func apiSetAppPhase(appPhase: AgentPhase) {
let r = chatSendCmdSync(.apiSetAppPhase(appPhase: appPhase))
func apiActivateChat() {
let r = chatSendCmdSync(.apiActivateChat)
if case .cmdOk = r { return }
logger.error("apiSetAppPhase error: \(String(describing: r))")
logger.error("apiActivateChat error: \(String(describing: r))")
}
func apiSuspendChat(timeoutMicroseconds: Int) {
let r = chatSendCmdSync(.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds))
if case .cmdOk = r { return }
logger.error("apiSuspendChat error: \(String(describing: r))")
}
func apiSetFilesFolder(filesFolder: String) throws {
@@ -220,8 +226,8 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM
throw r
}
func apiRegisterToken(token: String) async throws -> NtfTknStatus {
let r = await chatSendCmd(.apiRegisterToken(token: token))
func apiRegisterToken(token: String, notificationMode: NotificationMode) async throws -> NtfTknStatus {
let r = await chatSendCmd(.apiRegisterToken(token: token, notificationMode: notificationMode))
if case let .ntfTokenStatus(status) = r { return status }
throw r
}
@@ -702,8 +708,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.callCommand = .end
// CallController.shared.reportCallRemoteEnded(call: call)
}
case let .appPhase(appPhase):
appStateGroupDefault.set(AppState(appPhase: appPhase))
case .chatSuspended:
chatSuspended()
default:
logger.debug("unsupported event: \(res.responseType)")
}
+52
View File
@@ -0,0 +1,52 @@
//
// SuspendChat.swift
// SimpleX (iOS)
//
// Created by Evgeny on 26/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SimpleXChat
private let suspendLockQueue = DispatchQueue(label: "chat.simplex.app.suspend.lock")
let appSuspendTimeout: Int = 15 // seconds
let bgSuspendTimeout: Int = 5 // seconds
private func _suspendChat(timeout: Int) {
appStateGroupDefault.set(.suspending)
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
let endTask = beginBGTask(chatSuspended)
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask)
}
func suspendChat() {
suspendLockQueue.sync {
_suspendChat(timeout: appSuspendTimeout)
}
}
func suspendBgRefresh() {
suspendLockQueue.sync {
if case .bgRefresh = appStateGroupDefault.get() {
_suspendChat(timeout: bgSuspendTimeout)
}
}
}
func chatSuspended() {
suspendLockQueue.sync {
if case .suspending = appStateGroupDefault.get() {
appStateGroupDefault.set(.suspended)
}
}
}
func activateChat(appState: AppState = .active) {
suspendLockQueue.sync {
appStateGroupDefault.set(appState)
apiActivateChat()
}
}
+8 -15
View File
@@ -49,15 +49,20 @@ struct SimpleXApp: App {
logger.debug("scenePhase \(String(describing: scenePhase))")
switch (phase) {
case .background:
pauseApp()
suspendChat()
if chatModel.chatRunning == true {
ChatReceiver.shared.stop()
}
BGManager.shared.schedule()
if userAuthorized == true {
enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
case .active:
appStateGroupDefault.set(.active)
apiSetAppPhase(appPhase: .active)
activateChat()
if chatModel.chatRunning == true {
ChatReceiver.shared.start()
}
doAuthenticate = authenticationExpired()
default:
break
@@ -88,18 +93,6 @@ struct SimpleXApp: App {
}
}
private func pauseApp() {
appStateGroupDefault.set(.pausing)
apiSetAppPhase(appPhase: .paused)
let endTask = beginBGTask {
if appStateGroupDefault.get() != .active {
appStateGroupDefault.set(.suspending)
apiSetAppPhase(appPhase: .suspended)
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask)
}
private func authenticationExpired() -> Bool {
if let enteredBackground = enteredBackground {
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
@@ -221,6 +221,7 @@ struct DatabaseView: View {
Task {
do {
try await apiStopChat()
ChatReceiver.shared.stop()
await MainActor.run { m.chatRunning = false }
} catch let error {
await MainActor.run {
@@ -315,6 +316,7 @@ struct DatabaseView: View {
_ = try apiStartChat()
runChat = true
m.chatRunning = true
ChatReceiver.shared.start()
chatLastStartGroupDefault.set(Date.now)
} catch let error {
runChat = false
@@ -0,0 +1,163 @@
//
// NotificationsView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 26/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct NotificationsView: View {
@EnvironmentObject var m: ChatModel
@State private var notificationMode: NotificationMode?
@State private var alert: NotificationMode?
var body: some View {
List {
Section {
NavigationLink {
List {
Section {
SelectionListView(list: NotificationMode.values, selection: $notificationMode) { mode in
alert = mode
}
} footer: {
VStack(alignment: .leading) {
if let mode = notificationMode {
Text(ntfModeDescription(mode))
}
}
.font(.callout)
.padding(.top, 1)
}
}
.navigationTitle("Send notifications")
.navigationBarTitleDisplayMode(.inline)
.alert(item: $alert) { notificationAlert($0) }
.onAppear { notificationMode = m.notificationMode }
} label: {
HStack {
Text("Send notifications")
Spacer()
Text(m.notificationMode.label)
}
}
NavigationLink {
List {
Section {
SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview)
} footer: {
}
}
.navigationTitle("Show preview")
.navigationBarTitleDisplayMode(.inline)
} label: {
HStack {
Text("Show preview")
Spacer()
Text(m.notificationPreview?.label ?? "")
}
}
} header: {
Text("Message notifications")
}
}
}
private func notificationAlert(_ mode: NotificationMode) -> Alert {
switch mode {
case .off:
return Alert(
title: Text("Turn off notifications?"),
message: Text(ntfModeDescription(mode)),
primaryButton: .default(Text("Turn off")) {
notificationMode = mode
m.notificationMode = mode
},
secondaryButton: .cancel() {
notificationMode = m.notificationMode
}
)
case .periodic:
return Alert(
title: Text("Enable periodic notifcations?"),
message: Text(ntfModeDescription(mode)),
primaryButton: .default(Text("Enable")) {
notificationMode = mode
m.notificationMode = mode
},
secondaryButton: .cancel() {
notificationMode = m.notificationMode
}
)
case .instant:
return Alert(
title: Text("Enable instant notifications?"),
message: Text(ntfModeDescription(mode)),
primaryButton: .default(Text("Enable")) {
notificationMode = mode
m.notificationMode = mode
},
secondaryButton: .cancel() {
notificationMode = m.notificationMode
}
)
}
}
}
func ntfModeDescription(_ mode: NotificationMode) -> LocalizedStringKey {
switch mode {
case .off: return "**Maximum privacy**: push notifications are off.\nNo meta-data is shared with SimpleX Chat notification server."
case .periodic: return "**High privacy**: new messages are checked every 20 minutes.\nYour device token is shared with SimpleX Chat notification server, but it cannot see how many connections you have or how many messages you receive."
case .instant: return "**Medium privacy** (recommended): notifications are sent instantly.\nYour device token and notifications are sent to SimpleX Chat notification server, but it cannot access the message content, size or who is it from."
}
}
struct SelectionListView<Item: SelectableItem>: View {
var list: [Item]
@Binding var selection: Item?
var onSelection: ((Item) -> Void)?
@State private var tapped: Item? = nil
var body: some View {
ForEach(list) { item in
HStack {
Text(item.label)
Spacer()
if selection == item {
Image(systemName: "checkmark")
.resizable().scaledToFit().frame(width: 16)
.foregroundColor(.accentColor)
}
}
.contentShape(Rectangle())
.listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground))
.onTapGesture {
if let f = onSelection {
f(item)
} else {
selection = item
}
}
._onButtonGesture { down in
if down {
tapped = item
} else {
tapped = nil
}
} perform: {}
}
.environment(\.editMode, .constant(.active))
}
}
struct NotificationsView_Previews: PreviewProvider {
static var previews: some View {
NotificationsView()
}
}
@@ -95,14 +95,21 @@ struct SettingsView: View {
}
Section("Settings") {
NavigationLink {
NotificationsView()
.navigationTitle("Notifications")
} label: {
HStack {
notificationsIcon()
Text("Notifications")
}
}
if enableCalls {
NavigationLink {
CallSettings()
.navigationTitle("Your calls")
} label: {
settingsRow("video") {
Text("Audio & video calls")
}
settingsRow("video") { Text("Audio & video calls") }
}
}
NavigationLink {
@@ -269,7 +276,7 @@ struct SettingsView: View {
primaryButton: .destructive(Text("Confirm")) {
Task {
do {
chatModel.tokenStatus = try await apiRegisterToken(token: token)
chatModel.tokenStatus = try await apiRegisterToken(token: token, notificationMode: .instant)
} catch {
DispatchQueue.main.async {
useNotifications = false
+43 -29
View File
@@ -18,50 +18,60 @@ class NotificationService: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
logger.debug("NotificationService.didReceive")
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
let appState = appStateGroupDefault.get()
if appState.running {
switch appState {
case .suspended:
logger.debug("NotificationService: app is suspended")
self.contentHandler = contentHandler
receiveNtfMessages(request, contentHandler)
case .suspending:
self.contentHandler = contentHandler
receiveNtfMessages(request, contentHandler)
default:
print("userInfo", request.content.userInfo)
contentHandler(request.content)
return
}
logger.debug("NotificationService: app is in the background")
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
}
func receiveNtfMessages(_ request: UNNotificationRequest, _ contentHandler: @escaping (UNNotificationContent) -> Void) {
let userInfo = request.content.userInfo
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any],
let nonce = ntfData["nonce"] as? String,
let encNtfInfo = ntfData["message"] as? String,
let _ = startChat() {
apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)
if let content = receiveMessages() {
contentHandler(content)
return
if let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
if let content = receiveMessageForNotification() {
contentHandler(content)
} else if let connEntity = ntfMsgInfo.connEntity {
switch connEntity {
case let .rcvDirectMsgConnection(_, contact):
()
case let .rcvGroupMsgConnection(_, groupInfo, groupMember):
()
case let .sndFileConnection(_, sndFileTransfer):
()
case let .rcvFileConnection(_, rcvFileTransfer):
()
case let .userContactConnection(_, userContact):
()
}
contentHandler(request.content)
}
}
}
if let bestAttemptContent = bestAttemptContent {
// Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
contentHandler(bestAttemptContent)
}
}
override func serviceExtensionTimeWillExpire() {
logger.debug("NotificationService.serviceExtensionTimeWillExpire")
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
if let contentHandler = self.contentHandler, let bestAttemptContent = self.bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
func receivedAppMachMessage(msgId: Int32, msg: String) -> String? {
logger.debug("MachMessenger: receivedAppMachMessage \"\(msg)\" from App, replying")
return "reply from NSE to: \(msg)"
}
func startChat() -> User? {
hs_init(0, nil)
if let user = apiGetActiveUser() {
@@ -80,7 +90,7 @@ func startChat() -> User? {
return nil
}
func receiveMessages() -> UNNotificationContent? {
func receiveMessageForNotification() -> UNNotificationContent? {
logger.debug("NotificationService receiveMessages started")
while true {
if let res = recvSimpleXMsg() {
@@ -149,12 +159,16 @@ func apiSetFilesFolder(filesFolder: String) throws {
throw r
}
func apiGetNtfMessage(nonce: String, encNtfInfo: String) {
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r {
if let connEntity = connEntity { print("connEntity", connEntity) }
if let msgTs = msgTs { print("msgTs", msgTs) }
print("ntfMessages", ntfMessages)
return
return NtfMessages(connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages)
}
return nil
}
struct NtfMessages {
var connEntity: ConnectionEntity?
var msgTs: Date?
var ntfMessages: [NtfMsgInfo]
}
@@ -63,6 +63,8 @@
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; };
5CB0BA962827143500B3292C /* MakeConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA952827143500B3292C /* MakeConnection.swift */; };
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; };
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; };
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; };
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D327A853F100ACCCDD /* SettingsButton.swift */; };
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; };
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; };
@@ -236,6 +238,8 @@
5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; };
5CB0BA952827143500B3292C /* MakeConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeConnection.swift; sourceTree = "<group>"; };
5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = "<group>"; };
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; };
5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = "<group>"; };
5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
@@ -405,6 +409,7 @@
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */,
5C35CFC727B2782E00FB6C6D /* BGManager.swift */,
5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */,
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */,
);
path = Model;
sourceTree = "<group>";
@@ -506,6 +511,7 @@
children = (
5CB924D327A853F100ACCCDD /* SettingsButton.swift */,
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
5CB924E327A8683A00ACCCDD /* UserAddress.swift */,
@@ -832,6 +838,7 @@
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */,
5CB0BA962827143500B3292C /* MakeConnection.swift in Sources */,
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */,
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */,
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */,
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
@@ -847,6 +854,7 @@
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */,
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */,
5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */,
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */,
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */,
+53 -14
View File
@@ -7,6 +7,7 @@
//
import Foundation
import SwiftUI
let jsonDecoder = getJSONDecoder()
let jsonEncoder = getJSONEncoder()
@@ -16,7 +17,8 @@ public enum ChatCommand {
case createActiveUser(profile: Profile)
case startChat(subscribe: Bool)
case apiStopChat
case apiSetAppPhase(appPhase: AgentPhase)
case apiActivateChat
case apiSuspendChat(timeoutMicroseconds: Int)
case setFilesFolder(filesFolder: String)
case apiExportArchive(config: ArchiveConfig)
case apiImportArchive(config: ArchiveConfig)
@@ -26,7 +28,7 @@ public enum ChatCommand {
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent)
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
case apiRegisterToken(token: String)
case apiRegisterToken(token: String, notificationMode: NotificationMode)
case apiVerifyToken(token: String, code: String, nonce: String)
case apiIntervalNofication(token: String, interval: Int)
case apiDeleteToken(token: String)
@@ -62,7 +64,8 @@ public enum ChatCommand {
case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)"
case let .startChat(subscribe): return "/_start subscribe=\(subscribe ? "on" : "off")"
case .apiStopChat: return "/_stop"
case let .apiSetAppPhase(appPhase): return "/_app phase \(appPhase.rawValue)"
case .apiActivateChat: return "/_app activate"
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
@@ -74,7 +77,7 @@ public enum ChatCommand {
return "/_send \(ref(type, id)) json \(msg)"
case let .apiUpdateChatItem(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)"
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
case let .apiRegisterToken(token): return "/_ntf register apns \(token)"
case let .apiRegisterToken(token, notificationMode): return "/_ntf register apns \(token) \(notificationMode.rawValue)"
case let .apiVerifyToken(token, code, nonce): return "/_ntf verify apns \(token) \(code) \(nonce)"
case let .apiIntervalNofication(token, interval): return "/_ntf interval apns \(token) \(interval)"
case let .apiDeleteToken(token): return "/_ntf delete apns \(token)"
@@ -112,7 +115,8 @@ public enum ChatCommand {
case .createActiveUser: return "createActiveUser"
case .startChat: return "startChat"
case .apiStopChat: return "apiStopChat"
case .apiSetAppPhase: return "apiSetAppPhase"
case .apiActivateChat: return "apiActivateChat"
case .apiSuspendChat: return "apiSuspendChat"
case .setFilesFolder: return "setFilesFolder"
case .apiExportArchive: return "apiExportArchive"
case .apiImportArchive: return "apiImportArchive"
@@ -172,7 +176,7 @@ public enum ChatResponse: Decodable, Error {
case chatStarted
case chatRunning
case chatStopped
case appPhase(appPhase: AgentPhase)
case chatSuspended
case apiChats(chats: [ChatData])
case apiChat(chat: ChatData)
case userSMPServers(smpServers: [String])
@@ -236,7 +240,7 @@ public enum ChatResponse: Decodable, Error {
case .chatStarted: return "chatStarted"
case .chatRunning: return "chatRunning"
case .chatStopped: return "chatStopped"
case .appPhase: return "appPhase"
case .chatSuspended: return "chatSuspended"
case .apiChats: return "apiChats"
case .apiChat: return "apiChat"
case .userSMPServers: return "userSMPServers"
@@ -301,7 +305,7 @@ public enum ChatResponse: Decodable, Error {
case .chatStarted: return noDetails
case .chatRunning: return noDetails
case .chatStopped: return noDetails
case let .appPhase(appPhase): return appPhase.rawValue
case .chatSuspended: return noDetails
case let .apiChats(chats): return String(describing: chats)
case let .apiChat(chat): return String(describing: chat)
case let .userSMPServers(smpServers): return String(describing: smpServers)
@@ -367,12 +371,6 @@ struct ComposedMessage: Encodable {
var msgContent: MsgContent
}
public enum AgentPhase: String, Decodable {
case active = "ACTIVE"
case paused = "PAUSED"
case suspended = "SUSPENDED"
}
public struct ArchiveConfig: Encodable {
var archivePath: String
var disableCompression: Bool?
@@ -383,6 +381,47 @@ public struct ArchiveConfig: Encodable {
}
}
public protocol SelectableItem: Hashable, Identifiable {
var label: LocalizedStringKey { get }
static var values: [Self] { get }
}
public enum NotificationMode: String, SelectableItem {
case off = "OFF"
case periodic = "PERIODIC"
case instant = "INSTANT"
public var label: LocalizedStringKey {
switch self {
case .off: return "Off"
case .periodic: return "Periodically"
case .instant: return "Instantly"
}
}
public var id: String { self.rawValue }
public static var values: [NotificationMode] = [.instant, .periodic, .off]
}
public enum NotificationPreviewMode: String, SelectableItem {
case hidden
case contact
case message
public var label: LocalizedStringKey {
switch self {
case .hidden: return "Hidden"
case .contact: return "Contact"
case .message: return "Message"
}
}
public var id: String { self.rawValue }
public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden]
}
public func decodeJSON<T: Decodable>(_ json: String) -> T? {
if let data = json.data(using: .utf8) {
return try? jsonDecoder.decode(T.self, from: data)
+1 -19
View File
@@ -19,27 +19,9 @@ public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)!
public enum AppState: String {
case active
case pausing
case paused
case bgRefresh
case suspending
case suspended
public init(appPhase: AgentPhase) {
switch appPhase {
case .active: self = .active
case .paused: self = .paused
case .suspended: self = .suspended
}
}
public var running: Bool {
switch self {
case .paused: return false
case .suspending: return false
case .suspended: return false
default: return true
}
}
}
public enum DBContainer: String {