From ac29354b08efae48c32c64e1ce55908eb0c82fa3 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:21:13 +0100 Subject: [PATCH] Move any visible account settings into AccountSettings. (#6308) * Remove the now unused setting to disable in-app notifications. * Move visible account settings into AccountSettings. Have ignored enableNotifications for now as that will require reworking the NotificationManager. * Allow different sounds to be selected for each account. * Move defaultNotificationTone into AccountSettings. --- ElementX.xcodeproj/project.pbxproj | 36 +++++++--- ElementX/Sources/AppHooks/AppHooks.swift | 6 +- .../AppHooks/Hooks/UserSessionHook.swift | 10 +++ .../Sources/Application/AppCoordinator.swift | 35 +++++++++ .../Settings/AccountSettings.swift | 21 ++++-- .../Application/Settings/AppSettings.swift | 17 ----- .../Mocks/Generated/GeneratedMocks.swift | 5 ++ .../Sources/Mocks/NSEUserSessionMock.swift | 2 + ElementX/Sources/Other/Extensions/URL.swift | 10 +++ .../HomeScreen/HomeScreenViewModel.swift | 6 +- .../RoomScreen/RoomScreenViewModel.swift | 2 +- ...erationAndSafetySettingsScreenModels.swift | 4 +- .../NotificationSettingsScreenViewModel.swift | 12 ++-- .../Screens/Timeline/TimelineViewModel.swift | 6 +- .../AlertTones/NotificationTone.swift | 3 + .../AlertTones/NotificationToneManager.swift | 71 +++++++++---------- .../Manager/NotificationManager.swift | 3 - .../Services/Presence/PresenceService.swift | 4 +- .../TimelineController.swift | 2 +- .../UserSession/UserSessionStore.swift | 5 +- Enterprise | 2 +- .../CommonSettings+NotificationName.swift | 25 ------- .../Extensions/NSEAccountSettings.swift | 15 ++++ .../Extensions/NSENotificationTone.swift | 15 ++++ NSE/Sources/NSEUserSession.swift | 4 +- NSE/Sources/NotificationContentBuilder.swift | 3 +- NSE/Sources/NotificationHandler.swift | 1 - .../NotificationServiceExtension.swift | 2 +- NSE/SupportingFiles/target.yml | 1 + .../ShareExtensionViewController.swift | 1 + ShareExtension/SupportingFiles/target.yml | 2 + TIDY.md | 5 ++ .../NotificationContentBuilderTests.swift | 1 - ...ficationSettingsScreenViewModelTests.swift | 20 +++--- .../NotificationToneManagerTests.swift | 8 +-- UnitTests/Sources/PresenceServiceTests.swift | 24 +++---- project.yml | 2 +- 37 files changed, 240 insertions(+), 151 deletions(-) delete mode 100644 NSE/Sources/CommonSettings+NotificationName.swift create mode 100644 NSE/Sources/Extensions/NSEAccountSettings.swift create mode 100644 NSE/Sources/Extensions/NSENotificationTone.swift create mode 100644 TIDY.md diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index c3938a178..351298767 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 09693596AA3CA4E598C0D2F1 /* ThreadTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FEE625AB52042049DB9268 /* ThreadTimelineScreenCoordinator.swift */; }; 09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */; }; 09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */; }; + 09EBBB7F7384C5D5C03BC324 /* NSEAccountSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038DAB8462278EA64753315D /* NSEAccountSettings.swift */; }; 0AB4FF7645175FF1F6E942CC /* DeferredFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91AD590B0B40718A0AA0C61 /* DeferredFulfillment.swift */; }; 0ACAA31FD0399CEEBA3ECC21 /* UserDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */; }; 0AD8EF040A60D62F488C18B5 /* KnockRequestProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F957320D0EB7D7B4E30C79D /* KnockRequestProxyMock.swift */; }; @@ -188,6 +189,7 @@ 1AE4532D92E6ED42BFE87064 /* JoinRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A566F8F2C27B99E1FB80C69B /* JoinRule.swift */; }; 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; 1AF4A82B4332CAD2DB3EB5DA /* TopBannerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78483F0143E185EDC6ECD741 /* TopBannerModifier.swift */; }; + 1AF6BFB40515EB31B9485665 /* NSENotificationTone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060DA1A559FE85CF65ADFE07 /* NSENotificationTone.swift */; }; 1B2DADC008EE211AF1DA5292 /* NotificationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */; }; 1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */; }; 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; }; @@ -523,6 +525,7 @@ 51263FC33CC961A14F5D6BCA /* TracingHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A95C9B8299A36A6495DECA6 /* TracingHook.swift */; }; 5139F4BD5A5DF6F8D11A9BDE /* NotificationPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */; }; 513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */; }; + 5152FF71D3AE01A1ADF0FC62 /* NSEAccountSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038DAB8462278EA64753315D /* NSEAccountSettings.swift */; }; 51A075328D68FB4AE65C43B5 /* SearchServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD6FFE581238BD30B53BA0A /* SearchServiceProxy.swift */; }; 51B3B19FA5F91B455C807BA7 /* RoomPollsHistoryScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */; }; 51DBB7D725080FDAA4AB1412 /* UserSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE997F400545F3265935B53 /* UserSettingsTests.swift */; }; @@ -723,6 +726,7 @@ 709A9B52FC26B4CB86B8B020 /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */; }; 70A4C56E4E6C60A3124631FC /* ModerationAndSafetySettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8465F252997056A1E094204E /* ModerationAndSafetySettingsScreenModels.swift */; }; 70B83D44043293B4B77440B9 /* PollFormScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */; }; + 70D5D5E3ED6C3AD2449B382E /* NSENotificationTone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060DA1A559FE85CF65ADFE07 /* NSENotificationTone.swift */; }; 715FB2D768E8919979EACD71 /* StopButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A786DE47C0DBCE0CD163193 /* StopButton.swift */; }; 71643093F87153F633A1B025 /* ThreadDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */; }; 719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; }; @@ -749,7 +753,6 @@ 754602A7B2AAD443C4228ED4 /* ElementCall in Frameworks */ = {isa = PBXBuildFile; productRef = 99570A04BD7A395538321E94 /* ElementCall */; }; 755395927DDD6EBDDA5E217A /* SettingsFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */; }; 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; }; - 756BD0B80C1AF2A6DF0786CB /* CommonSettings+NotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7A66212DC7D097886BD01F /* CommonSettings+NotificationName.swift */; }; 756EA0D663261889EF64E6D4 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9CBF577B9711CFBB4FA40D /* VoiceMessageRecordingView.swift */; }; 7573D682F089205F7F1D96CF /* SessionDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C2067FF58B4996323EB40C /* SessionDirectories.swift */; }; 757862045774A0F458357E19 /* RoomThreadListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544B9262E1BC6F489C03FFFA /* RoomThreadListScreen.swift */; }; @@ -885,7 +888,6 @@ 88C8B87A26D6B4E731F2B5B7 /* SpacesTabFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4930A2674306A2DC9C92009D /* SpacesTabFlowCoordinator.swift */; }; 88CBF1595E39CE697928DE48 /* SFNumberedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */; }; 88F348E2CB14FF71CBBB665D /* AudioRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7475C5AE20BA896930907EA8 /* AudioRoomTimelineItemContent.swift */; }; - 88FF7B41DF76E22E6835F3C3 /* CommonSettings+NotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7A66212DC7D097886BD01F /* CommonSettings+NotificationName.swift */; }; 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C687844F60BFF532D49A994C /* AnalyticsTests.swift */; }; 89198AE2649DD77673D5793B /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; 8944548A684F1C837CEC47F4 /* RoomMembersListScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0946F77B696176E062D037 /* RoomMembersListScreenModels.swift */; }; @@ -970,6 +972,7 @@ 9462C62798F47E39DCC182D2 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA89A2DD51B6BBE1DA55E263 /* Application.swift */; }; 94871CA883F44022086FE750 /* AuthenticationStartLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98784280D98C852727BE0111 /* AuthenticationStartLogo.swift */; }; 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */; }; + 94B6EDC626DBECADF727879C /* UserSessionHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9773983346838AABFAA914D /* UserSessionHook.swift */; }; 94C2B531B96493B68B976E5F /* SpaceServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226A56746AD99ABDA97189E1 /* SpaceServiceProxy.swift */; }; 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 94D9172C4B91FC6971C668CE /* LeaveSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE4B7AD74E402BBC46A801A /* LeaveSpaceView.swift */; }; @@ -1061,6 +1064,7 @@ A2357AA4A188BC37085BC6F0 /* EditRoomAddressScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */; }; A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; + A30AAE44E872674D77B5D972 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */; }; A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; A36AD251013402EDBD666C75 /* AppMediatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAC027034248429A438886B /* AppMediatorMock.swift */; }; @@ -1260,6 +1264,7 @@ C486DA7A3E05BAA1E389D280 /* AuthenticationStartScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F69820F717FF11C6309916 /* AuthenticationStartScreenCoordinator.swift */; }; C49FCC766673006B6D299F1C /* RoomDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */; }; C4D2BCAA54E2C62B94B24AF4 /* InviteUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */; }; + C4E035CCFC3C457ED2F07528 /* UserSessionHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9773983346838AABFAA914D /* UserSessionHook.swift */; }; C4E0D03DF88242697545A9B7 /* UserIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */; }; C4FE0E11A907C8999F92D5A8 /* TimelineStartRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */; }; C525F6C2892CB0640E776B3D /* OAuthConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002DE5E9625122F37DE5F020 /* OAuthConfiguration.swift */; }; @@ -1806,6 +1811,7 @@ 035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; 0367E0AF1163F42292D788BD /* StartChatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatTests.swift; sourceTree = ""; }; 0376C429FAB1687C3D905F3E /* MockCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCoder.swift; sourceTree = ""; }; + 038DAB8462278EA64753315D /* NSEAccountSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEAccountSettings.swift; sourceTree = ""; }; 03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = ""; }; 044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = ""; }; @@ -1826,6 +1832,7 @@ 05A3E8741D199CD1A37F4CBF /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 05AF58372CA884A789EB9C5A /* AppMediatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorProtocol.swift; sourceTree = ""; }; 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = ""; }; + 060DA1A559FE85CF65ADFE07 /* NSENotificationTone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationTone.swift; sourceTree = ""; }; 06775BFC4D00C62B7E1353A8 /* SettingsScreenUserStatusPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUserStatusPickerView.swift; sourceTree = ""; }; 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelProtocol.swift; sourceTree = ""; }; 06B098A612DCB5A7358EECD5 /* DeveloperOptionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenModels.swift; sourceTree = ""; }; @@ -2815,7 +2822,6 @@ AB07F03461023BC39C730922 /* PhishingDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingDetector.swift; sourceTree = ""; }; AB26D5444A4A7E095222DE8B /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.stringsdict"; sourceTree = ""; }; AB389C38BD41EB3E47092CFB /* AccessibilityTests.xctestplan */ = {isa = PBXFileReference; path = AccessibilityTests.xctestplan; sourceTree = ""; }; - AB7A66212DC7D097886BD01F /* CommonSettings+NotificationName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommonSettings+NotificationName.swift"; sourceTree = ""; }; AB7D79FA27D094146E2B5C9D /* TimelineMediaContentScanningFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaContentScanningFailureView.swift; sourceTree = ""; }; ABF84AA68B2B7584D9275769 /* VoiceMessageTrashButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageTrashButton.swift; sourceTree = ""; }; AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelTests.swift; sourceTree = ""; }; @@ -5832,11 +5838,11 @@ isa = PBXGroup; children = ( 6F054DE7D47849687662C9D9 /* BootDetectionManager.swift */, - AB7A66212DC7D097886BD01F /* CommonSettings+NotificationName.swift */, D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */, BB6ED50FE104992419310EEB /* NotificationHandler.swift */, 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */, 6C9651CD1066F239C7739240 /* NSEUserSession.swift */, + C7EA0DC1683A468E57E933C4 /* Extensions */, 10450069C1510FAFAEABB4AA /* NotificationItemProxy */, ); path = Sources; @@ -6834,6 +6840,15 @@ path = SecurityAndPrivacyScreen; sourceTree = ""; }; + C7EA0DC1683A468E57E933C4 /* Extensions */ = { + isa = PBXGroup; + children = ( + 038DAB8462278EA64753315D /* NSEAccountSettings.swift */, + 060DA1A559FE85CF65ADFE07 /* NSENotificationTone.swift */, + ); + path = Extensions; + sourceTree = ""; + }; C844840F3DD48A154C65AE0C /* View */ = { isa = PBXGroup; children = ( @@ -8366,7 +8381,6 @@ DA6E57CB533213D438001A31 /* ClientFactory.swift in Sources */, 9AF73FA23C86B45801EE2949 /* ClientFactoryHook.swift in Sources */, 039FE1757084092146C0B616 /* ClientFactoryProtocol.swift in Sources */, - 88FF7B41DF76E22E6835F3C3 /* CommonSettings+NotificationName.swift in Sources */, 211B5F524E851178EE549417 /* CurrentValuePublisher.swift in Sources */, 24A75F72EEB7561B82D726FD /* Date.swift in Sources */, FE9A5D4715C7AB68682C030C /* Dictionary.swift in Sources */, @@ -8392,6 +8406,8 @@ 9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */, 7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */, E2DB696117BAEABAD5718023 /* MediaSourceProxy.swift in Sources */, + 09EBBB7F7384C5D5C03BC324 /* NSEAccountSettings.swift in Sources */, + 70D5D5E3ED6C3AD2449B382E /* NSENotificationTone.swift in Sources */, 4CB30D9BD72CA3FEEFAA0793 /* NSEUserSession.swift in Sources */, 1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */, 4DEEFB73181C3B023DB42686 /* NetworkMonitorProtocol.swift in Sources */, @@ -8434,6 +8450,7 @@ D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */, 34357B287357BC0B9715DD51 /* UserAgentBuilder.swift in Sources */, 90F7C2C3DF76B3B00B58CB57 /* UserDefaultsProtocol.swift in Sources */, + 94B6EDC626DBECADF727879C /* UserSessionHook.swift in Sources */, D61FCF6ABC1490F630E76AEC /* UserSettings.swift in Sources */, AF75E5CF932D80EAD578E02E /* VolatileUserDefaults.swift in Sources */, ); @@ -8682,6 +8699,7 @@ DAF63A9CF9932CA8F6830F11 /* ShareExtensionModels.swift in Sources */, 5AA81A4E2D40A32A9E7F71F2 /* ShareExtensionView.swift in Sources */, 5AC5CD6D893073EE4D9A277E /* ShareExtensionViewController.swift in Sources */, + A30AAE44E872674D77B5D972 /* Strings.swift in Sources */, 6E03A710799E6C65C0AB36BC /* TargetConfiguration.swift in Sources */, A5FD8284744E2FECFC842FC1 /* TraceLogPack.swift in Sources */, 89DF67AECBF9D0EE0DDB7737 /* Tracing.swift in Sources */, @@ -8691,6 +8709,7 @@ 26252AA9AED64010788F4C26 /* UIView.swift in Sources */, 66E9202BED03B5BB00E812A1 /* URL.swift in Sources */, 9F51563FBDD578F37020C852 /* UserDefaultsProtocol.swift in Sources */, + C4E035CCFC3C457ED2F07528 /* UserSessionHook.swift in Sources */, 452507BCE97B3EA0F24E206E /* UserSettings.swift in Sources */, 0B30DC7D9D445C405ADCB5FA /* VolatileUserDefaults.swift in Sources */, ); @@ -8895,7 +8914,6 @@ 78A3D84BA47DAC69B4D0A34C /* CollapsibleRoomTimelineView.swift in Sources */, 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */, 663E198678778F7426A9B27D /* Collection.swift in Sources */, - 756BD0B80C1AF2A6DF0786CB /* CommonSettings+NotificationName.swift in Sources */, 24B7CD41342C143117ADA768 /* Comparable.swift in Sources */, 19DF5600A7F547B22DD7872A /* CompletionSuggestionService.swift in Sources */, EAB3C1F0BC7F671ED8BDF82D /* CompletionSuggestionServiceProtocol.swift in Sources */, @@ -9240,6 +9258,8 @@ 5846723185B444D52E077874 /* ModerationAndSafetySettingsScreenViewModel.swift in Sources */, 7C8F9E3AF56E7FF5B338DBFA /* ModerationAndSafetySettingsScreenViewModelProtocol.swift in Sources */, A88A0C11EB4231EE2989C659 /* MultiAccountAnnouncementView.swift in Sources */, + 5152FF71D3AE01A1ADF0FC62 /* NSEAccountSettings.swift in Sources */, + 1AF6BFB40515EB31B9485665 /* NSENotificationTone.swift in Sources */, E938F7A45D6A3DBBE6789A03 /* NSEUserSession.swift in Sources */, 551E03A935A6B73158A1AB4A /* NSEUserSessionMock.swift in Sources */, F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */, @@ -10530,7 +10550,7 @@ IS_NIGHTLY_BUILD = NO; KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).$(BASE_BUNDLE_IDENTIFIER)"; MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 26.10.1; + MARKETING_VERSION = 26.10.2; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCTION_APP_NAME = Element; @@ -10612,7 +10632,7 @@ IS_NIGHTLY_BUILD = NO; KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).$(BASE_BUNDLE_IDENTIFIER)"; MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 26.10.1; + MARKETING_VERSION = 26.10.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; diff --git a/ElementX/Sources/AppHooks/AppHooks.swift b/ElementX/Sources/AppHooks/AppHooks.swift index 20d9c4e3d..6a8b67ba1 100644 --- a/ElementX/Sources/AppHooks/AppHooks.swift +++ b/ElementX/Sources/AppHooks/AppHooks.swift @@ -28,9 +28,6 @@ final nonisolated class AppHooks: AppHooksProtocol { @AppHook(default: DefaultOAuthPresenterHook()) var oAuthPresenterHook: OAuthPresenterHookProtocol - @AppHook(default: DefaultUserSessionHook()) - var userSessionHook: UserSessionHookProtocol - @AppHook(default: DefaultRoomScreenHook()) var roomScreenHook: RoomScreenHookProtocol @@ -64,6 +61,9 @@ final nonisolated class AppHooks: AppHooksProtocol { @AppHook(default: DefaultRemoteSettingsHook()) var remoteSettingsHook: RemoteSettingsHookProtocol + + @AppHook(default: DefaultUserSessionHook()) + var userSessionHook: UserSessionHookProtocol } nonisolated protocol AppHooksProtocol: Sendable { diff --git a/ElementX/Sources/AppHooks/Hooks/UserSessionHook.swift b/ElementX/Sources/AppHooks/Hooks/UserSessionHook.swift index 9cf4a647a..0ce449992 100644 --- a/ElementX/Sources/AppHooks/Hooks/UserSessionHook.swift +++ b/ElementX/Sources/AppHooks/Hooks/UserSessionHook.swift @@ -8,9 +8,19 @@ import MatrixRustSDK nonisolated protocol UserSessionHookProtocol: Sendable { + #if IS_MAIN_APP func configure(with userSession: UserSessionProtocol?) async + #endif + + func configure(_ userSettings: UserSettings, forHomeserver homeserver: String) -> UserSettings } nonisolated struct DefaultUserSessionHook: UserSessionHookProtocol { + #if IS_MAIN_APP func configure(with userSession: UserSessionProtocol?) async { } + #endif + + func configure(_ userSettings: UserSettings, forHomeserver homeserver: String) -> UserSettings { + userSettings + } } diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 43ee1a993..a67f88a69 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -467,10 +467,28 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg MXLog.info("Migrating to version 25.07.4, log files have been moved.") } + if oldVersion < Version(26, 10, 2) { + MXLog.info("Migrating to version 26.10.2, moving notification sounds.") + let soundsDirectory = URL.libraryDirectory.appending(component: "Sounds", directoryHint: .isDirectory) + + // Sounds are now referenced directly, the duplicate file can go. + try? FileManager.default.removeItem(at: soundsDirectory.appending(component: "currentAlert.caf")) + + let oldCustomTonesLocation = soundsDirectory.appending(component: "AvailableSounds", directoryHint: .isDirectory) + if FileManager.default.fileExists(atPath: oldCustomTonesLocation.path(percentEncoded: false)) { + do { + try FileManager.default.moveItem(at: oldCustomTonesLocation, to: URL.customTonesDirectory) + } catch { + MXLog.error("Failed moving custom notification sounds: \(error)") + } + } + } + // Store the old version to run additional migrations on the user session once it has been set up. userSessionMigrationsOldVersion = oldVersion } + // swiftlint:disable:next cyclomatic_complexity private func performUserSessionMigrations(_ userSession: UserSessionProtocol) async { guard let oldVersion = userSessionMigrationsOldVersion else { return } @@ -515,6 +533,23 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg userSession.userSettings.account.migrateAppSettingsValue(\.searchBreadcrumbsKey) } + if oldVersion < Version(26, 10, 2) { + MXLog.info("Migrating to version 26.10.2, moving visible account settings.") + userSession.userSettings.account.migrateAppSettingsValue(\.showAllRoomListActivityKey) + userSession.userSettings.account.migrateAppSettingsValue(\.selectedNotificationToneKey) + userSession.userSettings.account.migrateAppSettingsValue(\.sharePresenceKey) + + // Make sure system notification sounds are available in the new directory. + if let selectedNotificationTone = userSession.userSettings.account.selectedNotificationTone { + do { + MXLog.info("Copying the selected system notification sound.") + try NotificationToneManager.copySystemToneIfNeeded(selectedNotificationTone) + } catch { + MXLog.error("Failed migrating the selected notification sound: \(error)") + } + } + } + userSessionMigrationsOldVersion = nil } diff --git a/ElementX/Sources/Application/Settings/AccountSettings.swift b/ElementX/Sources/Application/Settings/AccountSettings.swift index ff2ddbdb3..d100aef94 100644 --- a/ElementX/Sources/Application/Settings/AccountSettings.swift +++ b/ElementX/Sources/Application/Settings/AccountSettings.swift @@ -14,23 +14,34 @@ final nonisolated class AccountSettings: @unchecked Sendable { private let store: AccountUserDefaults private let userID: String - // MARK: Accounts + // MARK: Session Activity /// When this account was last selected, used to order the accounts most recently selected first. @UserPreference var lastSelectedDate: Date? - // MARK: Session - @UserPreference(defaultValue: false) var hasRunIdentityConfirmationOnboarding: Bool - // MARK: Search - /// The queries the user searched for and the rooms they opened from the results, most recent first. @UserPreference(defaultValue: [SearchBreadcrumb]()) var searchBreadcrumbs: [SearchBreadcrumb] + // MARK: Visible Preferences + + @UserPreference(defaultValue: true) + var showAllRoomListActivity: Bool + + /// The tone played when the user hasn't selected one, which the homeserver can configure remotely. + let defaultNotificationTone: RemotePreference = .init(.default) + + /// The sound played when delivering noisy notifications. If nil, use ``defaultNotificationTone``. + @UserPreference + var selectedNotificationTone: NotificationTone? + + @UserPreference(defaultValue: true) + var sharePresence: Bool + init(userID: String, store: UserDefaultsProtocol) { self.store = .init(userID: userID, store: store) self.userID = userID diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index 4568f5c99..4dfc98c23 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -235,9 +235,6 @@ final nonisolated class AppSettings: @unchecked Sendable { @UserPreference(defaultValue: true) var enableNotifications: Bool - @UserPreference(defaultValue: true) - var enableInAppNotifications: Bool - @UserPreference(defaultValue: false) var hideQuietNotificationAlerts: Bool @@ -253,10 +250,6 @@ final nonisolated class AppSettings: @unchecked Sendable { @UserPreference(defaultValue: 0) var lastKnownBadgeCount: Int - /// The sound played when delivering noisy notifications. If nil, use the ElementX default - @UserPreference - var selectedNotificationTone: NotificationTone? - // MARK: - Logging @UserPreference(defaultValue: LogLevel.info) @@ -331,11 +324,6 @@ final nonisolated class AppSettings: @unchecked Sendable { @UserPreference(defaultValue: false) var liveLocationDisclaimerDisplayed: Bool - // MARK: - Home Screen - - @UserPreference(defaultValue: true) - var showAllRoomListActivity: Bool - // MARK: - Room Screen @UserPreference(defaultValue: AppBuildType.current == .debug) @@ -382,11 +370,6 @@ final nonisolated class AppSettings: @unchecked Sendable { /// The MapTiler configuration used to build map URLs, which defaults to the bundled one. private(set) var mapTilerConfiguration = RemotePreference(AppSettings.bundledMapTilerConfiguration) - // MARK: - Presence - - @UserPreference(defaultValue: true) - var sharePresence: Bool - // MARK: - Feature Flags /// Others diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 2b08fa553..1c620e909 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -8692,6 +8692,11 @@ nonisolated class MediaProviderMock: MediaProviderProtocol, @unchecked Sendable } } nonisolated class NSEUserSessionMock: NSEUserSessionProtocol, @unchecked Sendable { + var userSettings: UserSettings { + get { return underlyingUserSettings } + set(value) { underlyingUserSettings = value } + } + nonisolated(unsafe) var underlyingUserSettings: UserSettings! nonisolated(unsafe) var inviteAvatarsVisibilityCallsCount = 0 var inviteAvatarsVisibilityCalled: Bool { return inviteAvatarsVisibilityCallsCount > 0 diff --git a/ElementX/Sources/Mocks/NSEUserSessionMock.swift b/ElementX/Sources/Mocks/NSEUserSessionMock.swift index 022167054..e7298dd1e 100644 --- a/ElementX/Sources/Mocks/NSEUserSessionMock.swift +++ b/ElementX/Sources/Mocks/NSEUserSessionMock.swift @@ -8,6 +8,7 @@ import MatrixRustSDK struct NSEUserSessionMockConfiguration { + var userSettings = UserSettings.mock() var inviteAvatarsVisibility = InviteAvatars.on var mediaPreviewVisibility = MediaPreviews.on var threadsEnabled = true @@ -17,6 +18,7 @@ extension NSEUserSessionMock { convenience init(_ configuration: NSEUserSessionMockConfiguration) { self.init() + underlyingUserSettings = configuration.userSettings underlyingInviteAvatarsVisibility = configuration.inviteAvatarsVisibility underlyingMediaPreviewVisibility = configuration.mediaPreviewVisibility threadsEnabled = configuration.threadsEnabled diff --git a/ElementX/Sources/Other/Extensions/URL.swift b/ElementX/Sources/Other/Extensions/URL.swift index d8e2cbfdd..c01ed251d 100644 --- a/ElementX/Sources/Other/Extensions/URL.swift +++ b/ElementX/Sources/Other/Extensions/URL.swift @@ -103,6 +103,16 @@ nonisolated extension URL { return systemRoot.appending(components: "System", "Library", "Audio", "UISounds", directoryHint: .isDirectory) }() + /// The directory holding notification tones imported by the user, inside the Library/Sounds directory searched by the system. + static var customTonesDirectory: URL { + libraryDirectory.appending(components: "Sounds", "Custom", directoryHint: .isDirectory) + } + + /// The directory holding copies of selected system tones, inside the Library/Sounds directory searched by the system. + static var systemTonesDirectory: URL { + libraryDirectory.appending(components: "Sounds", "System", directoryHint: .isDirectory) + } + var globalProxy: String? { guard let proxySettings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() else { MXLog.error("Failed retrieving proxy settings") diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 7d46bb2bd..aa8002c7f 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -115,7 +115,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .weakAssign(to: \.state.selectedRoomID, on: self) .store(in: &cancellables) - userSettings.app.showAllRoomListActivityPublisher + userSettings.account.showAllRoomListActivityPublisher .removeDuplicates() .sink { [weak self] _ in self?.updateRooms() @@ -277,7 +277,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol case .success: analyticsService.trackInteraction(name: .MobileRoomListRoomContextMenuUnreadToggle) - if case .failure(let error) = await roomProxy.markAsRead(receiptType: userSettings.app.sharePresence ? .read : .readPrivate) { + if case .failure(let error) = await roomProxy.markAsRead(receiptType: userSettings.account.sharePresence ? .read : .readPrivate) { MXLog.error("Failed marking room \(roomIdentifier) as read with error: \(error)") } case .failure(let error): @@ -395,7 +395,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol for summary in roomSummaryProvider.roomListPublisher.value { let room = HomeScreenRoom(summary: summary, - showAllActivity: userSettings.app.showAllRoomListActivity, + showAllActivity: userSettings.account.showAllRoomListActivity, seenInvites: seenInvites) rooms.append(room) } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 36e7ade67..52f30847d 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -127,7 +127,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol Task { // When navigating away from the room, we need to mark the room as both read // and fully read for Synapse to clear this room from the app's badge count. - _ = await roomProxy.markAsRead(receiptType: userSettings.app.sharePresence ? .read : .readPrivate) + _ = await roomProxy.markAsRead(receiptType: userSettings.account.sharePresence ? .read : .readPrivate) _ = await roomProxy.markAsRead(receiptType: .fullyRead) } // Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier. diff --git a/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenModels.swift index f0035ec28..c8a8dcccc 100644 --- a/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/ModerationAndSafetySettingsScreen/ModerationAndSafetySettingsScreenModels.swift @@ -32,8 +32,8 @@ struct ModerationAndSafetySettingsScreenViewStateBindings { } var sharePresence: Bool { - get { userSettings.app.sharePresence } - set { userSettings.app.sharePresence = newValue } + get { userSettings.account.sharePresence } + set { userSettings.account.sharePresence = newValue } } } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift index efc15d00d..b89fa0c3d 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift @@ -39,10 +39,10 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy self.notificationToneManager = notificationToneManager let bindings = NotificationSettingsScreenViewStateBindings(enableNotifications: userSettings.app.enableNotifications, - showAllRoomListActivity: userSettings.app.showAllRoomListActivity) + showAllRoomListActivity: userSettings.account.showAllRoomListActivity) super.init(initialViewState: NotificationSettingsScreenViewState(bindings: bindings, isModallyPresented: isModallyPresented, - selectedAlertTone: userSettings.app.selectedNotificationTone ?? NotificationToneManager.defaultElementXMessageTone, + selectedAlertTone: userSettings.account.selectedNotificationTone ?? userSettings.account.defaultNotificationTone.publisher.value, availableCustomTones: notificationToneManager.customTones(), // macos lacks default sounds and its sandbox Sounds directory is immutable customToneSelectionEnabled: !ProcessInfo.processInfo.isiOSAppOnMac)) @@ -52,8 +52,8 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy .weakAssign(to: \.state.bindings.enableNotifications, on: self) .store(in: &cancellables) - userSettings.app.selectedNotificationTonePublisher - .map { $0 ?? NotificationToneManager.defaultElementXMessageTone } + userSettings.account.selectedNotificationTonePublisher + .map { $0 ?? userSettings.account.defaultNotificationTone.publisher.value } .weakAssign(to: \.state.selectedAlertTone, on: self) .store(in: &cancellables) @@ -104,7 +104,7 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy } Task { await enableInvitations(state.bindings.invitationsEnabled) } case .showAllRoomListActivityChanged: - userSettings.app.showAllRoomListActivity = state.bindings.showAllRoomListActivity + userSettings.account.showAllRoomListActivity = state.bindings.showAllRoomListActivity case .close: actionsSubject.send(.close) case .fixConfigurationMismatchTapped: @@ -302,7 +302,7 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy try notificationToneManager.deleteCustomTone(tone) if tone == state.selectedAlertTone { - userSettings.app.selectedNotificationTone = nil + userSettings.account.selectedNotificationTone = nil } } catch { MXLog.error("Error deleting alert tone \(tone.label): \(error)") diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 82a5fc94f..0ced31ea2 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -272,7 +272,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { case .voiceMessage(let voiceMessageAction): processVoiceMessageAction(voiceMessageAction) case .contentChanged(let isEmpty): - guard userSettings.app.sharePresence else { + guard userSettings.account.sharePresence else { return } @@ -508,7 +508,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { roomProxy.typingMembersPublisher .receive(on: DispatchQueue.main) - .filter { [weak self] _ in self?.userSettings.app.sharePresence ?? false } + .filter { [weak self] _ in self?.userSettings.account.sharePresence ?? false } .weakAssign(to: \.state.typingMembers, on: self) .store(in: &cancellables) @@ -572,7 +572,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } private func setupUserSettingsSubscriptions() { - userSettings.app.sharePresencePublisher + userSettings.account.sharePresencePublisher .weakAssign(to: \.state.showReadReceipts, on: self) .store(in: &cancellables) diff --git a/ElementX/Sources/Services/AlertTones/NotificationTone.swift b/ElementX/Sources/Services/AlertTones/NotificationTone.swift index 88a37c0c2..173c904ca 100644 --- a/ElementX/Sources/Services/AlertTones/NotificationTone.swift +++ b/ElementX/Sources/Services/AlertTones/NotificationTone.swift @@ -43,6 +43,9 @@ nonisolated struct NotificationTone: Hashable, Comparable, Codable { case appLibrary } + /// The default bundled message tone. + static let `default`: NotificationTone = .createBundledSound(label: L10n.screenNotificationSettingsSoundElementDefault, filename: "message.caf") + /// Creates a tone backed by a file in the system sounds directory. static func createSystemSound(label: String?, filename: String, systemSoundsSubdirectory: [String] = []) -> NotificationTone { NotificationTone(labelOverride: label, storageLocationRoot: .system, relativePath: systemSoundsSubdirectory + [filename]) diff --git a/ElementX/Sources/Services/AlertTones/NotificationToneManager.swift b/ElementX/Sources/Services/AlertTones/NotificationToneManager.swift index 41e9fe021..48c7386df 100644 --- a/ElementX/Sources/Services/AlertTones/NotificationToneManager.swift +++ b/ElementX/Sources/Services/AlertTones/NotificationToneManager.swift @@ -31,26 +31,40 @@ nonisolated struct NotificationToneManager: NotificationToneManagerProtocol { private let userSettings: UserSettings - /// The default Element X bundled message tone. - static let defaultElementXMessageTone: NotificationTone = .createBundledSound(label: L10n.screenNotificationSettingsSoundElementDefault, - filename: "message.caf") - /// All default tones (system + Element X), sorted by name. - static let allDefaultAlerts: [NotificationTone] = (defaultSystemAlerts + defaultElementXAlerts).sorted() + static let allDefaultAlerts: [NotificationTone] = (defaultSystemAlerts + defaultBundledAlerts).sorted() - /// Filename of the active tone file used by the notification service. - static let selectedToneFilename = "currentAlert.caf" + /// The name the notification service uses to find the tone's file in the app bundle or Library/Sounds. + /// + /// Note: Names with a subdirectory aren't documented for `UNNotificationSound(named:)` but do work. + static func soundName(for tone: NotificationTone) -> String { + switch tone.storageLocationRoot { + case .appBundle: + tone.filename + case .appLibrary: + "\(URL.customTonesDirectory.lastPathComponent)/\(tone.filename)" + case .system: + "\(URL.systemTonesDirectory.lastPathComponent)/\(tone.filename)" + } + } - /// Directory where user-imported custom tones are stored. - nonisolated static let libraryLocation = URL.libraryDirectory.appending(components: "Sounds", "AvailableSounds", directoryHint: .isDirectory) + /// Copies a system tone into Library/Sounds, as the system sounds directory isn't searched for notification sounds. + static func copySystemToneIfNeeded(_ tone: NotificationTone) throws { + guard tone.storageLocationRoot == .system else { return } + + let copyLocation = URL.systemTonesDirectory.appending(component: tone.filename) + guard (try? copyLocation.checkResourceIsReachable()) != true else { return } + + try FileManager.default.createDirectory(at: URL.systemTonesDirectory, withIntermediateDirectories: true) + try FileManager.default.copyItem(at: toneLocation(for: tone), to: copyLocation) + } /// Creates the manager and ensures required library directories exist. init(userSettings: UserSettings) { self.userSettings = userSettings do { - try FileManager.default.createDirectory(at: NotificationToneManager.libraryLocation, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: Self.selectedToneLocation.deletingLastPathComponent(), withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: URL.customTonesDirectory, withIntermediateDirectories: true) } catch { // Don't crash on a recoverable file system error. The underlying problem (directory creation // failing on launch, e.g. a background launch before the container is writable) is acknowledged @@ -60,29 +74,17 @@ nonisolated struct NotificationToneManager: NotificationToneManagerProtocol { } /// Sets the given tone as the active notification alert tone. - /// - /// Copies the tone's audio file to `selectedToneLocation` and persists the selection in app settings. func setSelectedTone(_ alertTone: NotificationTone) throws -> URL { - do { - try? FileManager.default.removeItem(at: Self.selectedToneLocation) - let toneLocation = Self.toneLocation(for: alertTone) - try FileManager.default.copyItem(at: toneLocation, to: Self.selectedToneLocation) - userSettings.app.selectedNotificationTone = alertTone - return Self.selectedToneLocation - } catch { - if (try? Self.selectedToneLocation.checkResourceIsReachable()) != true { - // make sure the selected tone is reset if there's no custom tone present - userSettings.app.selectedNotificationTone = nil - } - throw error - } + try Self.copySystemToneIfNeeded(alertTone) + userSettings.account.selectedNotificationTone = alertTone + return Self.toneLocation(for: alertTone) } /// Returns all user-imported CAF tones from the library directory, sorted by name. func customTones() -> [NotificationTone] { let availableFiles = try? FileManager .default - .contentsOfDirectory(at: NotificationToneManager.libraryLocation, includingPropertiesForKeys: nil) + .contentsOfDirectory(at: URL.customTonesDirectory, includingPropertiesForKeys: nil) return (availableFiles ?? []) .compactMap { @@ -100,7 +102,7 @@ nonisolated struct NotificationToneManager: NotificationToneManagerProtocol { @discardableResult func addNewToneToLibrary(from sourceURL: URL) throws -> URL { let baseName = sourceURL.deletingPathExtension().lastPathComponent - let outputURL = NotificationToneManager.libraryLocation.appending(component: baseName).appendingPathExtension("caf") + let outputURL = URL.customTonesDirectory.appending(component: baseName).appendingPathExtension("caf") guard (try? outputURL.checkResourceIsReachable()) != true else { throw ManagerError.fileAlreadyExists @@ -119,7 +121,7 @@ nonisolated struct NotificationToneManager: NotificationToneManagerProtocol { /// - Throws: `DeletionError.notACustomTone` if the tone is not stored in the library directory. func deleteCustomTone(_ alertTone: NotificationTone) throws { let toneLocation = Self.toneLocation(for: alertTone) - guard toneLocation.deletingLastPathComponent() == NotificationToneManager.libraryLocation else { + guard toneLocation.deletingLastPathComponent() == URL.customTonesDirectory else { throw ManagerError.notACustomTone } @@ -172,9 +174,6 @@ nonisolated struct NotificationToneManager: NotificationToneManagerProtocol { MXLog.info("Converted \(sourceURL.path(percentEncoded: false)) to caf") } - /// File URL of the active tone copied/linked for use by the system. - private static let selectedToneLocation = NotificationToneManager.libraryLocation.deletingLastPathComponent().appending(component: selectedToneFilename) - /// Pre-defined iOS system tones available for selection, sorted by name. private static let defaultSystemAlerts: [NotificationTone] = [ .createSystemSound(label: L10n.screenNotificationSettingsSoundSystemTriToneIos, @@ -256,9 +255,9 @@ nonisolated struct NotificationToneManager: NotificationToneManagerProtocol { } .sorted() - /// Element X bundled tones available for selection, sorted by name. - private static let defaultElementXAlerts: [NotificationTone] = [ - defaultElementXMessageTone, + /// Bundled tones available for selection, sorted by name. + private static let defaultBundledAlerts: [NotificationTone] = [ + .default, .createBundledSound(label: L10n.screenNotificationSettingsSoundElementFade, filename: "sound_01.caf") ].sorted() @@ -280,7 +279,7 @@ nonisolated struct NotificationToneManager: NotificationToneManagerProtocol { case .appBundle: root = Self.bundledLocation case .appLibrary: - root = Self.libraryLocation + root = URL.customTonesDirectory } return tone.relativePath.reduce(root) { diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index c1b3091b7..22d5cc928 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -235,9 +235,6 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { extension NotificationManager: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { - guard appSettings.enableInAppNotifications else { - return [] - } guard let delegate else { return [.badge, .sound, .list, .banner] } diff --git a/ElementX/Sources/Services/Presence/PresenceService.swift b/ElementX/Sources/Services/Presence/PresenceService.swift index 5edf39731..aafba1e1e 100644 --- a/ElementX/Sources/Services/Presence/PresenceService.swift +++ b/ElementX/Sources/Services/Presence/PresenceService.swift @@ -41,7 +41,7 @@ final class PresenceService { } private var desiredPresence: ClientProxyPresence { - switch (isForegroundActive, userSettings.app.sharePresence) { + switch (isForegroundActive, userSettings.account.sharePresence) { case (true, true): .online case (true, false): @@ -68,7 +68,7 @@ final class PresenceService { } private func observeSharePresence() { - userSettings.app.sharePresencePublisher + userSettings.account.sharePresencePublisher .removeDuplicates() .sink { [weak self] _ in self?.reportCurrentState() diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index 7197a39b4..24c9201af 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -128,7 +128,7 @@ class TimelineController: TimelineControllerProtocol { } func sendReadReceipt(for itemID: TimelineItemIdentifier) async { - let receiptType: MatrixRustSDK.ReceiptType = userSettings.app.sharePresence ? .read : .readPrivate + let receiptType: MatrixRustSDK.ReceiptType = userSettings.account.sharePresence ? .read : .readPrivate guard let eventID = itemID.eventID else { return diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index b59f223c5..f669fe946 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -57,7 +57,8 @@ class UserSessionStore: UserSessionStoreProtocol { return .failure(.unknownAccount) } - let userSettings = appSettings.userSettings(for: credentials.userID) + let userSettings = appHooks.userSessionHook.configure(appSettings.userSettings(for: credentials.userID), + forHomeserver: credentials.restorationToken.session.homeserverUrl) switch await restorePreviousLogin(credentials, userSettings: userSettings) { case .success(let clientProxy): @@ -81,7 +82,7 @@ class UserSessionStore: UserSessionStoreProtocol { do { let session = try client.session() let userID = try client.userId() - let userSettings = appSettings.userSettings(for: userID) + let userSettings = appHooks.userSessionHook.configure(appSettings.userSettings(for: userID), forHomeserver: client.homeserver()) let clientProxy = try await setupProxyForClient(client, userSettings: userSettings) keychainController.setRestorationToken(RestorationToken(session: session, diff --git a/Enterprise b/Enterprise index 8ff9415f2..191c56f2b 160000 --- a/Enterprise +++ b/Enterprise @@ -1 +1 @@ -Subproject commit 8ff9415f262e66cf530b63a6415d74b6ea080242 +Subproject commit 191c56f2bb31d04b92d66b81c7db750a9accf5dc diff --git a/NSE/Sources/CommonSettings+NotificationName.swift b/NSE/Sources/CommonSettings+NotificationName.swift deleted file mode 100644 index ec3a4c9a2..000000000 --- a/NSE/Sources/CommonSettings+NotificationName.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright 2026 Element Creations Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import UserNotifications - -nonisolated extension AppSettings { - /// The sound name to use in outgoing notifications. - /// Falls back to the default Element X tone if no custom tone has been selected. - var notificationSoundName: UNNotificationSoundName { - if selectedNotificationTone == nil { - return UNNotificationSoundName("message.caf") - } else { - return UNNotificationSoundName(NotificationToneManager.selectedToneFilename) - } - } - - /// A `UNNotificationSound` built from `notificationSoundName`, ready to attach to a notification content object. - var notificationSound: UNNotificationSound { - UNNotificationSound(named: notificationSoundName) - } -} diff --git a/NSE/Sources/Extensions/NSEAccountSettings.swift b/NSE/Sources/Extensions/NSEAccountSettings.swift new file mode 100644 index 000000000..b9d64d23c --- /dev/null +++ b/NSE/Sources/Extensions/NSEAccountSettings.swift @@ -0,0 +1,15 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import UserNotifications + +nonisolated extension AccountSettings { + /// The sound to attach to outgoing notifications, falling back to the default tone if no tone has been selected. + var notificationSound: UNNotificationSound { + (selectedNotificationTone ?? defaultNotificationTone.publisher.value).notificationSound + } +} diff --git a/NSE/Sources/Extensions/NSENotificationTone.swift b/NSE/Sources/Extensions/NSENotificationTone.swift new file mode 100644 index 000000000..a579f67a7 --- /dev/null +++ b/NSE/Sources/Extensions/NSENotificationTone.swift @@ -0,0 +1,15 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import UserNotifications + +nonisolated extension NotificationTone { + /// A `UNNotificationSound` for this tone, ready to attach to a notification content object. + var notificationSound: UNNotificationSound { + UNNotificationSound(named: UNNotificationSoundName(NotificationToneManager.soundName(for: self))) + } +} diff --git a/NSE/Sources/NSEUserSession.swift b/NSE/Sources/NSEUserSession.swift index 3765affe3..ad66dfefa 100644 --- a/NSE/Sources/NSEUserSession.swift +++ b/NSE/Sources/NSEUserSession.swift @@ -11,6 +11,7 @@ import MatrixRustSDK // sourcery: AutoMockable nonisolated protocol NSEUserSessionProtocol { + var userSettings: UserSettings { get } var inviteAvatarsVisibility: InviteAvatars { get async } var mediaPreviewVisibility: MediaPreviews { get async } var threadsEnabled: Bool { get } @@ -59,7 +60,8 @@ final nonisolated class NSEUserSession: NSEUserSessionProtocol { appSettings: AppSettings, appHooks: AppHooks) async throws { userID = credentials.userID - userSettings = appSettings.userSettings(for: credentials.userID) + userSettings = appHooks.userSessionHook.configure(appSettings.userSettings(for: credentials.userID), + forHomeserver: credentials.restorationToken.session.homeserverUrl) baseClient = try await clientFactory.makeNSEClient(credentials: credentials, roomID: roomID, diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift index 98c331fbd..1b563adff 100644 --- a/NSE/Sources/NotificationContentBuilder.swift +++ b/NSE/Sources/NotificationContentBuilder.swift @@ -15,7 +15,6 @@ import Version nonisolated struct NotificationContentBuilder { let messageEventStringBuilder: RoomMessageEventStringBuilder - let notificationSoundName: UNNotificationSoundName let userSession: NSEUserSessionProtocol /// Process the given notification item proxy @@ -50,7 +49,7 @@ nonisolated struct NotificationContentBuilder { notificationContent.threadIdentifier = threadIdentifier.replacingOccurrences(of: "@", with: "") MXLog.info("isNoisy: \(notificationItem.isNoisy)") - notificationContent.sound = notificationItem.isNoisy ? .init(named: notificationSoundName) : nil + notificationContent.sound = notificationItem.isNoisy ? userSession.userSettings.account.notificationSound : nil switch notificationItem.event { case .none: diff --git a/NSE/Sources/NotificationHandler.swift b/NSE/Sources/NotificationHandler.swift index c4b5bb365..a4c71e8f2 100644 --- a/NSE/Sources/NotificationHandler.swift +++ b/NSE/Sources/NotificationHandler.swift @@ -39,7 +39,6 @@ nonisolated class NotificationHandler { style: .plain) notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: eventStringBuilder, - notificationSoundName: userSession.userSettings.app.notificationSoundName, userSession: userSession) } diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 70a204f57..340b4e95f 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -257,7 +257,7 @@ actor NotificationServiceExtensionActor { let content = UNMutableNotificationContent() content.body = L10n.notificationReceivedWhileOfflineIos - content.sound = appSettings.notificationSound + content.sound = NotificationTone.default.notificationSound // Use the default sound as missed notifications could be for any account. let request = UNNotificationRequest(identifier: Self.receivedWhileOfflineNotificationID, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml index d6b0b781d..489dfad8a 100644 --- a/NSE/SupportingFiles/target.yml +++ b/NSE/SupportingFiles/target.yml @@ -89,6 +89,7 @@ targets: - path: ../../ElementX/Sources/AppHooks/Hooks/TracingHook.swift - path: ../../ElementX/Sources/AppHooks/Hooks/ClientFactoryHook.swift - path: ../../ElementX/Sources/AppHooks/Hooks/RemoteSettingsHook.swift + - path: ../../ElementX/Sources/AppHooks/Hooks/UserSessionHook.swift - path: ../../Components/Secrets/Secrets.swift - path: ../../ElementX/Sources/Application/Settings - path: ../../ElementX/Sources/Application/TargetConfiguration.swift diff --git a/ShareExtension/Sources/ShareExtensionViewController.swift b/ShareExtension/Sources/ShareExtensionViewController.swift index b04b064a7..92f6479a6 100644 --- a/ShareExtension/Sources/ShareExtensionViewController.swift +++ b/ShareExtension/Sources/ShareExtensionViewController.swift @@ -51,6 +51,7 @@ class ShareExtensionViewController: UIViewController { if let credentials = keychainController.restorationTokens().first { let homeserverURL = credentials.restorationToken.session.homeserverUrl appHooks.remoteSettingsHook.loadCache(forHomeserver: homeserverURL, applyingTo: appSettings) + _ = appHooks.userSessionHook.configure(appSettings.userSettings(for: credentials.userID), forHomeserver: homeserverURL) } else { // We should really show a different state when there isn't a logged in user, but for now this is fine. MXLog.error("Not logged in, launching app to show the authentication flow.") diff --git a/ShareExtension/SupportingFiles/target.yml b/ShareExtension/SupportingFiles/target.yml index e035a4ccf..8e5d502d1 100644 --- a/ShareExtension/SupportingFiles/target.yml +++ b/ShareExtension/SupportingFiles/target.yml @@ -89,9 +89,11 @@ targets: - path: ../../ElementX/Sources/AppHooks/Hooks/TracingHook.swift - path: ../../ElementX/Sources/AppHooks/Hooks/ClientFactoryHook.swift - path: ../../ElementX/Sources/AppHooks/Hooks/RemoteSettingsHook.swift + - path: ../../ElementX/Sources/AppHooks/Hooks/UserSessionHook.swift - path: ../../Components/Secrets/Secrets.swift - path: ../../ElementX/Sources/Application/Settings - path: ../../ElementX/Sources/Application/TargetConfiguration.swift + - path: ../../ElementX/Sources/Generated/Strings.swift - path: ../../ElementX/Sources/Services/AlertTones/NotificationTone.swift - path: ../../ElementX/Sources/Services/Keychain/KeychainController.swift - path: ../../ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift diff --git a/TIDY.md b/TIDY.md new file mode 100644 index 000000000..bd7944744 --- /dev/null +++ b/TIDY.md @@ -0,0 +1,5 @@ +# Tidy + +Follow-ups that aren't worth their own PR yet. Do each item in one go, not piecemeal. + +- **Enterprise hook tests should use `EnterpriseESSServiceMock(configuration:)` instead of the on-disk cache.** Today `EnterpriseCreateRoomScreenHookTests`, `EnterpriseSecurityAndPrivacyScreenHookTests`, `EnterpriseUserSessionHookTests` and parts of `EnterpriseRemoteSettingsHookTests` seed a real `EnterpriseESSService` by writing an `EnterpriseESSConfiguration` to `URL.cachedESSConfigurationFile(for:)`, and each wipes `.enterpriseApplicationSupportBaseDirectory` in `init`. Passing the mock removes the disk dependency and the shared-directory coupling. Switch all of them together so the pattern stays consistent, and leave `EnterpriseESSServiceTests` alone since the cache is what it tests. diff --git a/UnitTests/Sources/NotificationContentBuilderTests.swift b/UnitTests/Sources/NotificationContentBuilderTests.swift index 8e49cbd89..59ed63986 100644 --- a/UnitTests/Sources/NotificationContentBuilderTests.swift +++ b/UnitTests/Sources/NotificationContentBuilderTests.swift @@ -23,7 +23,6 @@ nonisolated struct NotificationContentBuilderTests { style: .plain) mediaProvider = MediaProviderMock(.init()) notificationContentBuilder = await NotificationContentBuilder(messageEventStringBuilder: stringBuilder, - notificationSoundName: UNNotificationSoundName("message.caf"), userSession: NSEUserSessionMock(.init())) } diff --git a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift index 6780df938..ee8458aa3 100644 --- a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift @@ -411,29 +411,29 @@ struct NotificationSettingsScreenViewModelTests { // Given a tone and a manager that persists the selection to app settings let tone = NotificationTone.createBundledSound(label: "Test", filename: "test.caf") toneManager.setSelectedToneClosure = { [userSettings] selectedTone in - userSettings.app.selectedNotificationTone = selectedTone - return NotificationToneManager.libraryLocation + userSettings.account.selectedNotificationTone = selectedTone + return URL.customTonesDirectory } // When the user selects that tone context.send(viewAction: .selectAlertTone(tone)) // Then it is reflected in app settings - #expect(userSettings.app.selectedNotificationTone == tone) + #expect(userSettings.account.selectedNotificationTone == tone) } @Test func deletingActiveToneResetsSelection() { // Given the active tone is a custom tone let customTone = NotificationTone.createCustomUserSound(filename: "custom.caf") - userSettings.app.selectedNotificationTone = customTone + userSettings.account.selectedNotificationTone = customTone toneManager.customTonesReturnValue = [customTone] // When that tone is deleted context.send(viewAction: .deleteCustomAlertTones([customTone])) // Then the selection is cleared, falling back to the default - #expect(userSettings.app.selectedNotificationTone == nil) + #expect(userSettings.account.selectedNotificationTone == nil) } @Test @@ -441,14 +441,14 @@ struct NotificationSettingsScreenViewModelTests { // Given a custom tone is active and a different custom tone also exists let activeTone = NotificationTone.createCustomUserSound(filename: "active.caf") let otherTone = NotificationTone.createCustomUserSound(filename: "other.caf") - userSettings.app.selectedNotificationTone = activeTone + userSettings.account.selectedNotificationTone = activeTone toneManager.customTonesReturnValue = [activeTone, otherTone] // When the non-active tone is deleted context.send(viewAction: .deleteCustomAlertTones([otherTone])) // Then the active selection is unchanged - #expect(userSettings.app.selectedNotificationTone == activeTone) + #expect(userSettings.account.selectedNotificationTone == activeTone) } @Test @@ -476,7 +476,7 @@ struct NotificationSettingsScreenViewModelTests { func deleteCustomToneFailurePreservesSelection() { // Given the active tone is a custom tone and deletion will fail let customTone = NotificationTone.createCustomUserSound(filename: "custom.caf") - userSettings.app.selectedNotificationTone = customTone + userSettings.account.selectedNotificationTone = customTone toneManager.deleteCustomToneThrowableError = NSError(domain: "test", code: 1) toneManager.customTonesReturnValue = [customTone] @@ -484,14 +484,14 @@ struct NotificationSettingsScreenViewModelTests { context.send(viewAction: .deleteCustomAlertTones([customTone])) // Then the selection is unchanged - #expect(userSettings.app.selectedNotificationTone == customTone) + #expect(userSettings.account.selectedNotificationTone == customTone) } @Test func importingCustomToneRefreshesAvailableList() async throws { // Given a tone file ready to import let importedTone = NotificationTone.createCustomUserSound(filename: "imported.caf") - let location = NotificationToneManager.libraryLocation.appending(component: importedTone.filename) + let location = URL.customTonesDirectory.appending(component: importedTone.filename) toneManager.addNewToneToLibraryFromReturnValue = location toneManager.customTonesReturnValue = [importedTone] diff --git a/UnitTests/Sources/NotificationToneManagerTests.swift b/UnitTests/Sources/NotificationToneManagerTests.swift index ab88b1842..67d410319 100644 --- a/UnitTests/Sources/NotificationToneManagerTests.swift +++ b/UnitTests/Sources/NotificationToneManagerTests.swift @@ -10,7 +10,7 @@ import Foundation import Testing /// These tests exercise `NotificationToneManager` against the real app sandbox filesystem. -/// Because all tests share a single `libraryLocation` directory on disk, they are inherently +/// Because all tests share a single `customTonesDirectory` on disk, they are inherently /// incapable of running in parallel — files written by one test would corrupt the state of /// another. This project runs test in serial regardless, but this is just to document this is a specific case /// relying on that behavior. @@ -56,8 +56,8 @@ struct NotificationToneManagerTests { @Test func customTonesFiltersToCAFOnly() throws { // Given both CAF and non-CAF files written to the library directory - let cafURL = NotificationToneManager.libraryLocation.appending(component: "\(UUID().uuidString).caf") - let mp3URL = NotificationToneManager.libraryLocation.appending(component: "\(UUID().uuidString).mp3") + let cafURL = URL.customTonesDirectory.appending(component: "\(UUID().uuidString).caf") + let mp3URL = URL.customTonesDirectory.appending(component: "\(UUID().uuidString).mp3") defer { try? FileManager.default.removeItem(at: cafURL) try? FileManager.default.removeItem(at: mp3URL) @@ -79,7 +79,7 @@ struct NotificationToneManagerTests { func addingDuplicateToneThrowsFileAlreadyExists() async throws { // Given a CAF file that has already been imported into the library let sourceURL = URL.temporaryDirectory.appending(component: "\(UUID().uuidString).caf") - let importedURL = NotificationToneManager.libraryLocation + let importedURL = URL.customTonesDirectory .appending(component: sourceURL.deletingPathExtension().lastPathComponent) .appendingPathExtension("caf") defer { diff --git a/UnitTests/Sources/PresenceServiceTests.swift b/UnitTests/Sources/PresenceServiceTests.swift index de1d1a1ee..61dd61195 100644 --- a/UnitTests/Sources/PresenceServiceTests.swift +++ b/UnitTests/Sources/PresenceServiceTests.swift @@ -39,7 +39,7 @@ struct PresenceServiceTests { @Test func serviceSeededActiveWhileSharingReportsOnline() async throws { - userSettings.app.sharePresence = true + userSettings.account.sharePresence = true let deferred = deferFulfillment(sendsSubject) { $0.presence == .online } let service = makeService(initialApplicationState: .active) @@ -53,7 +53,7 @@ struct PresenceServiceTests { @Test func serviceSeededBackgroundedWhileSharingReportsUnavailable() async throws { - userSettings.app.sharePresence = true + userSettings.account.sharePresence = true let deferred = deferFulfillment(sendsSubject) { $0.presence == .unavailable } let service = makeService(initialApplicationState: .background) @@ -66,7 +66,7 @@ struct PresenceServiceTests { @Test func serviceSeededActiveWhileNotSharingReportsOffline() async throws { - userSettings.app.sharePresence = false + userSettings.account.sharePresence = false let deferred = deferFulfillment(sendsSubject) { $0.presence == .offline } let service = makeService(initialApplicationState: .active) @@ -79,7 +79,7 @@ struct PresenceServiceTests { @Test func becomingActiveNotificationWhileSharingReportsOnlineImmediately() async throws { - userSettings.app.sharePresence = true + userSettings.account.sharePresence = true let service = makeService(initialApplicationState: .inactive) let deferred = deferFulfillment(sendsSubject) { $0.presence == .online } @@ -93,7 +93,7 @@ struct PresenceServiceTests { @Test func resigningActiveNotificationWhileSharingReportsUnavailableImmediately() async throws { - userSettings.app.sharePresence = true + userSettings.account.sharePresence = true let initialDeferred = deferFulfillment(sendsSubject) { $0.presence == .online } let service = makeService(initialApplicationState: .active) _ = try await initialDeferred.fulfill() @@ -109,19 +109,19 @@ struct PresenceServiceTests { @Test func togglingSharingWhileActiveDrivesOnlineAndOffline() async throws { - userSettings.app.sharePresence = false + userSettings.account.sharePresence = false let initialDeferred = deferFulfillment(sendsSubject) { $0.presence == .offline } let service = makeService(initialApplicationState: .active) _ = try await initialDeferred.fulfill() let onlineDeferred = deferFulfillment(sendsSubject) { $0.presence == .online } - userSettings.app.sharePresence = true + userSettings.account.sharePresence = true let online = try await onlineDeferred.fulfill() #expect(online.presence == .online) #expect(online.sendImmediately) let offlineDeferred = deferFulfillment(sendsSubject) { $0.presence == .offline } - userSettings.app.sharePresence = false + userSettings.account.sharePresence = false let offline = try await offlineDeferred.fulfill() #expect(offline.presence == .offline) #expect(offline.sendImmediately) @@ -131,7 +131,7 @@ struct PresenceServiceTests { @Test func repeatingTheSameStateSendsOnlyOnce() async throws { - userSettings.app.sharePresence = true + userSettings.account.sharePresence = true let initialDeferred = deferFulfillment(sendsSubject) { $0.presence == .online } let service = makeService(initialApplicationState: .active) @@ -148,7 +148,7 @@ struct PresenceServiceTests { @Test func failedImmediateSendIsNotRetriedUntilTheDesiredPresenceChanges() async throws { - userSettings.app.sharePresence = true + userSettings.account.sharePresence = true clientProxy.configurePresenceSendImmediatelyClosure = { [sendsSubject] presence, sendImmediately in sendsSubject.send((presence, sendImmediately)) return presence == .online ? .failure(.forbiddenAccess) : .success(()) @@ -173,7 +173,7 @@ struct PresenceServiceTests { @Test func newerTransitionWinsWhenAnEarlierSendIsInFlight() async throws { - userSettings.app.sharePresence = true + userSettings.account.sharePresence = true let (onlineStartedStream, onlineStartedContinuation) = AsyncStream.makeStream() let (resumeOnlineSendStream, resumeOnlineSendContinuation) = AsyncStream.makeStream() @@ -206,7 +206,7 @@ struct PresenceServiceTests { @Test func deinitCancelsPendingSend() async throws { - userSettings.app.sharePresence = true + userSettings.account.sharePresence = true let (onlineStartedStream, onlineStartedContinuation) = AsyncStream.makeStream(of: Void.self) let (onlineFinishedStream, onlineFinishedContinuation) = AsyncStream.makeStream(of: Void.self) diff --git a/project.yml b/project.yml index 69770774e..f5d6ebe7f 100644 --- a/project.yml +++ b/project.yml @@ -51,7 +51,7 @@ settings: ENABLE_BITCODE: false APP_NAME: ElementX KEYCHAIN_ACCESS_GROUP_IDENTIFIER: "$(DEVELOPMENT_TEAM).$(BASE_BUNDLE_IDENTIFIER)" - MARKETING_VERSION: 26.10.1 + MARKETING_VERSION: 26.10.2 CURRENT_PROJECT_VERSION: 1 SUPPORTS_MACCATALYST: false ARCHS: arm64