mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-05 02:26:01 +00:00
ios: choose notifications mode during onboarding and after DB migration (#773)
This commit is contained in:
committed by
GitHub
parent
c619092464
commit
2c121b5731
@@ -543,8 +543,10 @@ func startChat() throws {
|
||||
registerToken(token: token)
|
||||
}
|
||||
withAnimation {
|
||||
m.onboardingStage = m.chats.isEmpty
|
||||
? .step3_MakeConnection
|
||||
m.onboardingStage = m.onboardingStage == .step2_CreateProfile
|
||||
? .step3_SetNotificationsMode
|
||||
: m.chats.isEmpty
|
||||
? .step4_MakeConnection
|
||||
: .onboardingComplete
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,19 +82,21 @@ struct SimpleXApp: App {
|
||||
let legacyDatabase = hasLegacyDatabase()
|
||||
if legacyDatabase, case .documents = dbContainerGroupDefault.get() {
|
||||
dbContainerGroupDefault.set(.documents)
|
||||
switch v3DBMigrationDefault.get() {
|
||||
case .migrated: ()
|
||||
default: v3DBMigrationDefault.set(.offer)
|
||||
}
|
||||
setMigrationState(.offer)
|
||||
logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db")
|
||||
} else {
|
||||
dbContainerGroupDefault.set(.group)
|
||||
v3DBMigrationDefault.set(.ready)
|
||||
setMigrationState(.ready)
|
||||
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db")
|
||||
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present")
|
||||
}
|
||||
}
|
||||
|
||||
private func setMigrationState(_ state: V3DBMigrationState) {
|
||||
if case .migrated = v3DBMigrationDefault.get() { return }
|
||||
v3DBMigrationDefault.set(state)
|
||||
}
|
||||
|
||||
private func authenticationExpired() -> Bool {
|
||||
if let enteredBackground = enteredBackground {
|
||||
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
|
||||
|
||||
@@ -34,7 +34,7 @@ struct ChatListView: View {
|
||||
}
|
||||
.onChange(of: chatModel.chats.isEmpty) { empty in
|
||||
if !empty { return }
|
||||
withAnimation { chatModel.onboardingStage = .step3_MakeConnection }
|
||||
withAnimation { chatModel.onboardingStage = .step4_MakeConnection }
|
||||
}
|
||||
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
|
||||
.onAppear() { connectViaUrl() }
|
||||
|
||||
@@ -43,13 +43,13 @@ struct MigrateToAppGroupView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
Text("Database migration").font(.largeTitle)
|
||||
Text("Push notifications").font(.largeTitle)
|
||||
|
||||
switch chatModel.v3DBMigration {
|
||||
case .offer:
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("To support instant push notifications the chat database has to be migrated.")
|
||||
Text("If you need to use the chat now tap **Skip** below (you will be offered to migrate the database when you restart the app).")
|
||||
Text("If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).")
|
||||
}
|
||||
.padding(.top, 56)
|
||||
center {
|
||||
@@ -109,6 +109,7 @@ struct MigrateToAppGroupView: View {
|
||||
do {
|
||||
resetChatCtrl()
|
||||
try initializeChat(start: true)
|
||||
chatModel.onboardingStage = .step3_SetNotificationsMode
|
||||
setV3DBMigration(.ready)
|
||||
} catch let error {
|
||||
dbContainerGroupDefault.set(.documents)
|
||||
@@ -117,7 +118,7 @@ struct MigrateToAppGroupView: View {
|
||||
}
|
||||
deleteOldArchive()
|
||||
} label: {
|
||||
Text("Start using chat")
|
||||
Text("Start chat")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
@@ -165,16 +166,16 @@ struct MigrateToAppGroupView: View {
|
||||
fatalError("Failed to start or load chats: \(responseError(error))")
|
||||
}
|
||||
} label: {
|
||||
Text("Skip and start using chat")
|
||||
Text("Do it later")
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
||||
}
|
||||
|
||||
private func setV3DBMigration(_ value: V3DBMigrationState) {
|
||||
chatModel.v3DBMigration = value
|
||||
v3DBMigrationDefault.set(value)
|
||||
private func setV3DBMigration(_ state: V3DBMigrationState) {
|
||||
chatModel.v3DBMigration = state
|
||||
v3DBMigrationDefault.set(state)
|
||||
}
|
||||
|
||||
func migrateDatabaseToV3() {
|
||||
@@ -242,6 +243,9 @@ func deleteOldArchive() {
|
||||
|
||||
struct MigrateToGroupView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MigrateToAppGroupView()
|
||||
let chatModel = ChatModel()
|
||||
chatModel.v3DBMigration = .migrated
|
||||
return MigrateToAppGroupView()
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ struct CreateProfile: View {
|
||||
do {
|
||||
m.currentUser = try apiCreateActiveUser(profile)
|
||||
try startChat()
|
||||
withAnimation { m.onboardingStage = .step3_MakeConnection }
|
||||
withAnimation { m.onboardingStage = .step3_SetNotificationsMode }
|
||||
|
||||
} catch {
|
||||
fatalError("Failed to create user or start chat: \(responseError(error))")
|
||||
|
||||
@@ -15,7 +15,8 @@ struct OnboardingView: View {
|
||||
switch onboarding {
|
||||
case .step1_SimpleXInfo: SimpleXInfo(onboarding: true)
|
||||
case .step2_CreateProfile: CreateProfile()
|
||||
case .step3_MakeConnection: MakeConnection()
|
||||
case .step3_SetNotificationsMode: SetNotificationsMode()
|
||||
case .step4_MakeConnection: MakeConnection()
|
||||
case .onboardingComplete: EmptyView()
|
||||
}
|
||||
}
|
||||
@@ -24,7 +25,8 @@ struct OnboardingView: View {
|
||||
enum OnboardingStage {
|
||||
case step1_SimpleXInfo
|
||||
case step2_CreateProfile
|
||||
case step3_MakeConnection
|
||||
case step3_SetNotificationsMode
|
||||
case step4_MakeConnection
|
||||
case onboardingComplete
|
||||
}
|
||||
|
||||
|
||||
112
apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift
Normal file
112
apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// NotificationsModeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 03/07/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct SetNotificationsMode: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@State private var notificationMode = NotificationsMode.instant
|
||||
@State private var showAlert: NotificationAlert?
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Push notifications").font(.largeTitle)
|
||||
|
||||
Text("Send notifications:")
|
||||
ForEach(NotificationsMode.values) { mode in
|
||||
NtfModeSelector(mode: mode, selection: $notificationMode)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
if let token = m.deviceToken {
|
||||
setNotificationsMode(token, notificationMode)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(title: "No device token!")
|
||||
}
|
||||
m.onboardingStage = m.chats.isEmpty
|
||||
? .step4_MakeConnection
|
||||
: .onboardingComplete
|
||||
} label: {
|
||||
if case .off = notificationMode {
|
||||
Text("Use chat")
|
||||
} else {
|
||||
Text("Enable notifications")
|
||||
}
|
||||
}
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) {
|
||||
switch mode {
|
||||
case .off:
|
||||
m.tokenStatus = .new
|
||||
m.notificationMode = .off
|
||||
default:
|
||||
Task {
|
||||
do {
|
||||
let status = try await apiRegisterToken(token: token, notificationMode: mode)
|
||||
await MainActor.run {
|
||||
m.tokenStatus = status
|
||||
m.notificationMode = mode
|
||||
}
|
||||
} catch let error {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Error enabling notifications",
|
||||
message: "\(responseError(error))"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NtfModeSelector: View {
|
||||
var mode: NotificationsMode
|
||||
@Binding var selection: NotificationsMode
|
||||
@State private var tapped = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(mode.label)
|
||||
.font(.headline)
|
||||
.foregroundColor(selection == mode ? .accentColor : .secondary)
|
||||
Text(ntfModeDescription(mode))
|
||||
.lineLimit(10)
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding(12)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(uiColor: tapped ? .secondarySystemFill : .systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 18))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(selection == mode ? Color.accentColor : Color(uiColor: .secondarySystemFill), lineWidth: 2)
|
||||
)
|
||||
._onButtonGesture { down in
|
||||
tapped = down
|
||||
if down { selection = mode }
|
||||
} perform: {}
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationsModeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SetNotificationsMode()
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ struct OnboardingActionButton: View {
|
||||
if m.currentUser == nil {
|
||||
actionButton("Create your profile", onboarding: .step2_CreateProfile)
|
||||
} else {
|
||||
actionButton("Make a private connection", onboarding: .step3_MakeConnection)
|
||||
actionButton("Make a private connection", onboarding: .step4_MakeConnection)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ struct NotificationsView: View {
|
||||
title: Text(ntfModeAlertTitle(mode)),
|
||||
message: Text(ntfModeDescription(mode)),
|
||||
primaryButton: .default(Text(mode == .off ? "Turn off" : "Enable")) {
|
||||
setNotificationsMode(mode, token)
|
||||
setNotificationsMode(token, mode)
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
notificationMode = m.notificationMode
|
||||
@@ -108,17 +108,19 @@ struct NotificationsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func setNotificationsMode(_ mode: NotificationsMode, _ token: DeviceToken) {
|
||||
private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) {
|
||||
Task {
|
||||
switch mode {
|
||||
case .off:
|
||||
do {
|
||||
try await apiDeleteToken(token: token)
|
||||
m.tokenStatus = .new
|
||||
notificationMode = .off
|
||||
m.notificationMode = .off
|
||||
await MainActor.run {
|
||||
m.tokenStatus = .new
|
||||
notificationMode = .off
|
||||
m.notificationMode = .off
|
||||
}
|
||||
} catch let error {
|
||||
DispatchQueue.main.async {
|
||||
await MainActor.run {
|
||||
let err = responseError(error)
|
||||
logger.error("apiDeleteToken error: \(err)")
|
||||
showAlert = .error(title: "Error deleting token", error: err)
|
||||
@@ -126,16 +128,17 @@ struct NotificationsView: View {
|
||||
}
|
||||
default:
|
||||
do {
|
||||
do {
|
||||
m.tokenStatus = try await apiRegisterToken(token: token, notificationMode: mode)
|
||||
let status = try await apiRegisterToken(token: token, notificationMode: mode)
|
||||
await MainActor.run {
|
||||
m.tokenStatus = status
|
||||
notificationMode = mode
|
||||
m.notificationMode = mode
|
||||
} catch let error {
|
||||
DispatchQueue.main.async {
|
||||
let err = responseError(error)
|
||||
logger.error("apiRegisterToken error: \(err)")
|
||||
showAlert = .error(title: "Error enabling notifications", error: err)
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
let err = responseError(error)
|
||||
logger.error("apiRegisterToken error: \(err)")
|
||||
showAlert = .error(title: "Error enabling notifications", error: err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,9 +148,9 @@ struct NotificationsView: View {
|
||||
|
||||
func ntfModeDescription(_ mode: NotificationsMode) -> 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."
|
||||
case .off: return "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)."
|
||||
case .periodic: return "**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have."
|
||||
case .instant: return "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who is it from."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +174,7 @@ struct SelectionListView<Item: SelectableItem>: View {
|
||||
.contentShape(Rectangle())
|
||||
.listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground))
|
||||
.onTapGesture {
|
||||
if selection == item { return }
|
||||
if let f = onSelection {
|
||||
f(item)
|
||||
} else {
|
||||
|
||||
@@ -200,13 +200,13 @@ struct SettingsView: View {
|
||||
switch (chatModel.tokenStatus) {
|
||||
case .new:
|
||||
icon = "bolt"
|
||||
color = .primary
|
||||
color = .secondary
|
||||
case .registered:
|
||||
icon = "bolt.fill"
|
||||
color = .primary
|
||||
color = .secondary
|
||||
case .invalid:
|
||||
icon = "bolt.slash"
|
||||
color = .primary
|
||||
color = .secondary
|
||||
case .confirmed:
|
||||
icon = "bolt.fill"
|
||||
color = .yellow
|
||||
@@ -215,10 +215,10 @@ struct SettingsView: View {
|
||||
color = .green
|
||||
case .expired:
|
||||
icon = "bolt.slash.fill"
|
||||
color = .primary
|
||||
color = .secondary
|
||||
case .none:
|
||||
icon = "bolt"
|
||||
color = .primary
|
||||
color = .secondary
|
||||
}
|
||||
return Image(systemName: icon)
|
||||
.padding(.trailing, 9)
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
|
||||
5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; };
|
||||
5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; };
|
||||
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */; };
|
||||
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
|
||||
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
|
||||
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
|
||||
@@ -227,6 +228,7 @@
|
||||
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
|
||||
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = "<group>"; };
|
||||
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = "<group>"; };
|
||||
5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNotificationsMode.swift; sourceTree = "<group>"; };
|
||||
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
|
||||
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
|
||||
@@ -489,9 +491,10 @@
|
||||
children = (
|
||||
5CB0BA8D2827126500B3292C /* OnboardingView.swift */,
|
||||
5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */,
|
||||
5CB0BA91282713FD00B3292C /* CreateProfile.swift */,
|
||||
5CB0BA952827143500B3292C /* MakeConnection.swift */,
|
||||
5CB0BA992827FD8800B3292C /* HowItWorks.swift */,
|
||||
5CB0BA91282713FD00B3292C /* CreateProfile.swift */,
|
||||
5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */,
|
||||
5CB0BA952827143500B3292C /* MakeConnection.swift */,
|
||||
);
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
@@ -827,6 +830,7 @@
|
||||
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */,
|
||||
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */,
|
||||
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */,
|
||||
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */,
|
||||
5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */,
|
||||
5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */,
|
||||
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */,
|
||||
|
||||
@@ -427,7 +427,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem {
|
||||
|
||||
public var label: LocalizedStringKey {
|
||||
switch self {
|
||||
case .off: return "Off"
|
||||
case .off: return "Off (Local)"
|
||||
case .periodic: return "Periodically"
|
||||
case .instant: return "Instantly"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user