mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Set map tiler URL from well known if available (#5633)
This commit is contained in:
@@ -834,6 +834,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
tearDownUserSession()
|
||||
|
||||
appSettings.resetSessionSpecificSettings()
|
||||
appSettings.mapTilerSettings.reset()
|
||||
appHooks.remoteSettingsHook.reset(appSettings)
|
||||
|
||||
// Reset analytics
|
||||
|
||||
@@ -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<MapTilerSettings>(.configuration(AppSettings.bundledMapTilerConfiguration))
|
||||
|
||||
// MARK: - Presence
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ struct TimelineViewState: BindableState {
|
||||
|
||||
var linkMetadataProvider: LinkMetadataProviderProtocol?
|
||||
|
||||
var mapTilerConfiguration: MapTilerConfiguration
|
||||
var mapTilerSettings: MapTilerSettings
|
||||
|
||||
var stoppedLiveLocationIDs: Set<TimelineItemIdentifier> = []
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+2
-2
@@ -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)),
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user