mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 11:54:07 +00:00
ios: process notifications, suspend app, notifications settings UI (#754)
This commit is contained in:
@@ -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))")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user