diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 8a11be9e1..5501d8333 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -64,7 +64,6 @@ 0728314DD51AC3819F818EA8 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2711E5996016ABD6EAAEB58A /* LogLevel.swift */; }; 07376A5274822EB45CC320C7 /* InvitedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */; }; 0743CF689EBDAAF1CC0B4283 /* DeclineAndBlockScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011AFA4990C585D157829679 /* DeclineAndBlockScreenViewModel.swift */; }; - 074F741578307EF0179EE47C /* MapTilerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */; }; 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */; }; 07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */; }; 0847D85F823B55A3E95D16CC /* RoomPreviewProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D94852AD5BB376CBCC3544 /* RoomPreviewProxy.swift */; }; @@ -323,6 +322,7 @@ 34C752A73717C691582DC6C7 /* UnsupportedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */; }; 34F1261CEF6D6A00D559B520 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFD5EB0B0EEA4549FB49784 /* SettingsScreen.swift */; }; 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; }; + 354AFFDB2C05EC300E61D402 /* MapTilerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A7F76FE12E274D2ED8DF4 /* MapTilerSettings.swift */; }; 355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */; }; 3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */; }; 358290C2E4574456FE0C2DF6 /* LocationSharingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B73675A21580971E9ADF66A /* LocationSharingScreenViewModelProtocol.swift */; }; @@ -505,6 +505,7 @@ 53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */; }; 5415236FA271AF7885D4995E /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AB32690C6CE5C62A86D6FA /* NotificationItemProxy.swift */; }; 5470E62F65AE1803BBF3D528 /* CXProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86E1BAA7232081635662A83F /* CXProviderMock.swift */; }; + 54A9F957307CC55242564CE5 /* MapTilerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A7F76FE12E274D2ED8DF4 /* MapTilerSettings.swift */; }; 54AE8860D668AFD96E7E177B /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 54FDA3625AACBD9E438D084D /* BlurEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07934EF08BB39353E4A94272 /* BlurEffectView.swift */; }; 5500904C40D67D131E7E9A26 /* NotificationContentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */; }; @@ -567,6 +568,7 @@ 5FA1DCE55973862632961D7C /* PhotoLibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7495E1119753B06FF2C2279 /* PhotoLibraryManager.swift */; }; 5FCD8AFA364206EE32B909A3 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B050A6B233D95807A09289E7 /* Settings.bundle */; }; 601AB75BD52B0B4276CEB84A /* SessionVerificationScreenStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 161CD412E75F4086F422AE39 /* SessionVerificationScreenStateMachine.swift */; }; + 60A02A6FCE475B7F0A070836 /* MatrixClientWellKnownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2216751513D9DC0A855D657C /* MatrixClientWellKnownTests.swift */; }; 60ED66E63A169E47489348A8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = A20EA00CCB9DBE0FFB17DD09 /* Collections */; }; 611BEE29B8B622204E1E6B04 /* SecurityAndPrivacyScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F2F6B6E56055EF173A2DD3 /* SecurityAndPrivacyScreenCoordinator.swift */; }; 612196AF7826846A50C07FA6 /* SpaceServiceRoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCAA1B97A17A750AC706B112 /* SpaceServiceRoom.swift */; }; @@ -941,7 +943,6 @@ 9A0326D2375075871D2AB537 /* ResolveVerifiedUserSendFailureScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 574CB70E82D7EAEA538E4135 /* ResolveVerifiedUserSendFailureScreenViewModel.swift */; }; 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 9A8E6FCD86B89970EC72EFD8 /* BugReportServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7FC9580CABF797A2E6213A /* BugReportServiceMock.swift */; }; - 9AC47275B8E1EB0976BA7A80 /* MapTilerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */; }; 9AC5F8142413862A9E3A2D98 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; }; 9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */; }; 9B356742E035D90A8BB5CABE /* ProposedViewSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */; }; @@ -1100,6 +1101,7 @@ B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; }; B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; B6064D82FCDCB829601C1F59 /* SecureBackupLogoutConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */; }; + B66FBE9B08C202B955B7F6AC /* MapTilerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A7F76FE12E274D2ED8DF4 /* MapTilerSettings.swift */; }; B6B62437B92B6CA4083AA899 /* SessionDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C2067FF58B4996323EB40C /* SessionDirectories.swift */; }; B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; @@ -1167,7 +1169,6 @@ C1A5C386319835FB0C77736B /* ReportContentScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */; }; C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 622D09D4ECE759189009AEAF /* MapLibreMapView.swift */; }; C26DB49C06C00B5DF1A991A5 /* InviteUsersScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */; }; - C280BD841DA2EB6B5BC84F84 /* MapTilerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */; }; C2879369106A419A5071F1F8 /* VoiceMessageRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */; }; C292A8DA07E6DBD66E946383 /* RageshakeConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC598338E7CF41107293AB5 /* RageshakeConfiguration.swift */; }; C3317EF833AB4060988DF098 /* SAS.strings in Resources */ = {isa = PBXBuildFile; fileRef = 135FC689EA39AE1D34153B58 /* SAS.strings */; }; @@ -1805,6 +1806,7 @@ 1BEE119165C106FA83F253EF /* AnalyticsServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsServiceMock.swift; sourceTree = ""; }; 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = ""; }; 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; + 1C3A7F76FE12E274D2ED8DF4 /* MapTilerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerSettings.swift; sourceTree = ""; }; 1C78111573987B1D79ED0868 /* LinkMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMetadataProvider.swift; sourceTree = ""; }; 1C782FCBBCC9A0CD30453C50 /* ChatsTabFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsTabFlowCoordinator.swift; sourceTree = ""; }; 1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlankFormCoordinator.swift; sourceTree = ""; }; @@ -1842,6 +1844,7 @@ 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineItemProtocol.swift; sourceTree = ""; }; 21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = ""; }; 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFlowTests.swift; sourceTree = ""; }; + 2216751513D9DC0A855D657C /* MatrixClientWellKnownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixClientWellKnownTests.swift; sourceTree = ""; }; 226A56746AD99ABDA97189E1 /* SpaceServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxy.swift; sourceTree = ""; }; 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenViewModelProtocol.swift; sourceTree = ""; }; 227AC5D71A4CE43512062243 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; @@ -2423,7 +2426,6 @@ 89BB11A792EF6F70B95B467E /* EncryptionResetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetTests.swift; sourceTree = ""; }; 89D7846847BA9D2C5346ED9C /* EditAvatarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAvatarButtonStyle.swift; sourceTree = ""; }; 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormButtonStyles.swift; sourceTree = ""; }; - 8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerConfiguration.swift; sourceTree = ""; }; 8A8DCBD0ABAADFDE5AF17E1F /* LiveLocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationRoomTimelineItem.swift; sourceTree = ""; }; 8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = ""; }; @@ -4970,6 +4972,7 @@ 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */, D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */, 1D262A26713C18BB70C82CA5 /* MapTilerURLBuilderTests.swift */, + 2216751513D9DC0A855D657C /* MatrixClientWellKnownTests.swift */, F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */, 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */, AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */, @@ -6566,7 +6569,7 @@ F78B4E56DBFFD4A7A39D10F5 /* AudioPlaybackSpeed.swift */, 0A2074C0449B83D5858BD2D7 /* FrequentlyUsedEmoji.swift */, DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */, - 8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */, + 1C3A7F76FE12E274D2ED8DF4 /* MapTilerSettings.swift */, 002DE5E9625122F37DE5F020 /* OAuthConfiguration.swift */, 8FC598338E7CF41107293AB5 /* RageshakeConfiguration.swift */, 69A05E85E4872C3221C5C287 /* RemotePreference.swift */, @@ -7839,7 +7842,7 @@ D3ED5692672892F9F1E7375A /* LiveLocationSession.swift in Sources */, 0728314DD51AC3819F818EA8 /* LogLevel.swift in Sources */, AD2A81B65A9F6163012086F1 /* MXLog.swift in Sources */, - 9AC47275B8E1EB0976BA7A80 /* MapTilerConfiguration.swift in Sources */, + 54A9F957307CC55242564CE5 /* MapTilerSettings.swift in Sources */, 52473A4D7B1FBD4CD1E770C8 /* MatrixEntityRegex.swift in Sources */, 8B76191B9DDD1AC90A6E3A35 /* MediaFileHandleProxy.swift in Sources */, 68184EF36396424FE19A727D /* MediaLoader.swift in Sources */, @@ -7954,6 +7957,7 @@ 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */, 1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */, 3BEBDCB42BABFA3B456FECA7 /* MapTilerURLBuilderTests.swift in Sources */, + 60A02A6FCE475B7F0A070836 /* MatrixClientWellKnownTests.swift in Sources */, 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */, 4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */, 3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */, @@ -8077,7 +8081,7 @@ A0646F23876D6326AD27FF52 /* LiveLocationSession.swift in Sources */, DA10C99BA43A0F1E732F6274 /* LogLevel.swift in Sources */, 0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */, - 074F741578307EF0179EE47C /* MapTilerConfiguration.swift in Sources */, + B66FBE9B08C202B955B7F6AC /* MapTilerSettings.swift in Sources */, 1A3783005E6945F8583AF997 /* NSItemProvider.swift in Sources */, E7416C2EFDC2D475F69127FE /* NotificationTone.swift in Sources */, F43564003B017DB148DB1503 /* OAuthConfiguration.swift in Sources */, @@ -8583,7 +8587,7 @@ C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */, C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */, E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */, - C280BD841DA2EB6B5BC84F84 /* MapTilerConfiguration.swift in Sources */, + 354AFFDB2C05EC300E61D402 /* MapTilerSettings.swift in Sources */, 63DCEBC1DD555E0D645B9E98 /* MapTilerURLBuilderProtocol.swift in Sources */, F382E2FE3AC3719BC1F22D9C /* MapURLs.swift in Sources */, 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */, diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index c3856555d..e7c29c8c4 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -834,6 +834,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg tearDownUserSession() appSettings.resetSessionSpecificSettings() + appSettings.mapTilerSettings.reset() appHooks.remoteSettingsHook.reset(appSettings) // Reset analytics diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index bb4aa0e8f..16f0947f0 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -144,7 +144,7 @@ final class AppSettings: @unchecked Sendable { accountProvisioningHost: String, bugReportApplicationID: String, analyticsTermsURL: URL?, - mapTilerConfiguration: MapTilerConfiguration) { + mapTilerConfiguration: MapTilerSettings.Configuration) { self.accountProviders = accountProviders self.allowOtherAccountProviders = allowOtherAccountProviders self.hideBrandChrome = hideBrandChrome @@ -164,7 +164,7 @@ final class AppSettings: @unchecked Sendable { self.accountProvisioningHost = accountProvisioningHost self.bugReportApplicationID = bugReportApplicationID self.analyticsTermsURL = analyticsTermsURL - self.mapTilerConfiguration = mapTilerConfiguration + mapTilerSettings = RemotePreference(.configuration(mapTilerConfiguration)) } // MARK: - Application @@ -399,11 +399,16 @@ final class AppSettings: @unchecked Sendable { // MARK: - Maps - /// maptiler base url - private(set) var mapTilerConfiguration = MapTilerConfiguration(baseURL: "https://api.maptiler.com/maps", - apiKey: Secrets.mapLibreAPIKey, - lightStyleID: "9bc819c8-e627-474a-a348-ec144fe3d810", - darkStyleID: "dea61faf-292b-4774-9660-58fcef89a7f3") + /// The locally-bundled MapTiler configuration. + static let bundledMapTilerConfiguration = MapTilerSettings.Configuration(baseURL: "https://api.maptiler.com/maps", + apiKey: Secrets.mapLibreAPIKey, + lightStyleID: "9bc819c8-e627-474a-a348-ec144fe3d810", + darkStyleID: "dea61faf-292b-4774-9660-58fcef89a7f3") + + /// The resolved map tile settings. Defaults to ``MapTilerSettings.configuration(_:)`` with the + /// bundled configuration and is remotely overridden with ``MapTilerSettings.url(_:)`` when + /// the homeserver advertises a `style.json` URL via the matrix client well-known. + private(set) var mapTilerSettings = RemotePreference(.configuration(AppSettings.bundledMapTilerConfiguration)) // MARK: - Presence diff --git a/ElementX/Sources/Application/Settings/MapTilerConfiguration.swift b/ElementX/Sources/Application/Settings/MapTilerConfiguration.swift deleted file mode 100644 index 0671b7de1..000000000 --- a/ElementX/Sources/Application/Settings/MapTilerConfiguration.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright 2025 Element Creations Ltd. -// Copyright 2025 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Foundation - -/// All of the configuration necessary to use MapTiler maps. -/// -/// The style IDs need to be generated with the account that the API key belongs to. For more information read -/// [FORKING.md](https://github.com/element-hq/element-x-ios/blob/develop/docs/FORKING.md#setup-the-location-sharing) -struct MapTilerConfiguration { - let baseURL: URL - /// The API key for fetching map tiles. When not set, location sharing will be disabled and - /// any received locations will be shown in the timeline with a generic blurred map image. - let apiKey: String? - /// A MapLibre style ID for a light-mode map. - let lightStyleID: String - /// A MapLibre style ID for a dark-mode map. - let darkStyleID: String - - var isEnabled: Bool { - apiKey != nil - } -} diff --git a/ElementX/Sources/Application/Settings/MapTilerSettings.swift b/ElementX/Sources/Application/Settings/MapTilerSettings.swift new file mode 100644 index 000000000..cb74d4e78 --- /dev/null +++ b/ElementX/Sources/Application/Settings/MapTilerSettings.swift @@ -0,0 +1,40 @@ +// +// Copyright 2025 Element Creations Ltd. +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Foundation + +/// The map tile settings used to build interactive and static map URLs. +/// +/// Either resolves to a locally-bundled MapTiler configuration, or to a remote +/// `style.json` URL +enum MapTilerSettings: Equatable { + /// All of the configuration necessary to use MapTiler maps. + /// + /// The style IDs need to be generated with the account that the API key belongs to. For more information read + /// [FORKING.md](https://github.com/element-hq/element-x-ios/blob/develop/docs/FORKING.md#setup-the-location-sharing) + struct Configuration: Equatable { + let baseURL: URL + /// The API key for fetching map tiles. When not set, location sharing will be disabled and + /// any received locations will be shown in the timeline with a generic blurred map image. + let apiKey: String? + /// A MapLibre style ID for a light-mode map. + let lightStyleID: String + /// A MapLibre style ID for a dark-mode map. + let darkStyleID: String + } + + case configuration(Configuration) + case url(URL) + + var isEnabled: Bool { + switch self { + case .configuration(let configuration): configuration.apiKey != nil + case .url: true + } + } +} diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index 985aa3700..4e09cabc4 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -107,7 +107,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { let stackCoordinator = NavigationStackCoordinator() let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode, - mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration, + mapURLBuilder: flowParameters.appSettings.mapTilerSettings.publisher.value, roomProxy: roomProxy, timelineController: timelineController, liveLocationManager: flowParameters.userSession.liveLocationManager, diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index a568860bd..c80508e17 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -1181,7 +1181,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let stackCoordinator = NavigationStackCoordinator() let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode, - mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration, + mapURLBuilder: flowParameters.appSettings.mapTilerSettings.publisher.value, roomProxy: roomProxy, timelineController: timelineController, liveLocationManager: flowParameters.userSession.liveLocationManager, diff --git a/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift b/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift index 41e2dab0a..4fb72be8a 100644 --- a/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift +++ b/ElementX/Sources/Mocks/SDK/ClientSDKMock.swift @@ -21,6 +21,7 @@ extension ClientSDKMock { var supportsOAuthCreatePrompt = true var supportsPasswordLogin = true var elementWellKnown: String? + var tileServerMapStyleURL: String? var validCredentials = (username: "alice", password: "12345678") // MARK: Session @@ -68,6 +69,7 @@ extension ClientSDKMock { throw MockError.generic } } + tileServerReturnValue = configuration.tileServerMapStyleURL.map { TileServerInfo(mapStyleUrl: $0) } } } diff --git a/ElementX/Sources/Other/MapLibre/MapURLs.swift b/ElementX/Sources/Other/MapLibre/MapURLs.swift index 9e5effe98..78ab86066 100644 --- a/ElementX/Sources/Other/MapLibre/MapURLs.swift +++ b/ElementX/Sources/Other/MapLibre/MapURLs.swift @@ -8,12 +8,17 @@ import CoreLocation -extension MapTilerConfiguration: MapTilerURLBuilderProtocol { +extension MapTilerSettings: MapTilerURLBuilderProtocol { /// For interactive MGLMap components func interactiveMapURL(for style: MapTilerStyle) -> URL? { - var url = styleURL(for: style) - url?.appendPathComponent("style.json", conformingTo: .json) - return url + switch self { + case .configuration(let configuration): + var url = configuration.styleURL(for: style) + url?.appendPathComponent("style.json", conformingTo: .json) + return url + case .url(let url): + return url + } } /// Used in the timeline where a full MGLMapView loading is unwanted @@ -22,21 +27,42 @@ extension MapTilerConfiguration: MapTilerURLBuilderProtocol { zoomLevel: Double, size: CGSize, attribution: MapTilerAttributionPlacement) -> URL? { - var url = styleURL(for: style) - url?.appendPathComponent(String(format: "static/%f,%f,%f/%dx%d@2x.png", - coordinates.longitude, - coordinates.latitude, - zoomLevel, - Int(size.width), - Int(size.height)), - conformingTo: .png) - url?.append(queryItems: [.init(name: "attribution", value: attribution.rawValue)]) - return url + let staticComponent = String(format: "static/%f,%f,%f/%dx%d@2x.png", + coordinates.longitude, + coordinates.latitude, + zoomLevel, + Int(size.width), + Int(size.height)) + switch self { + case .configuration(let configuration): + var url = configuration.styleURL(for: style) + url?.appendPathComponent(staticComponent, conformingTo: .png) + url?.append(queryItems: [.init(name: "attribution", value: attribution.rawValue)]) + return url + case .url(let url): + // The override is a full URL to a `style.json` (with any necessary query items, + // such as an embedded API key). Derive the static URL by replacing the trailing + // `style.json` component while preserving existing query items. + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil } + var pathComponents = components.path.split(separator: "/", omittingEmptySubsequences: false).map(String.init) + guard pathComponents.last == "style.json" else { return nil } + pathComponents.removeLast() + pathComponents.append(staticComponent) + components.path = pathComponents.joined(separator: "/") + + var queryItems = components.queryItems ?? [] + queryItems.append(.init(name: "attribution", value: attribution.rawValue)) + components.queryItems = queryItems + + return components.url + } } - - // MARK: Private - - private func styleURL(for style: MapTilerStyle) -> URL? { +} + +// MARK: - Private + +private extension MapTilerSettings.Configuration { + func styleURL(for style: MapTilerStyle) -> URL? { guard let apiKey else { return nil } var url: URL = baseURL @@ -45,7 +71,7 @@ extension MapTilerConfiguration: MapTilerURLBuilderProtocol { return url } - private func styleID(for style: MapTilerStyle) -> String { + func styleID(for style: MapTilerStyle) -> String { switch style { case .light: lightStyleID case .dark: darkStyleID diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift index 86bb0300a..ad05569eb 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift @@ -390,7 +390,7 @@ extension LocationSharingScreenViewModel { roomProxy.makeLiveLocationServiceReturnValue = liveLocationServiceMock return LocationSharingScreenViewModel(interactionMode: interactionMode, - mapURLBuilder: AppSettings.volatile().mapTilerConfiguration, + mapURLBuilder: AppSettings.volatile().mapTilerSettings.publisher.value, roomProxy: roomProxy, timelineController: MockTimelineController(), liveLocationManager: LiveLocationManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift index 5d1ee8ce2..018da435e 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift @@ -68,7 +68,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool audioPlayerState: .init(id: .recorderPreview, title: L10n.commonVoiceMessage, duration: 0), audioRecorderState: .init(), isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted, - isLocationSharingEnabled: appSettings.mapTilerConfiguration.isEnabled, + isLocationSharingEnabled: appSettings.mapTilerSettings.publisher.value.isEnabled, bindings: .init()), mediaProvider: mediaProvider) diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 2670ef668..473554085 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -146,7 +146,7 @@ struct TimelineViewState: BindableState { var linkMetadataProvider: LinkMetadataProviderProtocol? - var mapTilerConfiguration: MapTilerConfiguration + var mapTilerSettings: MapTilerSettings var stoppedLiveLocationIDs: Set = [] diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 29757f654..b80311954 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -107,7 +107,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs, emojiProvider: emojiProvider, linkMetadataProvider: hideTimelineMedia ? nil : linkMetadataProvider, - mapTilerConfiguration: appSettings.mapTilerConfiguration, + mapTilerSettings: appSettings.mapTilerSettings.publisher.value, bindings: .init(reactionsCollapsed: [:])), mediaProvider: userSession.mediaProvider) diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LiveLocationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LiveLocationRoomTimelineView.swift index f7c2705da..bbf8b4c8b 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LiveLocationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LiveLocationRoomTimelineView.swift @@ -66,7 +66,7 @@ struct LiveLocationRoomTimelineView: View { } } .onTapGesture { - guard context.viewState.mapTilerConfiguration.isEnabled, + guard context.viewState.mapTilerSettings.isEnabled, timelineItem.content.lastGeoURI != nil, isLive else { return @@ -95,7 +95,7 @@ struct LiveLocationRoomTimelineView: View { private var liveContent: some View { if let geoURI = timelineItem.content.lastGeoURI { MapLibreStaticMapView(geoURI: geoURI, - mapURLBuilder: context.viewState.mapTilerConfiguration, + mapURLBuilder: context.viewState.mapTilerSettings, attributionPlacement: .topLeft, mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) { LocationMarkerView(kind: .liveUser(.init(sender: timelineItem.sender)), diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift index 34af44082..e0701e528 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift @@ -18,7 +18,7 @@ struct LocationRoomTimelineView: View { .accessibilityElement(children: .ignore) .accessibilityLabel(L10n.commonSharedLocation) .onTapGesture { - guard context.viewState.mapTilerConfiguration.isEnabled else { return } + guard context.viewState.mapTilerSettings.isEnabled else { return } context.send(viewAction: .mediaTapped(itemID: timelineItem.id)) } } @@ -28,7 +28,7 @@ struct LocationRoomTimelineView: View { private var mainContent: some View { if let geoURI = timelineItem.content.geoURI { MapLibreStaticMapView(geoURI: geoURI, - mapURLBuilder: context.viewState.mapTilerConfiguration, + mapURLBuilder: context.viewState.mapTilerSettings, mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) { LocationMarkerView(kind: timelineItem.content.kind == .sender ? .staticUser(.init(sender: timelineItem.sender)) : .pin, mediaProvider: context.mediaProvider) diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index 06d8dbb80..c39a780ce 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -252,6 +252,7 @@ class AuthenticationService: AuthenticationServiceProtocol { appSettings: appSettings, appHooks: appHooks) try await appHooks.remoteSettingsHook.initializeCache(using: client, applyingTo: appSettings).get() + await client.updateMapTilerSettings(in: appSettings) return client } diff --git a/ElementX/Sources/Services/Client/Client.swift b/ElementX/Sources/Services/Client/Client.swift index c7f3bcd48..aa2e0580a 100644 --- a/ElementX/Sources/Services/Client/Client.swift +++ b/ElementX/Sources/Services/Client/Client.swift @@ -29,4 +29,18 @@ extension ClientProtocol { return .failure(.sdkError(error)) } } + + /// Reads the homeserver-advertised map tile server (`tile_server.map_style_url` + /// from the matrix client well-known) and applies it to ``AppSettings``. + /// + /// Clears any previously applied URL when none is advertised (or the well-known + /// is unavailable), so each session start reflects the latest server-side + /// configuration. + func updateMapTilerSettings(in appSettings: AppSettings) async { + if let url = await tileServer().flatMap({ URL(string: $0.mapStyleUrl) }) { + appSettings.mapTilerSettings.applyRemoteValue(.url(url)) + } else { + appSettings.mapTilerSettings.reset() + } + } } diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index 2b53332e3..ffa139886 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -150,6 +150,7 @@ class UserSessionStore: UserSessionStoreProtocol { MXLog.info("Set up session for user \(credentials.userID) at: \(credentials.restorationToken.sessionDirectories)") Task(priority: .low) { await appHooks.remoteSettingsHook.updateCache(using: client) } + Task(priority: .low) { await client.updateMapTilerSettings(in: appSettings) } return try await .success(setupProxyForClient(client)) } catch UserSessionStoreError.failedSettingUpClientProxy(let error) { diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index bacc2cab1..92965a023 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -169,7 +169,7 @@ class MockScreen: Identifiable { accountProvisioningHost: appSettings.accountProvisioningHost, bugReportApplicationID: appSettings.bugReportApplicationID, analyticsTermsURL: appSettings.analyticsTermsURL, - mapTilerConfiguration: appSettings.mapTilerConfiguration) + mapTilerConfiguration: AppSettings.bundledMapTilerConfiguration) } let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: AuthenticationService.mock, diff --git a/Enterprise b/Enterprise index fb9a31514..6cce65068 160000 --- a/Enterprise +++ b/Enterprise @@ -1 +1 @@ -Subproject commit fb9a31514968c444c87ab4801f9aa69d25f1d269 +Subproject commit 6cce650681cc5524dda361808ba7eb3935eddad8 diff --git a/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift b/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift index 4f09f5091..b03c89eeb 100644 --- a/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift +++ b/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift @@ -350,7 +350,7 @@ final class AuthenticationStartScreenViewModelTests { accountProvisioningHost: appSettings.accountProvisioningHost, bugReportApplicationID: appSettings.bugReportApplicationID, analyticsTermsURL: appSettings.analyticsTermsURL, - mapTilerConfiguration: appSettings.mapTilerConfiguration) + mapTilerConfiguration: AppSettings.bundledMapTilerConfiguration) } } diff --git a/UnitTests/Sources/LocationSharingScreenViewModelTests.swift b/UnitTests/Sources/LocationSharingScreenViewModelTests.swift index cf1325dbc..dbf710878 100644 --- a/UnitTests/Sources/LocationSharingScreenViewModelTests.swift +++ b/UnitTests/Sources/LocationSharingScreenViewModelTests.swift @@ -397,7 +397,7 @@ struct LocationSharingScreenViewModelTests { let appSettings = AppSettings.volatile() viewModel = LocationSharingScreenViewModel(interactionMode: .viewLive(sender: nil, initialLiveLocationShare: nil), - mapURLBuilder: appSettings.mapTilerConfiguration, + mapURLBuilder: appSettings.mapTilerSettings.publisher.value, roomProxy: roomProxyMock, timelineController: MockTimelineController(timelineProxy: TimelineProxyMock(.init())), liveLocationManager: LiveLocationManagerMock(.init()), @@ -432,7 +432,7 @@ struct LocationSharingScreenViewModelTests { let appSettings = AppSettings.volatile() timelineProxy = TimelineProxyMock(.init()) viewModel = LocationSharingScreenViewModel(interactionMode: .picker(shouldShowLiveLocationOption: true), - mapURLBuilder: appSettings.mapTilerConfiguration, + mapURLBuilder: appSettings.mapTilerSettings.publisher.value, roomProxy: JoinedRoomProxyMock(.init(members: members)), timelineController: MockTimelineController(timelineProxy: timelineProxy), liveLocationManager: LiveLocationManagerMock(liveLocationManagerConfiguration), @@ -447,7 +447,7 @@ struct LocationSharingScreenViewModelTests { let appSettings = AppSettings.volatile() timelineProxy = TimelineProxyMock(.init()) viewModel = LocationSharingScreenViewModel(interactionMode: .picker(shouldShowLiveLocationOption: true), - mapURLBuilder: appSettings.mapTilerConfiguration, + mapURLBuilder: appSettings.mapTilerSettings.publisher.value, roomProxy: JoinedRoomProxyMock(.init(members: members)), timelineController: MockTimelineController(timelineProxy: timelineProxy), liveLocationManager: liveLocationManagerMock, @@ -469,7 +469,7 @@ struct LocationSharingScreenViewModelTests { roomProxyMock.makeLiveLocationServiceReturnValue = liveLocationServiceMock viewModel = LocationSharingScreenViewModel(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialShare), - mapURLBuilder: appSettings.mapTilerConfiguration, + mapURLBuilder: appSettings.mapTilerSettings.publisher.value, roomProxy: roomProxyMock, timelineController: MockTimelineController(timelineProxy: TimelineProxyMock(.init())), liveLocationManager: LiveLocationManagerMock(.init()), diff --git a/UnitTests/Sources/MapTilerURLBuilderTests.swift b/UnitTests/Sources/MapTilerURLBuilderTests.swift index 06a0f29c0..94dc0f975 100644 --- a/UnitTests/Sources/MapTilerURLBuilderTests.swift +++ b/UnitTests/Sources/MapTilerURLBuilderTests.swift @@ -19,10 +19,10 @@ struct MapTilerURLBuilderTests { var builder: MapTilerURLBuilderProtocol init() { - builder = MapTilerConfiguration(baseURL: Self.baseURL, - apiKey: Self.apiKey, - lightStyleID: Self.lightStyleID, - darkStyleID: Self.darkStyleID) + builder = MapTilerSettings.configuration(.init(baseURL: Self.baseURL, + apiKey: Self.apiKey, + lightStyleID: Self.lightStyleID, + darkStyleID: Self.darkStyleID)) } @Test @@ -58,10 +58,10 @@ struct MapTilerURLBuilderTests { @Test mutating func nilAPIKey() { - let configuration = MapTilerConfiguration(baseURL: Self.baseURL, - apiKey: nil, - lightStyleID: Self.lightStyleID, - darkStyleID: Self.darkStyleID) + let configuration = MapTilerSettings.configuration(.init(baseURL: Self.baseURL, + apiKey: nil, + lightStyleID: Self.lightStyleID, + darkStyleID: Self.darkStyleID)) #expect(!configuration.isEnabled) builder = configuration @@ -76,4 +76,66 @@ struct MapTilerURLBuilderTests { let dynamicMapURL = builder.interactiveMapURL(for: .light) #expect(dynamicMapURL == nil) } + + // MARK: - Override URL + + @Test + mutating func overrideInteractiveMap() { + let overrideURL: URL = "https://maps.example.com/maps/streets-v2/style.json?key=server_key" + let configuration = MapTilerSettings.url(overrideURL) + #expect(configuration.isEnabled) + + builder = configuration + + // The override is returned as-is for the interactive map, ignoring light/dark. + #expect(builder.interactiveMapURL(for: .light) == overrideURL) + #expect(builder.interactiveMapURL(for: .dark) == overrideURL) + } + + @Test + mutating func overrideStaticMapPreservesQueryItems() { + // The override includes an embedded API key as a query item — it must be preserved + // when deriving the static map URL. + let overrideURL: URL = "https://maps.example.com/maps/streets-v2/style.json?key=server_key" + builder = MapTilerSettings.url(overrideURL) + + let url = builder.staticMapTileImageURL(for: .light, + coordinates: .init(latitude: 1, longitude: 2), + zoomLevel: 5, + size: .init(width: 300, height: 200), + attribution: .topLeft) + + let expectedURL: URL = "https://maps.example.com/maps/streets-v2/static/2.000000,1.000000,5.000000/300x200@2x.png?key=server_key&attribution=topleft" + #expect(url == expectedURL) + } + + @Test + mutating func overrideStaticMapWithoutQueryItems() { + // An override without a key (e.g. self-hosted server proxying the key) still works. + let overrideURL: URL = "https://maps.example.com/maps/streets-v2/style.json" + builder = MapTilerSettings.url(overrideURL) + + let url = builder.staticMapTileImageURL(for: .dark, + coordinates: .init(latitude: 1, longitude: 2), + zoomLevel: 5, + size: .init(width: 300, height: 200), + attribution: .hidden) + + let expectedURL: URL = "https://maps.example.com/maps/streets-v2/static/2.000000,1.000000,5.000000/300x200@2x.png?attribution=false" + #expect(url == expectedURL) + } + + @Test + mutating func overrideMissingStyleJSONFails() { + // If the override URL doesn't end in /style.json we can't derive a static URL. + let overrideURL: URL = "https://maps.example.com/maps/streets-v2/" + builder = MapTilerSettings.url(overrideURL) + + let url = builder.staticMapTileImageURL(for: .light, + coordinates: .init(latitude: 1, longitude: 2), + zoomLevel: 5, + size: .init(width: 300, height: 200), + attribution: .topLeft) + #expect(url == nil) + } } diff --git a/UnitTests/Sources/MatrixClientWellKnownTests.swift b/UnitTests/Sources/MatrixClientWellKnownTests.swift new file mode 100644 index 000000000..7fc9eeb01 --- /dev/null +++ b/UnitTests/Sources/MatrixClientWellKnownTests.swift @@ -0,0 +1,75 @@ +// +// Copyright 2025 Element Creations Ltd. +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +@testable import ElementX +import Foundation +import MatrixRustSDKMocks +import Testing + +/// Exercises ``ClientProtocol.updateMapTilerSettings(in:)``. The SDK is now the +/// source of truth for parsing the matrix client well-known and exposing the +/// `tile_server` field as a typed ``TileServerInfo``; these tests just check +/// that we forward it into ``AppSettings`` correctly. +struct MatrixClientWellKnownTests { + private static let mapStyleURLString = "https://maps.example.com/maps/streets-v2/style.json?key=server" + + @Test + func mapStyleURLIsApplied() async throws { + let appSettings = AppSettings.volatile() + let client = ClientSDKMock(.init(tileServerMapStyleURL: Self.mapStyleURLString)) + + await client.updateMapTilerSettings(in: appSettings) + + #expect(appSettings.mapTilerSettings.isRemotelyConfigured) + let expectedURL = try #require(URL(string: Self.mapStyleURLString)) + #expect(appSettings.mapTilerSettings.publisher.value == .url(expectedURL)) + } + + @Test + func missingTileServerClearsExistingOverride() async throws { + let appSettings = AppSettings.volatile() + let expectedURL = try #require(URL(string: Self.mapStyleURLString)) + appSettings.mapTilerSettings.applyRemoteValue(.url(expectedURL)) + #expect(appSettings.mapTilerSettings.isRemotelyConfigured) + + // The SDK returns nil when the homeserver hasn't advertised a tile server. + let client = ClientSDKMock(.init()) + await client.updateMapTilerSettings(in: appSettings) + + #expect(!appSettings.mapTilerSettings.isRemotelyConfigured) + } + + @Test + func invalidMapStyleURLClearsOverride() async throws { + let appSettings = AppSettings.volatile() + let expectedURL = try #require(URL(string: Self.mapStyleURLString)) + appSettings.mapTilerSettings.applyRemoteValue(.url(expectedURL)) + + // The SDK returns the string verbatim — if it isn't a valid URL we just + // drop any previous override rather than crash. + let client = ClientSDKMock(.init(tileServerMapStyleURL: "")) + await client.updateMapTilerSettings(in: appSettings) + + #expect(!appSettings.mapTilerSettings.isRemotelyConfigured) + } + + @Test + func mapTilerSettingsResolveToOverride() async { + let appSettings = AppSettings.volatile() + let client = ClientSDKMock(.init(tileServerMapStyleURL: Self.mapStyleURLString)) + + await client.updateMapTilerSettings(in: appSettings) + + switch appSettings.mapTilerSettings.publisher.value { + case .url(let url): + #expect(url.absoluteString == Self.mapStyleURLString) + case .configuration: + Issue.record("Expected the resolved settings to be .url") + } + } +} diff --git a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift index 8b629e688..dbb76c609 100644 --- a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift +++ b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift @@ -339,7 +339,7 @@ final class ServerConfirmationScreenViewModelTests { accountProvisioningHost: appSettings.accountProvisioningHost, bugReportApplicationID: appSettings.bugReportApplicationID, analyticsTermsURL: appSettings.analyticsTermsURL, - mapTilerConfiguration: appSettings.mapTilerConfiguration) + mapTilerConfiguration: AppSettings.bundledMapTilerConfiguration) mode = .picker(appSettings.accountProviders) }