remove map tiler url from matrix wellknown

This commit is contained in:
Mauro Romito
2026-08-04 13:42:51 +02:00
committed by Mauro
parent 80530f0006
commit 748ec818ac
22 changed files with 120 additions and 280 deletions
@@ -849,7 +849,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
tearDownUserSession()
appSettings.resetSessionSpecificSettings()
appSettings.mapTilerSettings.reset()
appHooks.remoteSettingsHook.reset(appSettings)
// Reset analytics
@@ -25,6 +25,7 @@ nonisolated protocol CommonSettingsProtocol: AnyObject, Sendable {
var bugReportRageshakeURL: RemotePreference<RageshakeConfiguration> { get }
var contentScannerURL: RemotePreference<URL?> { get }
var forceDisableE2EE: RemotePreference<Bool> { get }
var mapTilerConfiguration: RemotePreference<MapTilerConfiguration> { get }
var enableOnlySignedDeviceIsolationMode: Bool { get }
var threadsEnabled: Bool { get }
@@ -95,7 +96,7 @@ final nonisolated class AppSettings: @unchecked Sendable {
accountProvisioningHost: String,
bugReportApplicationID: String,
analyticsTermsURL: URL?,
mapTilerConfiguration: MapTilerSettings.Configuration) {
mapTilerConfiguration: MapTilerConfiguration) {
self.accountProviders = accountProviders
self.allowOtherAccountProviders = allowOtherAccountProviders
self.hideBrandChrome = hideBrandChrome
@@ -116,7 +117,7 @@ final nonisolated class AppSettings: @unchecked Sendable {
self.accountProvisioningHost = accountProvisioningHost
self.bugReportApplicationID = bugReportApplicationID
self.analyticsTermsURL = analyticsTermsURL
mapTilerSettings = RemotePreference(.configuration(mapTilerConfiguration))
self.mapTilerConfiguration = RemotePreference(mapTilerConfiguration)
}
// MARK: - Application
@@ -376,15 +377,13 @@ final nonisolated class AppSettings: @unchecked Sendable {
// MARK: - Maps
/// 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")
static let bundledMapTilerConfiguration = MapTilerConfiguration(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<MapTilerSettings>(.configuration(AppSettings.bundledMapTilerConfiguration))
/// The MapTiler configuration used to build map URLs, which defaults to the bundled one.
private(set) var mapTilerConfiguration = RemotePreference(AppSettings.bundledMapTilerConfiguration)
// MARK: - Presence
@@ -0,0 +1,38 @@
//
// 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)
nonisolated struct MapTilerConfiguration: Equatable, Codable {
let baseURL: URL
/// The API key for fetching map tiles. When empty, the server at ``baseURL`` is expected to
/// provide the key itself, so none is sent with the requests.
///
/// 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
enum CodingKeys: String, CodingKey {
case baseURL = "base_url"
case apiKey = "api_key"
case lightStyleID = "light_style_id"
case darkStyleID = "dark_style_id"
}
var isEnabled: Bool {
apiKey != nil
}
}
@@ -1,40 +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
/// 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
nonisolated 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
}
}
}
@@ -107,7 +107,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
let stackCoordinator = NavigationStackCoordinator()
let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode,
mapURLBuilder: flowParameters.appSettings.mapTilerSettings.publisher.value,
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration.publisher.value,
roomProxy: roomProxy,
timelineController: timelineController,
liveLocationManager: flowParameters.userSession.liveLocationManager,
@@ -1175,7 +1175,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let stackCoordinator = NavigationStackCoordinator()
let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode,
mapURLBuilder: flowParameters.appSettings.mapTilerSettings.publisher.value,
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration.publisher.value,
roomProxy: roomProxy,
timelineController: timelineController,
liveLocationManager: flowParameters.userSession.liveLocationManager,
@@ -21,7 +21,6 @@ nonisolated extension ClientSDKMock {
var supportsOAuthCreatePrompt = true
var supportsPasswordLogin = true
var elementWellKnown: String?
var tileServerMapStyleURL: String?
var validCredentials = (username: "alice", password: "12345678")
// MARK: Session
@@ -69,7 +68,6 @@ nonisolated extension ClientSDKMock {
throw MockError.generic
}
}
tileServerReturnValue = configuration.tileServerMapStyleURL.map { TileServerInfo(mapStyleUrl: $0) }
}
}
+18 -40
View File
@@ -8,17 +8,12 @@
import CoreLocation
extension MapTilerSettings: MapTilerURLBuilderProtocol {
extension MapTilerConfiguration: MapTilerURLBuilderProtocol {
/// For interactive MGLMap components
func interactiveMapURL(for style: MapTilerStyle) -> 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
}
var url = styleURL(for: style)
url?.appendPathComponent("style.json", conformingTo: .json)
return url
}
/// Used in the timeline where a full MGLMapView loading is unwanted
@@ -27,47 +22,30 @@ extension MapTilerSettings: MapTilerURLBuilderProtocol {
zoomLevel: Double,
size: CGSize,
attribution: MapTilerAttributionPlacement) -> 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
}
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
}
}
// MARK: - Private
private extension MapTilerSettings.Configuration {
private extension MapTilerConfiguration {
func styleURL(for style: MapTilerStyle) -> URL? {
guard let apiKey else { return nil }
var url: URL = baseURL
url.appendPathComponent(styleID(for: style), conformingTo: .item)
url.append(queryItems: [URLQueryItem(name: "key", value: apiKey)])
if !apiKey.isEmpty {
url.append(queryItems: [URLQueryItem(name: "key", value: apiKey)])
}
return url
}
@@ -435,7 +435,7 @@ extension LocationSharingScreenViewModel {
let appSettings = AppSettings.volatile()
return LocationSharingScreenViewModel(interactionMode: interactionMode,
mapURLBuilder: appSettings.mapTilerSettings.publisher.value,
mapURLBuilder: appSettings.mapTilerConfiguration.publisher.value,
roomProxy: roomProxy,
timelineController: TimelineControllerMock(.init()),
liveLocationManager: LiveLocationManagerMock(),
@@ -65,7 +65,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
super.init(initialViewState: ComposerToolbarViewState(wysiwygViewModel: wysiwygViewModel,
isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted,
isLocationSharingEnabled: appSettings.mapTilerSettings.publisher.value.isEnabled,
isLocationSharingEnabled: appSettings.mapTilerConfiguration.publisher.value.isEnabled,
bindings: .init()),
mediaProvider: mediaProvider)
@@ -151,7 +151,7 @@ struct TimelineViewState: BindableState {
var linkMetadataProvider: LinkMetadataProviderProtocol?
var mapTilerSettings: MapTilerSettings
var mapTilerConfiguration: MapTilerConfiguration
var stoppedLiveLocationIDs: Set<TimelineItemIdentifier> = []
@@ -106,7 +106,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs,
emojiProvider: emojiProvider,
linkMetadataProvider: hideTimelineMedia ? nil : linkMetadataProvider,
mapTilerSettings: appSettings.mapTilerSettings.publisher.value,
mapTilerConfiguration: appSettings.mapTilerConfiguration.publisher.value,
bindings: .init(reactionsCollapsed: [:])),
mediaProvider: userSession.mediaProvider,
contentScannerService: userSession.contentScannerService)
@@ -64,7 +64,7 @@ struct LiveLocationRoomTimelineView: View {
}
}
.onTapGesture {
guard context.viewState.mapTilerSettings.isEnabled,
guard context.viewState.mapTilerConfiguration.isEnabled,
timelineItem.content.lastGeoURI != nil,
isLive else {
return
@@ -93,7 +93,7 @@ struct LiveLocationRoomTimelineView: View {
private var liveContent: some View {
if let geoURI = timelineItem.content.lastGeoURI {
MapLibreStaticMapView(geoURI: geoURI,
mapURLBuilder: context.viewState.mapTilerSettings,
mapURLBuilder: context.viewState.mapTilerConfiguration,
attributionPlacement: .topLeft,
mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
LocationMarkerView(kind: .liveUser(.init(sender: timelineItem.sender)),
@@ -18,7 +18,7 @@ struct LocationRoomTimelineView: View {
.accessibilityElement(children: .ignore)
.accessibilityLabel(L10n.commonSharedLocation)
.onTapGesture {
guard context.viewState.mapTilerSettings.isEnabled else { return }
guard context.viewState.mapTilerConfiguration.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.mapTilerSettings,
mapURLBuilder: context.viewState.mapTilerConfiguration,
mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
LocationMarkerView(kind: timelineItem.content.kind == .sender ? .staticUser(.init(sender: timelineItem.sender)) : .pin,
mediaProvider: context.mediaProvider)
@@ -259,7 +259,6 @@ class AuthenticationService: AuthenticationServiceProtocol {
appSettings: appSettings,
appHooks: appHooks)
try await appHooks.remoteSettingsHook.initializeCache(using: client, applyingTo: appSettings).get()
await client.updateMapTilerSettings(in: appSettings)
return client
}
@@ -29,18 +29,4 @@ nonisolated 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()
}
}
}
@@ -152,7 +152,6 @@ 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) {