damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 279854a9fd1ee5730b6df195e47edd0d677525ce
parent 19ba020bd04f82f191c57015a11753fd6d9b6c68
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Mon, 22 Apr 2024 23:09:37 +0000

ui: add First Aid view to settings

also create the contact list reset First Aid action

Automatically detecting whether or not to create a blank contact list
when we could not find any is very tricky. It could mean that no contact
list exists, but it could also mean that a temporary network or relay
outage occurred.

Since resetting the contact list when one already exists is a
destructive action, we should make no assumptions. Instead, we should
provide users the tool to fix it based on their own judgement.

For that reason, the first aid view was created. It detects if no
contact list was found, and in those cases, it gives them an option to
reset (with appropriate warning messages).

Testing 1: Contact list creation robustness
-----------------------------

Setup:
1. Network Link Conditioner installed and configured to this profile:
  - DNS delay: 400 ms
  - Downlink bandwidth: 100 kbps
  - Uplink bandwidth: 50 kbps
  - Packets dropped: 50% (On both uplink and downlink)
  - Delay: 1000 ms (Both uplink and downlink)

Procedure:
1. Turn Network Link conditioner ON
2. Go through the account creation steps
3. At the moment the onboarding follow suggestions screen shows up, quit the app
3. Turn Network Link conditioner OFF
4. Start the app again
5. Verify the home screen. It should present notes from the Damus account (the default follow)
6. Follow someone and wait for 5 seconds
7. Restart app
8. Look at the home feed. Notes from user from step 6 should appear, and that user should appear as being followed by you.

- Repro details:
  - Damus version: ada99418f6fcdb1354bc5c1c3f3cc3b4db994ce6
  - Device: iPhone 15 simulator
  - iOS: 17.4
  - Number of runs: 3 times
  - Result: FAILS (issue is reproduced) 3 out of 3 times
- Test details:
  - Damus version: This commit
  - Device: iPhone 15 simulator
  - iOS: 17.4
  - Number of runs: 3 times
  - Result: PASSES all criteria 3 out of 3 times

Testing 2: Contact list First Aid
------------------------------

Setup:
1. Reproduce the issue with the old version as outlined in "Testing 1" above
2. Upgrade to the version in this commit

Steps:
1. Go to Settings > First Aid
2. A button to reset the contact list (and some text for context) should appear. PASS
3. Click on the button. A warning message should appear. PASS
4. Click "cancel". The action should be cancelled and nothing should have changed. PASS
5. Click on the reset button again.
6. Click "Continue" on the warning prompt. The reset button will now show "Contact list has been reset" with a green checkmark. PASS
5. Go back to the home tab. Notes from the Damus account should immediately appear. PASS
6. Try to follow someone and restart the app. Follows should now stick persistently. PASS
7. Go to the First Aid screen again. The reset option should no longer be present. PASS

