mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
fb49f5b83b
Add the element-call-ios dependency and a developer option for native calls The package carries the call UI and the matrix-rust-rtc media stack, pinned by exact version the way the SDK is, with the local path kept commented out for working against a checkout. Its media xcframework has no Intel simulator slice and a package cannot say so for the projects that consume it, hence the excluded architecture at the project level. The developer option is off by default and nothing reads it yet. It is placed next to the Element Call URL override, which only applies to the web view that native calls replace.
279 lines
10 KiB
Swift
279 lines
10 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-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 SwiftUI
|
|
|
|
struct DeveloperOptionsScreen: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var showMarkAllRoomsAsReadAlert = false
|
|
|
|
@Bindable var context: DeveloperOptionsScreenViewModel.Context
|
|
|
|
@State private var showConfetti = false
|
|
@State private var elementCallURLOverrideString: String
|
|
|
|
init(context: DeveloperOptionsScreenViewModel.Context) {
|
|
self.context = context
|
|
elementCallURLOverrideString = context.elementCallBaseURLOverride?.absoluteString ?? ""
|
|
}
|
|
|
|
var body: some View {
|
|
Form {
|
|
if let storeSizes = context.viewState.storeSizes {
|
|
Section("Usage") {
|
|
ForEach(storeSizes) { storeSize in
|
|
LabeledContent(storeSize.name, value: storeSize.size)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("Logging") {
|
|
LogLevelConfigurationView(logLevel: $context.logLevel)
|
|
|
|
DisclosureGroup("SDK trace packs") {
|
|
ForEach(TraceLogPack.allCases, id: \.self) { pack in
|
|
Toggle(isOn: $context.traceLogPacks[pack]) {
|
|
Text(pack.title)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("General") {
|
|
Toggle(isOn: $context.linkNewDeviceEnabled) {
|
|
Text("Link new device with QR code")
|
|
}
|
|
|
|
Toggle(isOn: $context.globalSearchEnabled) {
|
|
Text("Global search")
|
|
Text("Moves search to a separate tab")
|
|
}
|
|
|
|
context.viewState.appHooks
|
|
.developerOptionsScreenHook
|
|
.generalSectionRows(isSignedIn: context.viewState.isSignedIn)
|
|
}
|
|
|
|
Section("Room List") {
|
|
Picker("Room list activity visibility", selection: $context.roomListActivityVisibility) {
|
|
ForEach(RoomListActivityVisibility.allCases, id: \.self) { visibility in
|
|
Text(visibility.rawValue.capitalized)
|
|
.tag(visibility)
|
|
}
|
|
}
|
|
|
|
Toggle(isOn: $context.roomListNotificationCountEnabled) {
|
|
Text("Show unread notification count")
|
|
Text("Also makes the app icon badge use the SDK's own unread notification count")
|
|
}
|
|
|
|
Toggle(isOn: $context.fuzzyRoomListSearchEnabled) {
|
|
Text("Fuzzy searching")
|
|
}
|
|
|
|
Toggle(isOn: $context.lowPriorityFilterEnabled) {
|
|
Text("Low priority filter")
|
|
}
|
|
|
|
Toggle(isOn: $context.mentionsFilterEnabled) {
|
|
Text("Mentions filter")
|
|
}
|
|
|
|
Toggle(isOn: $context.automaticBackPaginationEnabled) {
|
|
Text("Automatic back pagination")
|
|
Text("Requires app reboot")
|
|
}
|
|
}
|
|
|
|
Section("Room") {
|
|
Toggle(isOn: $context.linkPreviewsEnabled) {
|
|
Text("Link previews")
|
|
Text("Follows the timeline media visibility settings.")
|
|
Text("Can leak the device IP address when loading link metadata.")
|
|
.foregroundStyle(.compound.textCriticalPrimary)
|
|
}
|
|
|
|
Toggle(isOn: $context.jumpToReadMarkerEnabled) {
|
|
Text("Jump to unread")
|
|
Text("Adds a button to jump to the read marker, plus a presence dot on the scroll-to-bottom button when new messages arrive while scrolled away.")
|
|
}
|
|
|
|
Toggle(isOn: $context.messageMultiSelectEnabled) {
|
|
Text("Multi-select messages")
|
|
Text("Adds a Select action to the message menu to pick several messages at once.")
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Toggle(isOn: $context.enableOnlySignedDeviceIsolationMode) {
|
|
Text("Exclude insecure devices when sending/receiving messages")
|
|
Text("Requires app reboot")
|
|
}
|
|
} header: {
|
|
Text("Trust and Decoration")
|
|
} footer: {
|
|
Text("This setting controls how end-to-end encryption (E2EE) keys are exchanged. Enabling it will prevent the inclusion of devices that have not been explicitly verified by their owners.")
|
|
}
|
|
|
|
Section {
|
|
TextField("Leave empty to use EC locally", text: $elementCallURLOverrideString)
|
|
.autocorrectionDisabled(true)
|
|
.autocapitalization(.none)
|
|
.foregroundColor(URL(string: elementCallURLOverrideString) == nil ? .red : .primary)
|
|
.submitLabel(.done)
|
|
.onSubmit {
|
|
if elementCallURLOverrideString.isEmpty {
|
|
context.elementCallBaseURLOverride = nil
|
|
} else if let url = URL(string: elementCallURLOverrideString) {
|
|
context.elementCallBaseURLOverride = url
|
|
}
|
|
}
|
|
Toggle(isOn: $context.nativeCallEnabled) {
|
|
Text("Experimental native calls")
|
|
}
|
|
} header: {
|
|
Text("Calls")
|
|
} footer: {
|
|
Text("The URL override only applies to calls run in the Element Call web view, which native calls replace.")
|
|
}
|
|
|
|
Section("Notifications") {
|
|
Toggle(isOn: $context.hideQuietNotificationAlerts) {
|
|
Text("Hide quiet alerts")
|
|
Text("The badge count will still be updated")
|
|
}
|
|
|
|
Toggle(isOn: $context.focusEventOnNotificationTap) {
|
|
Text("Focus event on notification tap")
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Button {
|
|
showMarkAllRoomsAsReadAlert = true
|
|
} label: {
|
|
Text("Mark all rooms as read")
|
|
}.alert("Are you sure you want to mark all the rooms as read?", isPresented: $showMarkAllRoomsAsReadAlert) {
|
|
Button("Cancel", role: .cancel) { }
|
|
|
|
Button("Yes") {
|
|
context.send(viewAction: .markAllRoomsAsRead)
|
|
}
|
|
}
|
|
} footer: {
|
|
Text("""
|
|
This will send a private read receipt and a read marker in every room you are part of. \
|
|
It's a long running operation that might get rate limited. \
|
|
It will run in the background but the app must be alive for it to finish.
|
|
""")
|
|
}
|
|
|
|
Section {
|
|
Button {
|
|
showConfetti = true
|
|
} label: {
|
|
Text("🥳")
|
|
.frame(maxWidth: .infinity)
|
|
.alignmentGuide(.listRowSeparatorLeading) { _ in 0 } // Fix separator alignment
|
|
}
|
|
}
|
|
|
|
if context.viewState.shouldShowClearCache {
|
|
Section {
|
|
Button(role: .destructive) {
|
|
context.send(viewAction: .clearCache)
|
|
} label: {
|
|
Text("Clear cache")
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.overlay(effectsView)
|
|
.navigationTitle(L10n.commonDeveloperOptions)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar { toolbar }
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var effectsView: some View {
|
|
if showConfetti {
|
|
EffectsView(effect: .confetti)
|
|
.ignoresSafeArea()
|
|
.allowsHitTesting(false)
|
|
.task { await removeConfettiAfterDelay() }
|
|
}
|
|
}
|
|
|
|
private func removeConfettiAfterDelay() async {
|
|
try? await Task.sleep(for: .seconds(4))
|
|
showConfetti = false
|
|
}
|
|
|
|
@ToolbarContentBuilder
|
|
private var toolbar: some ToolbarContent {
|
|
if !context.viewState.isSignedIn {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
if #available(iOS 26.0, *) {
|
|
Button(role: .close, action: dismiss.callAsFunction)
|
|
} else {
|
|
Button(L10n.actionDone, action: dismiss.callAsFunction)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct LogLevelConfigurationView: View {
|
|
@Binding var logLevel: LogLevel
|
|
|
|
var body: some View {
|
|
Picker(selection: $logLevel) {
|
|
ForEach(logLevels, id: \.self) { logLevel in
|
|
Text(logLevel.title)
|
|
}
|
|
} label: {
|
|
Text("Log level")
|
|
Text("Requires app reboot")
|
|
}
|
|
}
|
|
|
|
/// Allows the picker to work with associated values
|
|
private var logLevels: [LogLevel] {
|
|
[.error, .warn, .info, .debug, .trace]
|
|
}
|
|
}
|
|
|
|
private extension Set<TraceLogPack> {
|
|
/// A custom subscript that allows binding a toggle to add/remove a pack from the array.
|
|
subscript(pack: TraceLogPack) -> Bool {
|
|
get { contains(pack) }
|
|
set {
|
|
if newValue {
|
|
insert(pack)
|
|
} else {
|
|
remove(pack)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
struct DeveloperOptionsScreen_Previews: PreviewProvider {
|
|
static let viewModel = DeveloperOptionsScreenViewModel(developerOptions: AppSettings.volatile(),
|
|
appHooks: AppHooks(),
|
|
clientProxy: ClientProxyMock(.init()))
|
|
|
|
static var previews: some View {
|
|
ElementNavigationStack {
|
|
DeveloperOptionsScreen(context: viewModel.context)
|
|
}
|
|
}
|
|
}
|