ios: choose notifications mode during onboarding and after DB migration (#773)

This commit is contained in:
Evgeny Poberezkin
2022-07-03 19:53:07 +01:00
committed by GitHub
parent c619092464
commit 2c121b5731
12 changed files with 175 additions and 45 deletions

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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() }

View File

@@ -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)
}
}

View File

@@ -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))")

View File

@@ -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
}

View 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()
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 */,

View File

@@ -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"
}