Changelog-Added: Add First Aid solution for users who do not have a contact list created for their account
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Link: 20240422230912.65056-4-daniel@daquino.me
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/Util/Router.swift | 5+++++
Mdamus/Views/ConfigView.swift | 4++++
Adamus/Views/Settings/FirstAidSettingsView.swift | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 97 insertions(+), 0 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -640,6 +640,7 @@ D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */; }; D7EDED342B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */; }; D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; }; + D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */; }; D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */; }; E02429952B7E97740088B16C /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E02429942B7E97740088B16C /* CameraController.swift */; }; E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */; }; @@ -1434,6 +1435,7 @@ D7EDED202B117DCA0018B19C /* SequenceUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceUtils.swift; sourceTree = "<group>"; }; D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = "<group>"; }; D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusUserDefaults.swift; sourceTree = "<group>"; }; + D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstAidSettingsView.swift; sourceTree = "<group>"; }; D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = "<group>"; }; E02429942B7E97740088B16C /* CameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = "<group>"; }; E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bech32ObjectTests.swift; sourceTree = "<group>"; }; @@ -1725,6 +1727,7 @@ 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */, E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */, 5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */, + D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */, ); path = Settings; sourceTree = "<group>"; @@ -3110,6 +3113,7 @@ D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */, 504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */, 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */, + D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */, D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */, 4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */, 4C363AA228296A7E006E126D /* SearchView.swift in Sources */, diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift @@ -30,6 +30,7 @@ enum Route: Hashable { case ReactionsSettings(settings: UserSettingsStore) case SearchSettings(settings: UserSettingsStore) case DeveloperSettings(settings: UserSettingsStore) + case FirstAidSettings(settings: UserSettingsStore) case Thread(thread: ThreadModel) case Reposts(reposts: EventsModel) case QuoteReposts(quotes: EventsModel) @@ -89,6 +90,8 @@ enum Route: Hashable { SearchSettingsView(settings: settings) case .DeveloperSettings(let settings): DeveloperSettingsView(settings: settings) + case .FirstAidSettings(settings: let settings): + FirstAidSettingsView(damus_state: damusState, settings: settings) case .Thread(let thread): ThreadView(state: damusState, thread: thread) case .Reposts(let reposts): @@ -175,6 +178,8 @@ enum Route: Hashable { hasher.combine("searchSettings") case .DeveloperSettings: hasher.combine("developerSettings") + case .FirstAidSettings: + hasher.combine("firstAidSettings") case .Thread(let threadModel): hasher.combine("thread") hasher.combine(threadModel.event.id) diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift @@ -67,6 +67,10 @@ struct ConfigView: View { NavigationLink(value: Route.DeveloperSettings(settings: settings)) { IconLabel(NSLocalizedString("Developer", comment: "Section header for developer settings"), img_name: "magic-stick2.fill", color: DamusColors.adaptableBlack) } + + NavigationLink(value: Route.FirstAidSettings(settings: settings)) { + IconLabel(NSLocalizedString("First Aid", comment: "Section header for first aid tools and settings"), img_name: "help2", color: .red) + } } Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) { diff --git a/damus/Views/Settings/FirstAidSettingsView.swift b/damus/Views/Settings/FirstAidSettingsView.swift @@ -0,0 +1,84 @@ +// +// FirstAidSettingsView.swift +// damus +// +// Created by Daniel D’Aquino on 2024-04-19. +// + +import SwiftUI + +struct FirstAidSettingsView: View { + let damus_state: DamusState + @ObservedObject var settings: UserSettingsStore + @State var reset_contact_list_state: ContactListResetState = .not_started + + enum ContactListResetState: Equatable { + case not_started + case confirming_with_user + case error(String) + case in_progress + case completed + } + + + var body: some View { + Form { + if damus_state.contacts.event == nil { + Section( + header: Text(NSLocalizedString("Contact list (Follows + Relay list)", comment: "Section title for Contact list first aid tools")), + footer: Text(NSLocalizedString("No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it", comment: "Section footer for Contact list first aid tools")) + ) { + Button(action: { + reset_contact_list_state = .confirming_with_user + }, label: { + HStack(spacing: 6) { + switch reset_contact_list_state { + case .not_started, .error: + Label(NSLocalizedString("Reset contact list", comment: "Button to reset contact list."), image: "broom") + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundColor(.red) + case .confirming_with_user, .in_progress: + ProgressView() + Text(NSLocalizedString("In progress…", comment: "Loading message indicating that a contact list reset operation is in progress.")) + case .completed: + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text(NSLocalizedString("Contact list has been reset", comment: "Message indicating that the contact list was successfully reset.")) + } + } + }) + .disabled(reset_contact_list_state == .in_progress || reset_contact_list_state == .completed) + + if case let .error(error_message) = reset_contact_list_state { + Text(error_message) + .foregroundStyle(.red) + } + } + .alert(NSLocalizedString("WARNING:\n\nThis will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.", comment: "Alert for resetting the user's contact list."), + isPresented: Binding(get: { reset_contact_list_state == .confirming_with_user }, set: { _ in return }) + ) { + Button(NSLocalizedString("Cancel", comment: "Cancel resetting the contact list."), role: .cancel) { + reset_contact_list_state = .not_started + } + Button(NSLocalizedString("Continue", comment: "Continue with resetting the contact list.")) { + guard let new_contact_list_event = make_first_contact_event(keypair: damus_state.keypair) else { + reset_contact_list_state = .error(NSLocalizedString("An unexpected error happened while trying to create the new contact list. Please contact support.", comment: "Error message for a failed contact list reset operation")) + return + } + damus_state.pool.send(.event(new_contact_list_event)) + reset_contact_list_state = .completed + } + } + } + + if damus_state.contacts.event != nil { + Text(NSLocalizedString("We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support", comment: "Message indicating that no First Aid actions are available.")) + } + } + .navigationTitle(NSLocalizedString("First Aid", comment: "Navigation title for first aid settings and tools")) + } +} + +#Preview { + FirstAidSettingsView(damus_state: test_damus_state, settings: test_damus_state.settings) +}