From 3bfbc8d0a48e4d51ab1764801c2065e9e41084c1 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 3 Mar 2022 17:29:41 +0000 Subject: [PATCH 001/135] Add UISIDetector --- .../UISIAutoReporter/UISIDetector.swift | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 Riot/Managers/UISIAutoReporter/UISIDetector.swift diff --git a/Riot/Managers/UISIAutoReporter/UISIDetector.swift b/Riot/Managers/UISIAutoReporter/UISIDetector.swift new file mode 100644 index 000000000..e1865d080 --- /dev/null +++ b/Riot/Managers/UISIAutoReporter/UISIDetector.swift @@ -0,0 +1,148 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import MatrixSDK +import Foundation + +protocol UISIDetectorDelegate: AnyObject { + var reciprocateToDeviceEventType: String { get } + func uisiDetected(source: E2EMessageDetected) + func uisiReciprocateRequest(source: MXEvent) +} + +enum UISIEventSource { + case initialSync + case incrementalSync + case pagination +} + +struct E2EMessageDetected { + let eventId: String + let roomId: String + let senderUserId: String + let senderDeviceId: String + let senderKey: String + let sessionId: String + let source: UISIEventSource + + static func fromEvent(event: MXEvent, roomId: String, source: UISIEventSource) -> E2EMessageDetected { + return E2EMessageDetected( + eventId: event.eventId ?? "", + roomId: roomId, + senderUserId: event.sender, + senderDeviceId: event.content["device_id"] as? String ?? "", + senderKey: event.content["sender_key"] as? String ?? "", + sessionId: event.content["session_id"] as? String ?? "", + source: source + ) + } +} + +extension E2EMessageDetected: Hashable { + func hash(into hasher: inout Hasher) { + hasher.combine(eventId) + hasher.combine(roomId) + } +} + + +class UISIDetector: MXLiveEventListener { + + weak var delegate: UISIDetectorDelegate? + var enabled = false + + private var trackedEvents = [String: (E2EMessageDetected, DispatchSourceTimer)]() + private let dispatchQueue = DispatchQueue(label: "io.element.UISIDetector.queue") + private static let timeoutSeconds = 30 + + + func onLiveEvent(roomId: String, event: MXEvent) { + guard enabled, !event.isEncrypted else { return } + dispatchQueue.async { + self.handleEventReceived(detectorEvent: E2EMessageDetected.fromEvent(event: event, roomId: roomId, source: .incrementalSync)) + } + } + + func onPaginatedEvent(roomId: String, event: MXEvent) { + guard enabled else { return } + dispatchQueue.async { + self.handleEventReceived(detectorEvent: E2EMessageDetected.fromEvent(event: event, roomId: roomId, source: .pagination)) + } + } + + func onEventDecrypted(eventId: String, roomId: String, clearEvent: [AnyHashable: Any]) { + guard enabled else { return } + dispatchQueue.async { + self.unTrack(eventId: eventId, roomId: roomId) + } + } + + func onEventDecryptionError(eventId: String, roomId: String, error: Error) { + guard enabled else { return } + dispatchQueue.async { + if let event = self.unTrack(eventId: eventId, roomId: roomId) { + self.triggerUISI(source: event) + } + } + } + + func onLiveToDeviceEvent(event: MXEvent) { + guard enabled, event.type == delegate?.reciprocateToDeviceEventType else { return } + delegate?.uisiReciprocateRequest(source: event) + } + + private func handleEventReceived(detectorEvent: E2EMessageDetected) { + guard enabled else { return } + let trackedId = Self.trackedEventId(roomId: detectorEvent.roomId, eventId: detectorEvent.eventId) + guard trackedEvents[trackedId] == nil else { + MXLog.warning("## UISIDetector: Event \(detectorEvent.eventId) is already tracked") + return + } + // track it and start timer + let timer = DispatchSource.makeTimerSource(queue: dispatchQueue) + timer.schedule(deadline: .now() + .seconds(Self.timeoutSeconds)) + timer.setEventHandler { [weak self] in + guard let self = self else { return } + self.unTrack(eventId: detectorEvent.eventId, roomId: detectorEvent.roomId) + MXLog.verbose("## UISIDetector: Timeout on \(detectorEvent.eventId)") + self.triggerUISI(source: detectorEvent) + } + trackedEvents[trackedId] = (detectorEvent, timer) + timer.activate() + } + + private func triggerUISI(source: E2EMessageDetected) { + guard enabled else { return } + MXLog.info("## UISIDetector: Unable To Decrypt \(source)") + self.delegate?.uisiDetected(source: source) + } + + @discardableResult private func unTrack(eventId: String, roomId: String) -> E2EMessageDetected? { + let trackedId = Self.trackedEventId(roomId: roomId, eventId: eventId) + guard let (event, timer) = trackedEvents[trackedId] + else { + return nil + } + trackedEvents[trackedId] = nil + timer.cancel() + return event + } + + static func trackedEventId(roomId: String, eventId: String) -> String { + return "\(roomId)-\(eventId)" + } + +} From 477dc34a50f1fc11b19ecb1206de7509d8e3ab78 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 11 Mar 2022 16:47:08 +0000 Subject: [PATCH 002/135] Add AutoReported, re-work big client interface and hook up AutoReporter. --- Config/BuildSettings.swift | 4 + Riot/Categories/Codable.swift | 26 ++ .../MXBugReportRestClient+Riot.swift | 117 +++++++++ Riot/Categories/Publisher+Riot.swift | 37 +++ .../Settings/RiotSettings+Publisher.swift | 29 +++ Riot/Managers/Settings/RiotSettings.swift | 5 + .../UISIAutoReporter/UISIAutoReporter.swift | 226 ++++++++++++++++++ .../UISIAutoReporter/UISIDetector.swift | 20 +- Riot/Modules/Application/LegacyAppDelegate.m | 26 ++ .../BugReport/BugReportViewController.m | 98 +------- 10 files changed, 494 insertions(+), 94 deletions(-) create mode 100644 Riot/Categories/Codable.swift create mode 100644 Riot/Categories/MXBugReportRestClient+Riot.swift create mode 100644 Riot/Categories/Publisher+Riot.swift create mode 100644 Riot/Managers/Settings/RiotSettings+Publisher.swift create mode 100644 Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 8c7ebba3f..46856b850 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -191,6 +191,7 @@ final class BuildSettings: NSObject { static let bugReportEndpointUrlString = "https://riot.im/bugreports" // Use the name allocated by the bug report server static let bugReportApplicationId = "riot-ios" + static let bugReportUISIId = "element-auto-uisi" // MARK: - Integrations @@ -376,6 +377,9 @@ final class BuildSettings: NSObject { // MARK: - Secrets Recovery static let secretsRecoveryAllowReset = true + // MARK: - UISI Autoreporting + static let cryptoUISIAutoReportingEnabled = true + // MARK: - Polls static var pollsEnabled: Bool { diff --git a/Riot/Categories/Codable.swift b/Riot/Categories/Codable.swift new file mode 100644 index 000000000..b27c9c7ee --- /dev/null +++ b/Riot/Categories/Codable.swift @@ -0,0 +1,26 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension Encodable { + /// Convenience method to get the json string of an Encodable + var jsonString: String? { + let encoder = JSONEncoder() + guard let jsonData = try? encoder.encode(self) else { return nil } + return String(data: jsonData, encoding: .utf8) + } +} diff --git a/Riot/Categories/MXBugReportRestClient+Riot.swift b/Riot/Categories/MXBugReportRestClient+Riot.swift new file mode 100644 index 000000000..fd3fe313e --- /dev/null +++ b/Riot/Categories/MXBugReportRestClient+Riot.swift @@ -0,0 +1,117 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MatrixSDK +import GBDeviceInfo + +extension MXBugReportRestClient { + + @objc static func vc_bugReportRestClient(appName: String) -> MXBugReportRestClient { + guard let client = MXBugReportRestClient(bugReportEndpoint: BuildSettings.bugReportEndpointUrlString) else { + fatalError("Could not create MXBugReportRestClient") + } + + // App info + client.appName = appName + client.version = AppDelegate.theDelegate().appVersion + client.build = AppDelegate.theDelegate().build + + client.deviceModel = GBDeviceInfo.deviceInfo().modelString + client.deviceOS = "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)" + return client + } + + @objc func vc_sendBugReport( + description: String, + sendLogs: Bool, + sendCrashLog: Bool, + sendFiles: [URL]? = nil, + additionalLabels: [String]? = nil, + customFields: [String: String]? = nil, + progress: ((MXBugReportState, Progress?) -> Void)? = nil, + success: ((String?) -> Void)? = nil, + failure: ((Error?) -> Void)? = nil + ) { + // User info (TODO: handle multi-account and find a way to expose them in rageshake API) + var userInfo = [String: String]() + let mainAccount = MXKAccountManager.shared().accounts.first + if let userId = mainAccount?.mxSession.myUser.userId { + userInfo["user_id"] = userId + } + if let deviceId = mainAccount?.mxSession.matrixRestClient.credentials.deviceId { + userInfo["device_id"] = deviceId + } + + userInfo["locale"] = NSLocale.preferredLanguages[0] + userInfo["default_app_language"] = Bundle.main.preferredLocalizations[0] // The language chosen by the OS + userInfo["app_language"] = Bundle.mxk_language() ?? userInfo["default_app_language"] // The language chosen by the user + + // Application settings + userInfo["lazy_loading"] = MXKAppSettings.standard().syncWithLazyLoadOfRoomMembers ? "ON" : "OFF" + + let currentDate = Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + userInfo["local_time"] = dateFormatter.string(from: currentDate) + + dateFormatter.timeZone = TimeZone(identifier: "UTC") + userInfo["utc_time"] = dateFormatter.string(from: currentDate) + + if let customFields = customFields { + // combine userInfo with custom fields overriding with custom where there is a conflict + userInfo.merge(customFields) { (_, new) in new } + } + others = userInfo + + var labels: [String] = additionalLabels ?? [String]() + // Add a Github label giving information about the version + if var versionLabel = version, let buildLabel = build { + + // If this is not the app store version, be more accurate on the build origin + if buildLabel == VectorL10n.settingsConfigNoBuildInfo { + // This is a debug session from Xcode + versionLabel += "-debug" + } else if !buildLabel.contains("master") { + // This is a Jenkins build. Add the branch and the build number + let buildString = buildLabel.replacingOccurrences(of: " ", with: "-") + versionLabel += "-\(buildString)" + } + labels += [versionLabel] + } + if sendCrashLog { + labels += ["crash"] + } + + var sendDescription = description + if sendCrashLog, + let crashLogFile = MXLogger.crashLog(), + let crashLog = try? String(contentsOfFile: crashLogFile, encoding: .utf8) { + // Append the crash dump to the user description in order to ease triaging of GH issues + sendDescription += "\n\n\n--------------------------------------------------------------------------------\n\n\(crashLog)" + } + + sendBugReport(sendDescription, + sendLogs: sendLogs, + sendCrashLog: sendCrashLog, + sendFiles: sendFiles, + attachGitHubLabels: labels, + progress: progress, + success: success, + failure: failure) + } + +} diff --git a/Riot/Categories/Publisher+Riot.swift b/Riot/Categories/Publisher+Riot.swift new file mode 100644 index 000000000..6fa9e2051 --- /dev/null +++ b/Riot/Categories/Publisher+Riot.swift @@ -0,0 +1,37 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +@available(iOS 14.0, *) +extension Publisher { + + /// + /// Buffer upstream items and guarantee a time interval spacing out the published items. + /// - Parameters: + /// - spacingDelay: A delay in seconds to guarantee between emissions + /// - scheduler: The `DispatchQueue` on which to schedule emissions. + /// - Returns: The new wrapped publisher + func bufferAndSpace(spacingDelay: Int, scheduler: DispatchQueue = DispatchQueue.main) -> Publishers.FlatMap< + Publishers.SetFailureType.Output>, DispatchQueue>, Publishers.Buffer.Failure>, + Publishers.Buffer + > { + return buffer(size: .max, prefetch: .byRequest, whenFull: .dropNewest) + .flatMap(maxPublishers: .max(1)) { + Just($0).delay(for: .seconds(spacingDelay), scheduler: scheduler) + } + } +} diff --git a/Riot/Managers/Settings/RiotSettings+Publisher.swift b/Riot/Managers/Settings/RiotSettings+Publisher.swift new file mode 100644 index 000000000..f1e6f17cb --- /dev/null +++ b/Riot/Managers/Settings/RiotSettings+Publisher.swift @@ -0,0 +1,29 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +extension RiotSettings { + + @available(iOS 13.0, *) + func publisher(for key: String) -> AnyPublisher { + return NotificationCenter.default.publisher(for: .userDefaultValueUpdated) + .filter({ $0.object as? String == key }) + .eraseToAnyPublisher() + } + +} diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index fdff093ce..581757b73 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -30,6 +30,7 @@ final class RiotSettings: NSObject { static let pinRoomsWithMissedNotificationsOnHome = "pinRoomsWithMissedNotif" static let pinRoomsWithUnreadMessagesOnHome = "pinRoomsWithUnread" static let showAllRoomsInHomeSpace = "showAllRoomsInHomeSpace" + static let enableUISIAutoReporting = "enableUISIAutoReporting" } static let shared = RiotSettings() @@ -146,6 +147,10 @@ final class RiotSettings: NSObject { @UserDefault(key: "enableThreads", defaultValue: false, storage: defaults) var enableThreads + /// Indicates if threads enabled in the timeline. + @UserDefault(key: UserDefaultsKeys.enableUISIAutoReporting, defaultValue: BuildSettings.cryptoUISIAutoReportingEnabled, storage: defaults) + var enableUISIAutoReporting + // MARK: Calls /// Indicate if `allowStunServerFallback` settings has been set once. diff --git a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift new file mode 100644 index 000000000..2b2d8ff41 --- /dev/null +++ b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift @@ -0,0 +1,226 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MatrixSDK +import Combine + +struct UISIAutoReportData { + let eventId: String? + let roomId: String? + let senderKey: String? + let deviceId: String? + let source: UISIEventSource? + let userId: String? + let sessionId: String? +} + +extension UISIAutoReportData: Codable { + enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case roomId = "room_id" + case senderKey = "sender_key" + case deviceId = "device_id" + case source + case userId = "user_id" + case sessionId = "session_id" + } +} + +@available(iOS 14.0, *) +@objcMembers class UISIAutoReporter: NSObject, UISIDetectorDelegate { + + struct ReportInfo: Hashable { + let roomId: String + let sessionId: String + } + + static let autoRsRequest = "im.vector.auto_rs_request" + + private let bugReporter: MXBugReportRestClient + private let dispatchQueue = DispatchQueue(label: "io.element.UISIAutoReporter.queue") + // Simple in memory cache of already sent report + private var alreadyReportedUisi = Set() + private let e2eDetectedSubject = PassthroughSubject() + private let matchingRSRequestSubject = PassthroughSubject() + private var cancellables = Set() + private var sessions = [MXSession]() + private var enabled = false { + didSet { + guard oldValue != enabled else { return } + detector.enabled = enabled + } + } + + override init() { + self.bugReporter = MXBugReportRestClient.vc_bugReportRestClient(appName: BuildSettings.bugReportUISIId) + super.init() + // Simple rate limiting, for any rage-shakes emitted we guarantee a spacing between requests. + e2eDetectedSubject + .bufferAndSpace(spacingDelay: 2) + .sink { [weak self] in + guard let self = self else { return } + self.sendRageShake(source: $0) + }.store(in: &cancellables) + + matchingRSRequestSubject + .bufferAndSpace(spacingDelay: 2) + .sink { [weak self] in + guard let self = self else { return } + self.sendMatchingRageShake(source: $0) + }.store(in: &cancellables) + + self.enabled = RiotSettings.shared.enableUISIAutoReporting + RiotSettings.shared.publisher(for: RiotSettings.UserDefaultsKeys.enableUISIAutoReporting) + .sink { [weak self] _ in + guard let self = self else { return } + self.enabled = RiotSettings.shared.enableUISIAutoReporting + } + .store(in: &cancellables) + } + + private lazy var detector: UISIDetector = { + let detector = UISIDetector() + detector.delegate = self + return detector + }() + + + var reciprocateToDeviceEventType: String { + return Self.autoRsRequest + } + + func uisiDetected(source: E2EMessageDetected) { + guard source.source != UISIEventSource.initialSync else { return } + dispatchQueue.async { + let reportInfo = ReportInfo(roomId: source.roomId, sessionId: source.sessionId) + let alreadySent = self.alreadyReportedUisi.contains(reportInfo) + if !alreadySent { + self.alreadyReportedUisi.insert(reportInfo) + self.e2eDetectedSubject.send(source) + } + } + } + + func uisiReciprocateRequest(source: MXEvent) { + guard source.type == Self.autoRsRequest else { return } + self.matchingRSRequestSubject.send(source) + } + + func sendRageShake(source: E2EMessageDetected) { + MXLog.debug("dl sendRageShake") + guard let session = sessions.first else { return } + let uisiData = UISIAutoReportData( + eventId: source.eventId, + roomId: source.roomId, + senderKey: source.senderKey, + deviceId: source.senderDeviceId, + source: source.source, + userId: source.senderUserId, + sessionId: source.sessionId + ).jsonString ?? "" + + self.bugReporter.vc_sendBugReport( + description: "Auto-reporting decryption error", + sendLogs: true, + sendCrashLog: true, + additionalLabels: [ + "Z-UISI", + "ios", + "uisi-recipient" + ], + customFields: ["auto_uisi": uisiData], + success: { reportUrl in + let contentMap = MXUsersDevicesMap() + let content = [ + "event_id": source.eventId, + "room_id": source.roomId, + "session_id": source.sessionId, + "device_id": source.senderDeviceId, + "user_id": source.senderUserId, + "sender_key": source.senderKey, + "recipient_rageshake": reportUrl + ] + contentMap.setObject(content as NSDictionary, forUser: source.senderUserId, andDevice: source.senderDeviceId) + session.matrixRestClient.sendDirectToDevice( + eventType: Self.autoRsRequest, + contentMap: contentMap, + txnId: nil + ) { response in + if response.isFailure { + MXLog.warning("failed to send auto-uisi to device") + } + } + }, + failure: { [weak self] error in + guard let self = self else { return } + self.dispatchQueue.async { + self.alreadyReportedUisi.remove(ReportInfo(roomId: source.roomId, sessionId: source.sessionId)) + } + }) + } + + func sendMatchingRageShake(source: MXEvent) { + MXLog.debug("dl sendMatchingRageShake") + let eventId = source.content["event_id"] as? String + let roomId = source.content["room_id"] as? String + let sessionId = source.content["session_id"] as? String + let deviceId = source.content["device_id"] as? String + let userId = source.content["user_id"] as? String + let senderKey = source.content["sender_key"] as? String + let matchingIssue = source.content["recipient_rageshake"] as? String ?? "" + + + let uisiData = UISIAutoReportData( + eventId: eventId, + roomId: roomId, + senderKey: senderKey, + deviceId: deviceId, + source: nil, + userId: userId, + sessionId: sessionId + ).jsonString ?? "" + + self.bugReporter.vc_sendBugReport( + description: "Auto-reporting decryption error", + sendLogs: true, + sendCrashLog: true, + additionalLabels: [ + "Z-UISI", + "ios", + "uisi-sender" + ], + customFields: [ + "auto_uisi": uisiData, + "recipient_rageshake": matchingIssue + ] + ) + } + + + func add(_ session: MXSession) { + sessions.append(session) + detector.enabled = enabled + session.eventStreamService.add(eventStreamListener: detector) + } + + func remove(_ session: MXSession) { + if let index = sessions.firstIndex(of: session) { + sessions.remove(at: index) + } + session.eventStreamService.remove(eventStreamListener: detector) + } +} diff --git a/Riot/Managers/UISIAutoReporter/UISIDetector.swift b/Riot/Managers/UISIAutoReporter/UISIDetector.swift index e1865d080..d16dcfd01 100644 --- a/Riot/Managers/UISIAutoReporter/UISIDetector.swift +++ b/Riot/Managers/UISIAutoReporter/UISIDetector.swift @@ -23,12 +23,14 @@ protocol UISIDetectorDelegate: AnyObject { func uisiReciprocateRequest(source: MXEvent) } -enum UISIEventSource { - case initialSync - case incrementalSync - case pagination +enum UISIEventSource: String { + case initialSync = "INITIAL_SYNC" + case incrementalSync = "INCREMENTAL_SYNC" + case pagination = "PAGINATION" } +extension UISIEventSource: Equatable, Codable { } + struct E2EMessageDetected { let eventId: String let roomId: String @@ -43,9 +45,9 @@ struct E2EMessageDetected { eventId: event.eventId ?? "", roomId: roomId, senderUserId: event.sender, - senderDeviceId: event.content["device_id"] as? String ?? "", - senderKey: event.content["sender_key"] as? String ?? "", - sessionId: event.content["session_id"] as? String ?? "", + senderDeviceId: event.wireContent["device_id"] as? String ?? "", + senderKey: event.wireContent["sender_key"] as? String ?? "", + sessionId: event.wireContent["session_id"] as? String ?? "", source: source ) } @@ -70,14 +72,14 @@ class UISIDetector: MXLiveEventListener { func onLiveEvent(roomId: String, event: MXEvent) { - guard enabled, !event.isEncrypted else { return } + guard enabled, event.isEncrypted, event.clear == nil else { return } dispatchQueue.async { self.handleEventReceived(detectorEvent: E2EMessageDetected.fromEvent(event: event, roomId: roomId, source: .incrementalSync)) } } func onPaginatedEvent(roomId: String, event: MXEvent) { - guard enabled else { return } + guard enabled, event.isEncrypted, event.clear == nil else { return } dispatchQueue.async { self.handleEventReceived(detectorEvent: E2EMessageDetected.fromEvent(event: event, roomId: roomId, source: .pagination)) } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 628a76508..9293aac79 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -220,6 +220,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni @property (nonatomic, strong) PushNotificationStore *pushNotificationStore; @property (nonatomic, strong) LocalAuthenticationService *localAuthenticationService; @property (nonatomic, strong, readwrite) CallPresenter *callPresenter; +@property (nonatomic, strong, readwrite) id uisiAutoReporter; @property (nonatomic, strong) MajorUpdateManager *majorUpdateManager; @@ -466,6 +467,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni self.spaceFeatureUnavailablePresenter = [SpaceFeatureUnavailablePresenter new]; + if (@available(iOS 14.0, *)) { + self.uisiAutoReporter = [[UISIAutoReporter alloc] init]; + } + // Add matrix observers, and initialize matrix sessions if the app is not launched in background. [self initMatrixSessions]; @@ -2124,6 +2129,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // register the session to the call service [_callPresenter addMatrixSession:mxSession]; + // register the session to the uisi auto-reporter + if (_uisiAutoReporter != nil) + { + if (@available(iOS 14.0, *)) + { + UISIAutoReporter* uisiAutoReporter = (UISIAutoReporter*)_uisiAutoReporter; + [uisiAutoReporter add:mxSession]; + } + } + [_callPresenter addMatrixSession:mxSession]; + [mxSessionArray addObject:mxSession]; // Do the one time check on device id @@ -2139,6 +2155,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // remove session from the call service [_callPresenter removeMatrixSession:mxSession]; + + // register the session to the uisi auto-reporter + if (_uisiAutoReporter != nil) + { + if (@available(iOS 14.0, *)) + { + UISIAutoReporter* uisiAutoReporter = (UISIAutoReporter*)_uisiAutoReporter; + [uisiAutoReporter remove:mxSession]; + } + } // Update the widgets manager [[WidgetManager sharedManager] removeMatrixSession:mxSession]; diff --git a/Riot/Modules/BugReport/BugReportViewController.m b/Riot/Modules/BugReport/BugReportViewController.m index 3dd714654..2839daea8 100644 --- a/Riot/Modules/BugReport/BugReportViewController.m +++ b/Riot/Modules/BugReport/BugReportViewController.m @@ -295,47 +295,8 @@ { self.isSendingLogs = YES; - // Setup data to send - bugReportRestClient = [[MXBugReportRestClient alloc] initWithBugReportEndpoint:BuildSettings.bugReportEndpointUrlString]; - - // App info - bugReportRestClient.appName = BuildSettings.bugReportApplicationId; - bugReportRestClient.version = [AppDelegate theDelegate].appVersion; - bugReportRestClient.build = [AppDelegate theDelegate].build; - - // Device info - bugReportRestClient.deviceModel = [GBDeviceInfo deviceInfo].modelString; - bugReportRestClient.deviceOS = [NSString stringWithFormat:@"%@ %@", [[UIDevice currentDevice] systemName], [[UIDevice currentDevice] systemVersion]]; - - // User info (TODO: handle multi-account and find a way to expose them in rageshake API) - NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; - MXKAccount *mainAccount = [MXKAccountManager sharedManager].accounts.firstObject; - if (mainAccount.mxSession.myUser.userId) - { - userInfo[@"user_id"] = mainAccount.mxSession.myUser.userId; - } - if (mainAccount.mxSession.matrixRestClient.credentials.deviceId) - { - userInfo[@"device_id"] = mainAccount.mxSession.matrixRestClient.credentials.deviceId; - } - - userInfo[@"locale"] = [NSLocale preferredLanguages][0]; - userInfo[@"default_app_language"] = [[NSBundle mainBundle] preferredLocalizations][0]; // The language chosen by the OS - userInfo[@"app_language"] = [NSBundle mxk_language] ? [NSBundle mxk_language] : userInfo[@"default_app_language"]; // The language chosen by the user - - // Application settings - userInfo[@"lazy_loading"] = [MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers ? @"ON" : @"OFF"; - - NSDate *currentDate = [NSDate date]; - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; - userInfo[@"local_time"] = [dateFormatter stringFromDate:currentDate]; - - [dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]]; - userInfo[@"utc_time"] = [dateFormatter stringFromDate:currentDate]; - - bugReportRestClient.others = userInfo; - + bugReportRestClient = [MXBugReportRestClient vc_bugReportRestClientWithAppName:BuildSettings.bugReportApplicationId]; + // Screenshot NSArray *files; if (_screenshot && _sendScreenshot) @@ -347,56 +308,23 @@ files = @[screenShotFile]; } - - // Prepare labels to attach to the GitHub issue - NSMutableArray *gitHubLabels = [NSMutableArray array]; - if (_reportCrash) - { - // Label the GH issue as "crash" - [gitHubLabels addObject:@"crash"]; - } - - // Add a Github label giving information about the version - if (bugReportRestClient.version && bugReportRestClient.build) - { - NSString *build = bugReportRestClient.build; - NSString *versionLabel = bugReportRestClient.version; - - // If this is not the app store version, be more accurate on the build origin - if ([build isEqualToString:[VectorL10n settingsConfigNoBuildInfo]]) - { - // This is a debug session from Xcode - versionLabel = [versionLabel stringByAppendingString:@"-debug"]; - } - else if (build && ![build containsString:@"master"]) - { - // This is a Jenkins build. Add the branch and the build number - NSString *buildString = [build stringByReplacingOccurrencesOfString:@" " withString:@"-"]; - versionLabel = [[versionLabel stringByAppendingString:@"-"] stringByAppendingString:buildString]; - } - - [gitHubLabels addObject:versionLabel]; - } - + NSMutableString *bugReportDescription = [NSMutableString stringWithString:_bugReportDescriptionTextView.text]; - - if (_reportCrash) - { - // Append the crash dump to the user description in order to ease triaging of GH issues - NSString *crashLogFile = [MXLogger crashLog]; - NSString *crashLog = [NSString stringWithContentsOfFile:crashLogFile encoding:NSUTF8StringEncoding error:nil]; - [bugReportDescription appendFormat:@"\n\n\n--------------------------------------------------------------------------------\n\n%@", crashLog]; - } - + // starting a background task to have a bit of extra time in case of user forgets about the report and sends the app to background __block UIBackgroundTaskIdentifier operationBackgroundId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [[UIApplication sharedApplication] endBackgroundTask:operationBackgroundId]; operationBackgroundId = UIBackgroundTaskInvalid; }]; - // Submit - [bugReportRestClient sendBugReport:bugReportDescription sendLogs:_sendLogs sendCrashLog:_reportCrash sendFiles:files attachGitHubLabels:gitHubLabels progress:^(MXBugReportState state, NSProgress *progress) { - + [bugReportRestClient vc_sendBugReportWithDescription:bugReportDescription + sendLogs:_sendLogs + sendCrashLog:_reportCrash + sendFiles:files + additionalLabels:nil + customFields:nil + progress:^(MXBugReportState state, NSProgress *progress) { + switch (state) { case MXBugReportStateProgressZipping: @@ -413,7 +341,7 @@ self.sendingProgress.progress = progress.fractionCompleted; - } success:^{ + } success:^(NSString *reportUrl){ self->bugReportRestClient = nil; From e598b33469bea91f0c4f1aa3fe4d0e490e4eec24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 8 Mar 2022 15:13:16 +0000 Subject: [PATCH 003/135] Translated using Weblate (Estonian) Currently translated at 100.0% (1871 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 0923de120..ff6b4bcaa 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2075,3 +2075,5 @@ "home_syncing" = "Sünkroniseerimine"; "room_participants_leave_success" = "Sa oled jututoast lahkunud"; "room_participants_leave_processing" = "Lahkumine"; +"notice_error_unformattable_event" = "** Sõnumi töötlemine ei õnnestu. Palun anna meile sellest veast teada"; +"settings_labs_use_only_latest_user_avatar_and_name" = "Sõnumite ajaloos leiduvate kasutajate puhul näita viimati kasutatud tunnuspilti ning nime"; From eff83afc008aa41fa0a2c732524566e157fe7074 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 9 Mar 2022 11:01:05 +0000 Subject: [PATCH 004/135] Translated using Weblate (Albanian) Currently translated at 99.7% (1866 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ --- Riot/Assets/sq.lproj/Vector.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 9c2024b3d..509287f56 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -2124,3 +2124,7 @@ "attachment_unsupported_preview_title" = "S’arrihet të bëhet paraparje"; "room_displayname_all_other_members_left" = "%@ (Iku)"; "message_reply_to_sender_sent_their_location" = "ka dhënë vendndodhjen e vet."; +"room_participants_leave_processing" = "Dalje"; +"notice_error_unformattable_event" = "** S’arrihet të riprodhohet mesazhi. Ju lutemi, njoftoni një të metë"; +"settings_labs_use_only_latest_user_avatar_and_name" = "Shfaq në historik mesazhesh avatarin dhe emrin më të ri të përdoruesve"; +"room_participants_leave_success" = "Doli nga dhoma"; From 5ffb8aa0abbae72b7a884b4e92d0b48d0b31b54d Mon Sep 17 00:00:00 2001 From: Szimszon Date: Fri, 11 Mar 2022 09:31:56 +0000 Subject: [PATCH 005/135] Translated using Weblate (Hungarian) Currently translated at 100.0% (1871 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index cca7148ed..757189a94 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2136,3 +2136,5 @@ "home_syncing" = "Szinkronizálás"; "room_participants_leave_success" = "Szobából kilépve"; "room_participants_leave_processing" = "Távozás"; +"notice_error_unformattable_event" = "** Az üzenetet nem lehet megjeleníteni. Kérlek jelezd ezt a hibát"; +"settings_labs_use_only_latest_user_avatar_and_name" = "A felhasználó jelenlegi profilképének és nevének megjelenítése a régi üzeneteknél is"; From 60441221f419dbc0df5e7604f2a4603bf2d5ca0b Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Wed, 9 Mar 2022 08:45:09 +0000 Subject: [PATCH 006/135] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1871 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ --- Riot/Assets/pt_BR.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 92f52bca6..7ca1ea2d9 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -2107,3 +2107,5 @@ "home_syncing" = "Sincando"; "room_participants_leave_success" = "Saiu de sala"; "room_participants_leave_processing" = "Saindo"; +"notice_error_unformattable_event" = "** Incapaz de render mensagem. Por favor reporte um bug"; +"settings_labs_use_only_latest_user_avatar_and_name" = "Mostrar avatar e nomes mais recentes para usuárias(os) em histórico de mensagem"; From 762e43159ba17f06b606ae7da54664d9381d5641 Mon Sep 17 00:00:00 2001 From: Itzik Abukrat Date: Thu, 10 Mar 2022 08:55:34 +0000 Subject: [PATCH 007/135] Translated using Weblate (Hebrew) Currently translated at 23.3% (436 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/he/ --- Riot/Assets/he.lproj/Vector.strings | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/Riot/Assets/he.lproj/Vector.strings b/Riot/Assets/he.lproj/Vector.strings index f13b3b81c..94309cca6 100644 --- a/Riot/Assets/he.lproj/Vector.strings +++ b/Riot/Assets/he.lproj/Vector.strings @@ -490,3 +490,74 @@ "spaces_empty_space_title" = "למרחב הזה אין עדיין חדרים"; "space_beta_announce_title" = "בקרוב - מרחבים"; "space_feature_unavailable_subtitle" = "מרחבים עדיין לא קיימים עבור iOS, אפשר להשתמש בהם בממשק ה WEB והמחשב"; + +// Recover with key + +"secrets_recovery_with_key_title" = "מפתח אבטחה"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "אנא וודא שהכנסת ביטוי אבטחה נכון."; +"secrets_recovery_with_passphrase_invalid_passphrase_title" = "לא יכול לגשת לאחסון סודי"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "השתמש במפתח האבטחה שלך"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "לא יודע את ביטוי האבטחה שלך? אתה יכול "; +"secrets_recovery_with_passphrase_recover_action" = "השתמש בביטוי"; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "הכנס ביטוי אבטחה"; +"secrets_recovery_with_passphrase_passphrase_title" = "הכנס"; +"secrets_recovery_with_passphrase_information_verify_device" = "השתמש בביטוי האבטחה שלך על מנת לאמת התקן זה."; +"secrets_recovery_with_passphrase_information_default" = "הכנס את הודעת האבטחה הישנה שלך ואת זיהוי החתימה שלך על מנת לאמת ממשקים אחרים ע\"י הכנסת ביטוי האבטחה שלך."; + +// Recover with passphrase + +"secrets_recovery_with_passphrase_title" = "ביטוי אבטחה"; +"secrets_recovery_reset_action_part_2" = "אתחל הכל"; + +// MARK: - Secrets recovery + +"secrets_recovery_reset_action_part_1" = "אפשרויות שחזור נשכחו או הלכו לאיבוד? "; +"user_verification_session_details_verify_action_other_user" = "מאומת ידנית"; +"user_verification_session_details_verify_action_current_user_manually" = "אימות טקסט ידני"; +"user_verification_session_details_verify_action_current_user" = "אימות אינטראקטיבי"; +"user_verification_session_details_additional_information_untrusted_current_user" = "אם לא התחברת לממשק זה, חשבונך עלול להיחסם."; +"user_verification_session_details_additional_information_untrusted_other_user" = "עד לאישור הממשק ע\"י היוזר, הודעות שנשלחות אליו וממנו יסומנו עם הודעת אזהרה. לחילופין, אתה יכול לאמת אותו ידנית."; +"user_verification_session_details_information_untrusted_other_user" = " התחבר ע\"י שימוש בממשק חדש:"; +"user_verification_session_details_information_untrusted_current_user" = "אמת ממשק זה ע\"מ לסמן אותו כבטוח ואפשר לו גישה להודעות מוצפנות:"; +"user_verification_session_details_information_trusted_other_user_part2" = " אמת זאת:"; +"user_verification_session_details_information_trusted_other_user_part1" = "ממשק זה מהימן עבור הודעות מוצפנות מכיון "; +"user_verification_session_details_information_trusted_current_user" = "ממשק זה מהימן עבור הודעות מוצפנות מכיון שכבר אימתת אותו:"; +"user_verification_session_details_untrusted_title" = "לא מהימן"; + +// Session details + +"user_verification_session_details_trusted_title" = "מהימן"; +"user_verification_sessions_list_session_untrusted" = "לא מהימן"; +"user_verification_sessions_list_session_trusted" = "מהימן"; +"user_verification_sessions_list_table_title" = "ממשק"; +"user_verification_sessions_list_information" = "הודעות עם משתמש זה בחדר זה מוצפנות מקצה לקצה ואינן ניתנות לקריאה ע\"י צד שלישי."; +"user_verification_sessions_list_user_trust_level_unknown_title" = "לא ידוע"; +"user_verification_sessions_list_user_trust_level_warning_title" = "זהירות"; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "מהימן"; +"user_verification_start_additional_information" = "ע\"מ לשמור על אבטחה, בצע פעולה זו לבד או מצא דרך חלופית ליצור קשר."; +"user_verification_start_waiting_partner" = "ממתין ל %@…"; +"user_verification_start_information_part2" = " ע\"י בדיקה באמצעות קוד חד פעמי בשני ההתקנים שלך."; +"user_verification_start_information_part1" = "לטובת אבטחה מקסימלית, אנא אשר. "; + +// MARK: - User verification + +// Start + +"user_verification_start_verify_action" = "התחל אימות"; +"key_verification_scan_confirmation_scanned_device_information" = "האם התקן אחר מראה גם את אותה הגנה?"; +"key_verification_scan_confirmation_scanned_user_information" = "האם %@ מציג את אותה הגנה?"; + +// Scanned +"key_verification_scan_confirmation_scanned_title" = "כמעט סיימנו!"; +"key_verification_scan_confirmation_scanning_device_waiting_other" = "ממתין להתקן אחר…"; +"key_verification_scan_confirmation_scanning_user_waiting_other" = "ממתין ל%@…"; + +// MARK: Scan confirmation + +// Scanning +"key_verification_scan_confirmation_scanning_title" = "כמעט סיימנו! ממתין לאישור…"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "קוד QR אומת בהצלחה."; From f269379097c9260de3462f7a645a86535a58c194 Mon Sep 17 00:00:00 2001 From: Uri Avraham Date: Tue, 8 Mar 2022 16:15:11 +0000 Subject: [PATCH 008/135] Translated using Weblate (Hebrew) Currently translated at 23.3% (436 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/he/ --- Riot/Assets/he.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/he.lproj/Vector.strings b/Riot/Assets/he.lproj/Vector.strings index 94309cca6..eb3e45bec 100644 --- a/Riot/Assets/he.lproj/Vector.strings +++ b/Riot/Assets/he.lproj/Vector.strings @@ -561,3 +561,16 @@ // Scanning "key_verification_scan_confirmation_scanning_title" = "כמעט סיימנו! ממתין לאישור…"; "key_verification_verify_qr_code_scan_other_code_success_message" = "קוד QR אומת בהצלחה."; +"auth_add_phone_message_2" = "הגדר מספר טלפון, בהמשך יהווה אפשרות לזיהויך על ידי משתמשים שמכירים אותך."; +"auth_add_email_message_2" = "הגדר דוא\"ל לטובת שחזור החשבון, בהמשך יהווה אפשרות לזיהויך על ידי משתמשים שמכירים אותך."; +"auth_invalid_user_name" = "שמות משתמשים יכולים להכיל רק אותיות, מספרים, נקודות, מקפים וקווים תחתונים"; +"auth_home_server_placeholder" = "כתובת אתר (לדוגמה https://matrix.org)"; +"auth_user_id_placeholder" = "דואר אלקטרוני או שם משתמש"; +"auth_login_single_sign_on" = "התחבר"; +"onboarding_use_case_existing_server_button" = "התחבר לשרת"; +"onboarding_use_case_existing_server_message" = "מחפשים להצטרף לשרת קיים ?"; +"onboarding_use_case_skip_button" = "דלג על השאלה"; +/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ +"onboarding_use_case_not_sure_yet" = "מתלבט ? אתה מסוגל %@"; +"onboarding_use_case_community_messaging" = "קהילות"; +"onboarding_use_case_personal_messaging" = "חברים ומשפחה"; From 0fe78bed2029e74e00c792a4e76704161475e397 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Thu, 10 Mar 2022 22:41:31 +0000 Subject: [PATCH 009/135] Translated using Weblate (Slovak) Currently translated at 99.5% (1863 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 0d9b39df7..453c52a42 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -1680,7 +1680,7 @@ "poll_edit_form_poll_type" = "Typ ankety"; "poll_edit_form_update_failure_title" = "Nepodarilo sa aktualizovať anketu"; "poll_edit_form_update_failure_subtitle" = "Prosím, skúste to znova"; -"poll_edit_form_poll_type_open" = "Otvoriť anketu"; +"poll_edit_form_poll_type_open" = "Otvorená anketa"; "poll_edit_form_poll_type_open_description" = "Hlasujúci uvidia výsledky hneď po hlasovaní"; "poll_edit_form_poll_type_closed_description" = "Výsledky sa zobrazia až po ukončení ankety"; "poll_timeline_total_votes_not_voted" = "%lu odovzdaných hlasov. Hlasujte a pozrite si výsledky"; @@ -2320,3 +2320,5 @@ "notice_event_redacted_by_you" = " vami"; "home_syncing" = "Synchronizácia"; "room_participants_leave_success" = "Opustil miestnosť"; +"notice_error_unformattable_event" = "** Správa sa nedá zobraziť. Prosím, nahláste chybu"; +"settings_labs_use_only_latest_user_avatar_and_name" = "Zobraziť posledný obrázok a meno používateľov v histórii správ"; From 57f7d512fccefc296f8dd32ead50ae423e53fb78 Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 11 Mar 2022 19:41:11 +0000 Subject: [PATCH 010/135] Translated using Weblate (German) Currently translated at 99.8% (1868 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 4ddccf894..433c136ae 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2138,3 +2138,8 @@ "attachment_unsupported_preview_title" = "Vorschau kann nicht angezeigt werden"; "message_reply_to_sender_sent_their_location" = "hat den eigenen Standort geteilt."; "room_displayname_all_other_members_left" = "%@ (Verlassen)"; +"notice_error_unformattable_event" = "** Nachricht kann nicht dargestellt werden. Bitte erstelle einen Bug-Report"; +"home_syncing" = "Synchronisiere"; +"settings_labs_use_only_latest_user_avatar_and_name" = "Immer aktuelle Profilbilder und Nicknamen anzeigen"; +"room_participants_leave_success" = "Raum verlassen"; +"room_participants_leave_processing" = "Verlassen"; From 6ec7cabb9ee9fe55dcc5a487f20d4d0adb362416 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 8 Mar 2022 14:38:12 +0000 Subject: [PATCH 011/135] Translated using Weblate (Ukrainian) Currently translated at 100.0% (1871 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index dc6711737..c3b7f33a4 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2330,3 +2330,5 @@ "home_syncing" = "Синхронізація"; "room_participants_leave_success" = "Вихід успішний"; "room_participants_leave_processing" = "Вихід триває"; +"notice_error_unformattable_event" = "** Неможливо показати повідомлення. Надішліть звіт про помилку"; +"settings_labs_use_only_latest_user_avatar_and_name" = "Показувати останній аватар та ім'я для користувачів у історії повідомлень"; From 635d30cd714e2df4b6afde674e736828b387d812 Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 8 Mar 2022 15:33:27 +0000 Subject: [PATCH 012/135] Translated using Weblate (Indonesian) Currently translated at 100.0% (1871 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 7940ead90..3d8478f45 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -1859,7 +1859,7 @@ "login_error_resource_limit_exceeded_title" = "Melebihi Batas Sumber"; "login_desktop_device" = "Desktop"; "login_tablet_device" = "Tablet"; -"login_mobile_device" = "Mobile"; +"login_mobile_device" = "Ponsel"; "login_error_forgot_password_is_not_supported" = "Lupa kata sandi saat ini belum didukung"; "register_error_title" = "Pendaftaran Gagal"; "login_invalid_param" = "Parameter tidak valid"; @@ -1947,7 +1947,7 @@ "notification_settings_per_sender_notifications" = "Notifikasi per pengirim"; "notification_settings_per_room_notifications" = "Notifikasi per ruangan"; "notification_settings_custom_sound" = "Suara kustom"; -"notification_settings_highlight" = "Highlight"; +"notification_settings_highlight" = "Sorotan"; "notification_settings_word_to_match" = "kata untuk dicocokkan"; "notification_settings_never_notify" = "Jangan diberitahu"; "notification_settings_always_notify" = "Selalu diberitahu"; @@ -2333,3 +2333,5 @@ "home_syncing" = "Menyinkronkan"; "room_participants_leave_success" = "Telah keluar dari ruangan"; "room_participants_leave_processing" = "Meninggalkan"; +"notice_error_unformattable_event" = "** Tidak dapat memuat pesan. Mohon laporkan sebuah kutu"; +"settings_labs_use_only_latest_user_avatar_and_name" = "Tampilkan avatar dan nama terkini untuk pengguna di riwayat pesan"; From 2847c4c455518327a354dda8e710ac102cb1f562 Mon Sep 17 00:00:00 2001 From: random Date: Wed, 9 Mar 2022 10:04:55 +0000 Subject: [PATCH 013/135] Translated using Weblate (Italian) Currently translated at 100.0% (1871 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index e9b963254..83bf336fc 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2109,3 +2109,5 @@ "home_syncing" = "Sincronizzazione"; "room_participants_leave_success" = "Stanza abbandonata"; "room_participants_leave_processing" = "Uscita in corso"; +"notice_error_unformattable_event" = "** Impossibile visualizzare il messaggio. Si prega di segnalare l'errore"; +"settings_labs_use_only_latest_user_avatar_and_name" = "Mostra avatar e nome più recenti per gli utenti nella cronologia dei messaggi"; From e0e914c47ff0b3093ce9d235b0c3a1b132653fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Sun, 13 Mar 2022 16:45:09 +0000 Subject: [PATCH 014/135] Translated using Weblate (Icelandic) Currently translated at 100.0% (7 of 7 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/is/ --- Riot/Assets/is.lproj/InfoPlist.strings | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Riot/Assets/is.lproj/InfoPlist.strings b/Riot/Assets/is.lproj/InfoPlist.strings index 8b1378917..ea0b89629 100644 --- a/Riot/Assets/is.lproj/InfoPlist.strings +++ b/Riot/Assets/is.lproj/InfoPlist.strings @@ -1 +1,10 @@ + +"NSLocationWhenInUseUsageDescription" = "Þegar þú deilir staðsetningunni þinni með öðru fólki, þarf Element aðgang að henni til að geta birt hana á landakorti."; +"NSFaceIDUsageDescription" = "Face ID er notað til að fá aðgang að forritinu þínu."; +"NSCalendarsUsageDescription" = "Skoðaðu áætlaða fundi þína í forritinu."; +"NSContactsUsageDescription" = "Element mun birta tengiliðina þína svo þú getir boðið þeim að spjalla."; +"NSMicrophoneUsageDescription" = "Element þarf að fá aðgang að hljóðnemanum þínum fyrir símtöl, upptöku á myndskeiðum og upptöku talskilaboða."; +"NSPhotoLibraryUsageDescription" = "Myndasafnið er notað til að senda myndir og myndskeið."; +// Permissions usage explanations +"NSCameraUsageDescription" = "Myndavélin er notuð til að taka myndir og myndskeið og fyrir myndsímtöl."; From 8492f9a96ad4b1dc663a9b73b34483256c6b0d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Sun, 13 Mar 2022 16:46:14 +0000 Subject: [PATCH 015/135] Translated using Weblate (Icelandic) Currently translated at 100.0% (49 of 49 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/is/ --- Riot/Assets/is.lproj/Localizable.strings | 169 +++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/Riot/Assets/is.lproj/Localizable.strings b/Riot/Assets/is.lproj/Localizable.strings index 8b1378917..562f6da4b 100644 --- a/Riot/Assets/is.lproj/Localizable.strings +++ b/Riot/Assets/is.lproj/Localizable.strings @@ -1 +1,170 @@ + + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ sendi mynd"; + +/** Key verification **/ + +"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ vill sannreyna"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (hópsímtal)"; + +/* A user added a Jitsi call to a room */ +"GROUP_CALL_STARTED" = "Hópsímtal er byrjað"; + +/* Incoming named video conference invite from a specific person */ +"VIDEO_CONF_NAMED_FROM_USER" = "Myndsamtal í hópi frá %@: '%@'"; + +/* Incoming named voice conference invite from a specific person */ +"VOICE_CONF_NAMED_FROM_USER" = "Hópsímtal frá %@: '%@'"; + +/* Incoming unnamed video conference invite from a specific person */ +"VIDEO_CONF_FROM_USER" = "Myndsamtal í hópi frá %@"; + +/* Incoming unnamed voice conference invite from a specific person */ +"VOICE_CONF_FROM_USER" = "Hópsímtal frá %@"; + +/* Incoming one-to-one video call */ +"VIDEO_CALL_FROM_USER" = "Myndsamtal frá %@"; + +/** Calls **/ + +/* Incoming one-to-one voice call */ +"VOICE_CALL_FROM_USER" = "Símtal frá %@"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ uppfærði notandasniðið sitt"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ breytti auðkennismynd sinni"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ breytti nafni sínu"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ breyttu nafni sínu í %@"; + +/* A user has invited you to a named room */ +"USER_INVITE_TO_NAMED_ROOM" = "%@ bauð þér í %@"; + +/* A user has invited you to an (unamed) group chat */ +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ bauð þér að taka þátt í hópspjalli"; + +/** Invites **/ + +/* A user has invited you to a chat */ +"USER_INVITE_TO_CHAT" = "%@ hefur boðið þér að spjalla"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ sendi viðbrögð"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ brást við með %@"; + +/* Look, stuff's happened, alright? Just open the app. */ +"MSGS_IN_TWO_PLUS_ROOMS" = "@ ný skilaboð í %@, %@ og víðar"; + +/* Multiple messages in two rooms */ +"MSGS_IN_TWO_ROOMS" = "%@ ný skilaboð í %@ og %@"; + +/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ +"MSGS_FROM_TWO_PLUS_USERS" = "%@ ný skilaboð frá %@, %@ og fleirum"; + +/* Multiple unread messages from three people */ +"MSGS_FROM_THREE_USERS" = "%@ ný skilaboð frá %@, %@ og %@"; + +/* Multiple unread messages from two people */ +"MSGS_FROM_TWO_USERS" = "%@ ný skilaboð frá %@ og %@"; + +/* Multiple unread messages from a specific person, not referencing a room */ +"MSGS_FROM_USER" = "%@ ný skilaboð í %@"; + +/** Coalesced messages **/ + +/* Multiple unread messages in a room */ +"UNREAD_IN_ROOM" = "%@ ný skilaboð í %@"; + +/* New message with hidden content due to PIN enabled */ +"MESSAGE_PROTECTED" = "Ný skilaboð"; + +/* New message indicator on a room */ +"MESSAGE_IN_X" = "Skilaboð í %@"; + +/* New message indicator from a DM */ +"MESSAGE_FROM_X" = "Skilaboð frá %@"; + +/** Notification messages **/ + +/* New message indicator on unknown room */ +"MESSAGE" = "Skilaboð"; + +/* Sticker from a specific person, not referencing a room. */ +"STICKER_FROM_USER" = "%@ sendi límmerki"; + +/* A single unread message */ +"SINGLE_UNREAD" = "Þú hefur fengið skilaboð"; + +/* A single unread message in a room */ +"SINGLE_UNREAD_IN_ROOM" = "Þú hefur fengið skilaboð í %@"; + +/* New file message from a specific person, not referencing a room. */ +"LOCATION_FROM_USER" = "%@ hefur deilt staðsetningu sinni"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ sendi skrá %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ sendi talskilaboð"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ sendi hljóðskrá %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ sendi myndskeið"; + +/* New image message from a specific person in a named room. */ +"IMAGE_FROM_USER_IN_ROOM" = "%@ birti mynd %@ í %@"; + +/* New action message from a specific person in a named room. */ +"ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; + +/* New action message from a specific person, not referencing a room. */ +"ACTION_FROM_USER" = "* %@ %@"; + +/* New message from a specific person in a named room. Content included. */ +"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ í %@: %@"; + +/** Single, unencrypted messages (where we can include the content */ + +/* New message from a specific person, not referencing a room. Content included. */ +"MSG_FROM_USER_WITH_CONTENT" = "%@: %@"; + +/* New message from a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM" = "%@ birti í %@"; + +/** Single, end-to-end encrypted messages (ie. we don't know what they say) */ + +/* New message from a specific person, not referencing a room */ +"MSG_FROM_USER" = "%@ sendi skilboð"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ svaraði í %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ svaraði"; + +/** Titles **/ + +/* Message title for a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ í %@"; +/** General **/ + +"NOTIFICATION" = "Tilkynning"; From b62e6b57cc304effd57b907f8152588065280794 Mon Sep 17 00:00:00 2001 From: Itzik Abukrat Date: Mon, 14 Mar 2022 14:44:40 +0000 Subject: [PATCH 016/135] Translated using Weblate (Hebrew) Currently translated at 30.1% (564 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/he/ --- Riot/Assets/he.lproj/Vector.strings | 148 ++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/Riot/Assets/he.lproj/Vector.strings b/Riot/Assets/he.lproj/Vector.strings index eb3e45bec..1077bd901 100644 --- a/Riot/Assets/he.lproj/Vector.strings +++ b/Riot/Assets/he.lproj/Vector.strings @@ -574,3 +574,151 @@ "onboarding_use_case_not_sure_yet" = "מתלבט ? אתה מסוגל %@"; "onboarding_use_case_community_messaging" = "קהילות"; "onboarding_use_case_personal_messaging" = "חברים ומשפחה"; +"discard" = "לבטל"; +"abort" = "בטל"; +"yes" = "כן"; + +// Action +"no" = "לא"; +"login_error_resource_limit_exceeded_contact_button" = "צור קשר עם מנהל המערכת"; +"login_error_resource_limit_exceeded_message_contact" = "\n\nאנא צור קשר מנהל השירות שלך על מנת להמשיך להשתמש בשירות זה."; +"login_error_resource_limit_exceeded_message_monthly_active_user" = "שרת זה הגיע למגבלת המשתמשים הפעילים בו."; +"login_error_resource_limit_exceeded_message_default" = "שרת בית זה הגיע לסף של אחד המשאבים שלו."; +"login_error_resource_limit_exceeded_title" = "הגעה לסף משאבים"; +"login_desktop_device" = "שולחן עבודה"; +"login_tablet_device" = "טאבלט"; +"login_mobile_device" = "נייד"; +"login_error_forgot_password_is_not_supported" = "שכחתי סיסמה לא נתמך כרגע"; +"register_error_title" = "רישום נכשל"; +"room_accessibility_upload" = "העלאה"; +"room_accessibility_integrations" = "התקנות"; +"room_accessibility_search" = "חפש"; +"room_message_edits_history_title" = "עריכות הודעה"; +"room_resource_usage_limit_reached_message_contact_3" = " על מנת להגיע לעלית סף זה."; +"room_resource_usage_limit_reached_message_2" = "חלק מהמשתמשים לא יוכלו להתחבר."; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "שרת הבית הזה הגיע לסף החודשי של משתמשים פעילים בו אז "; +"room_resource_usage_limit_reached_message_1_default" = "שרת הבית הזה הגיע לסף של אחד המשאבים שלו אז "; +"room_resource_limit_exceeded_message_contact_3" = " על מנת להמשיך לעשות שימוש בשירות זה."; +"room_resource_limit_exceeded_message_contact_2_link" = "צור קשר עם מנהל השירות שלך"; +"room_resource_limit_exceeded_message_contact_1" = " בבקשה "; +"room_predecessor_link" = "לחץ כאן כדי לראות הודעות ישנות."; +"room_predecessor_information" = "חדר זה הוא המשך של שיחה אחרת."; +"room_replacement_link" = "השיחה ממשיכה כאן."; +"room_replacement_information" = "חדר זה הוחלף והוא אינו פעיל יותר."; +"room_action_reply" = "תשובה"; +"room_action_send_file" = "שלח קובץ"; +"room_action_send_sticker" = "שלח תוית"; +"room_action_send_photo_or_video" = "שלח תמונה אם סרטון וידאו"; +"room_action_camera" = "צלם תמונה או וידאו"; +"room_creation_error_invite_user_by_email_without_identity_server" = "לא הוגדר אף שרת זיהוי לכן אתה לא יכול להוסיף משתתף עם אימייל."; +"room_creation_invite_another_user" = "מספר משתמש, שם או אימייל"; +"room_creation_wait_for_creation" = "החדר כבר הוגדר. אנא המתן."; +"room_creation_make_private" = "הפוך לפרטי"; +"room_creation_keep_private" = "שמור על פרטיות"; +"room_creation_make_public_prompt_msg" = "האם אתה בטוח שברצונך ליצור צ'אט זה ציבורי? כל אחד יוכל לקרא את ההודעות שלך ולהצטרף לצ'אט."; +"room_creation_make_public_prompt_title" = "הגדר צ'אט זה ציבורי?"; +"room_creation_make_public" = "הפוך לציבורי"; +"room_creation_public_room" = "צ'אט זה הוא ציבורי"; +"room_creation_private_room" = "צ'אט זה הוא פרטי"; +"room_creation_privacy" = "פרטי"; +"room_creation_appearance_picture" = "תמונת צ'אט(אופציונלי)"; +"room_creation_appearance_name" = "שם"; +"room_creation_appearance" = "קיים"; +"room_creation_account" = "חשבון"; + +// Chat creation +"room_creation_title" = "צ'אט חדש"; + +// Errors +"error_user_already_logged_in" = "נראה שאתה מנסה להתחבר לשרת בית אחר. האם אתה מעוניין להתנתק?"; +"social_login_button_title_sign_up" = "הירשם באמצעות %@"; +"social_login_button_title_sign_in" = "התחבר באמצעות %@"; +"social_login_button_title_continue" = "המשך עם %@"; +"space_feature_unavailable_information" = "מרחבים הם דרך חדשה לקבץ חדרים ואנשים.\n\nהם יהיו כאן בקרוב. בינתיים, אם תצטרף לאחד בפלטפורמה אחרת, תוכל לגשת לכל חדר אליו אתה מצטרף כאן."; +"leave_space_and_all_rooms_action" = "עזוב את כל החדרים והמרחבים"; +"leave_space_only_action" = "אל תעזוב אף אחד מהחדרים"; +"leave_space_message_admin_warning" = "אתה עם הרשאות admin במרחב זה, וודא שהעברת הרשאות admin לחבר אחר לפני שאתה עוזב."; +"leave_space_message" = "האתם אתה בטוח שברצונך לעזוב את %@? האם אתה מעוניין לעזוב גם את כל החדרים והמרחבים של מרחב זה?"; +"leave_space_title" = "עזוב את %@"; +"spaces_home_space_title" = "בית"; +"space_beta_announce_information" = "מרחבים הם דרך לקבץ חדרים ואנשים. הם לא ב IOS לבינתיים, אבל אתה יכול להשתמש בהם כעת ב WEB או בשולחן העבודה."; +"space_beta_announce_subtitle" = "הגרסה החדשה של קהילות"; +"space_beta_announce_badge" = "בטה"; +"spaces_explore_rooms" = "חשוף חדרים"; +"ssl_fingerprint_hash" = "טביעת אצבע (%@):"; +"call_transfer_to_user" = "העבר ל %@"; +"call_consulting_with_user" = "התייעצות עם %@"; +"call_video_with_user" = "שיחת וידאו באמצעות %@"; +"call_voice_with_user" = "חיוג קולי באמצעות %@"; +"call_more_actions_dialpad" = "מסך חיוג"; +"call_more_actions_transfer" = "העבר"; +"call_more_actions_audio_use_device" = "מכשיר רמקול"; +"call_more_actions_change_audio_device" = "החלף התקן שמע"; +"call_more_actions_unhold" = "התחל שוב"; +"call_more_actions_hold" = "החזק"; +"call_holded" = "אתה מחזיק את השיחה"; +"call_remote_holded" = "%@ מחזיק את השיחה"; +"call_invite_expired" = "פג תוקף הזמנת שיחה"; +"incoming_voice_call" = "שיחת קול נכנסת"; +"incoming_video_call" = "שיחת וידאו נכנסת"; +"call_ended" = "שיחה הסתיימה"; +"call_ringing" = "מצלצל…"; + +// Settings keys + +// call string +"call_connecting" = "מתחבר…"; + +// gcm section +"settings_config_identity_server" = "זהות שרת:%@"; +"notification_settings_notify_all_other" = "עדכן עבור כל ההודעות/החדרים האחרים"; +"notification_settings_by_default" = "בברירת מחדל..."; +"notification_settings_suppress_from_bots" = "השתק התראות מבוטים"; +"notification_settings_receive_a_call" = "עדכן אותי כאשר אני מקבל שיחה"; +"notification_settings_people_join_leave_rooms" = "עדכן אותי כאשר אנשים מצטרפים או עוזבים חדרים"; +"notification_settings_invite_to_a_new_room" = "עדכן אותי כאשר אני מוזמן לחדר חדש"; +"notification_settings_just_sent_to_me" = "עדכן אותי עם צליל עבור הודעות שנשלחות רק אליי"; +"notification_settings_contain_my_display_name" = "עדכן אותי עם צליל לגבי הודעות הכוללות תצוגת שמי"; +"notification_settings_contain_my_user_name" = "עדכן אותי עם צליל לגבי הודעות הכוללות את שם המשתמש שלי"; +"notification_settings_other_alerts" = "התראות אחרות"; +"notification_settings_highlight" = "דבר חשוב"; +"notification_settings_global_info" = "התראת הגדרות עבור החשבון שלך נשמרה ותשותף עם כל ההתקנים שתומכים בה(כולל התראות מסך).\n\nחוקים יושמו; החוק הראשון שיותאם יגדיר את התוצאה שתישלח כהודעה.\nאז:התראות עבור מלה חשובות יותר מהתראות עבור חדר שחשובות יותר מהתראות שולח.\nעבור מספר חוקים מאותו סוג, הראשון ברשימה שיימצא מתאים יקבל את העדיפות."; +"notification_settings_per_word_notifications" = "התראות מלה"; +"notification_settings_enable_notifications_warning" = "ההתראות עבור כל ההתקנים נחסמו כעת."; +"notification_settings_enable_notifications" = "אפשר התראות"; + +// Notification settings screen +"notification_settings_disable_all" = "בטל את כל ההתראות"; +"settings_title_notifications" = "התראות"; + +// Settings screen +"settings_title_config" = "תצורה"; +"settings_integrations" = "התקנה"; +"settings_identity_server_settings" = "שרת הזדהות"; +"settings_discovery_settings" = "גלוי"; +"settings_calls_settings" = "שיחות"; +"account_logout_all" = "צא מכל החשבונות"; + +// Settings +"settings_title" = "הגדרות"; +"settings_ui_theme_picker_title" = "בחר נושא"; +"settings_ui_theme_black" = "שחור"; +"settings_ui_theme_dark" = "חשוך"; +"share_extension_low_quality_video_message" = "שלח ב%@ עבור איכות משופרת, או שלח באיכות נמוכה מזו."; +"share_extension_low_quality_video_title" = "וידאו יישלח באיכות נמוכה"; +"share_extension_failed_to_encrypt" = "שליחה נכשלה. בדוק באפליקציה הראשית את הגדרות ההצפנה עבור חדר זה"; + +// Share extension +"share_extension_auth_prompt" = "התחבר אל האפליקציה הראשית על מנת לחלוק תוכן"; +"room_widget_permission_room_id_permission" = "מספר חדר"; +"room_widget_permission_widget_id_permission" = "מספר יישומון"; +"room_widget_permission_theme_permission" = "שם הנושא"; +"room_widget_permission_user_id_permission" = "מספר המשתמש שלך"; +"room_widget_permission_avatar_url_permission" = "דמות ה URL שלך"; +"room_widget_permission_display_name_permission" = "שם התצוגה שלך"; +"search_no_result" = "אין תוצאות"; +"search_people_placeholder" = "חיפוש באמצעות מספר משתמש, שם או אימייל"; +"search_filter_placeholder" = "סינון"; +"search_default_placeholder" = "חפש"; +"secrets_recovery_with_key_information_verify_device" = "השתמש במפתח האבטחה שלך ע\"מ לאשר התקן זה."; +"secrets_recovery_with_key_information_default" = "היכנס אל ההודעות המאובטחות הישנות שלך ואל זיהוי החתימה שלך ע\"מ לאשר קישורים אחרים ע\"י הכנסת מפתח האבטחה שלך."; From ab1013a59623f1a4ed46ab866b49e2bda733b1de Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Mon, 14 Mar 2022 13:28:30 +0000 Subject: [PATCH 017/135] Translated using Weblate (Slovak) Currently translated at 100.0% (1871 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 453c52a42..1c0474721 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -644,7 +644,7 @@ "room_details_low_priority_tag" = "Nízka priorita"; "room_details_room_name" = "Názov miestnosti"; "room_details_photo" = "Obrázok miestnosti"; -"room_details_search" = "Hľadať miestnosť"; +"room_details_search" = "Prehľadať miestnosť"; "identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Napriek tomu sa odpojiť"; // Identity server settings @@ -2322,3 +2322,11 @@ "room_participants_leave_success" = "Opustil miestnosť"; "notice_error_unformattable_event" = "** Správa sa nedá zobraziť. Prosím, nahláste chybu"; "settings_labs_use_only_latest_user_avatar_and_name" = "Zobraziť posledný obrázok a meno používateľov v histórii správ"; +"room_participants_ago" = "pred"; +"notice_location_attachment" = "umiestnenie prílohy"; +"room_details_fail_to_update_room_direct" = "Nepodarilo sa aktualizovať priamy príznak tejto miestnosti"; +"room_details_flair_section" = "Zobraziť štýl pre komunity"; +"settings_flair" = "Zobraziť štýl, kde je to povolené"; +"room_participants_leave_processing" = "Opustenie"; +"joined" = "Sa pripojil/a"; +"callbar_return" = "Späť"; From e16d26cb9f174ee71d0e2af041e7eda01aa9d93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Sun, 13 Mar 2022 17:09:30 +0000 Subject: [PATCH 018/135] Translated using Weblate (Icelandic) Currently translated at 64.5% (1207 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/is/ --- Riot/Assets/is.lproj/Vector.strings | 1066 ++++++++++++++++++++++++++- 1 file changed, 1035 insertions(+), 31 deletions(-) diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index 2c53b14b6..e06bf258a 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -46,7 +46,7 @@ "auth_optional_phone_placeholder" = "Símanúmer (valfrjálst)"; "auth_phone_placeholder" = "Símanúmer"; "auth_repeat_password_placeholder" = "Endurtaka lykilorð"; -"auth_repeat_new_password_placeholder" = "Staðfestu nýtt lykilorð"; +"auth_repeat_new_password_placeholder" = "Staðfestu nýtt lykilorð á Matrix-aðganginn þinn"; "auth_invalid_login_param" = "Rangt notandanafn og/eða lykilorð"; "auth_invalid_user_name" = "Notandanöfn mega einungis innihalda bókstafi, tölustafi, punkta, bandstrik eða undirstrik"; "auth_invalid_password" = "Lykilorð er of stutt (lágmark 6 stafir)"; @@ -56,7 +56,7 @@ "auth_missing_email_or_phone" = "Vantar tölvupóstfang eða símanúmer"; "auth_password_dont_match" = "Lykilorðin stemma ekki"; "auth_username_in_use" = "Notandanafn er í notkun"; -"auth_forgot_password" = "Gleymt lykilorð?"; +"auth_forgot_password" = "Gleymt lykilorð á Matrix-notandaaðgangi?"; "auth_msisdn_validation_title" = "Sannvottun í bið"; "room_creation_account" = "Notandaaðgangur"; "room_creation_appearance" = "Útlit"; @@ -107,8 +107,8 @@ "room_participants_ago" = "síðan"; "room_participants_action_section_admin_tools" = "Kerfisstjóratól"; "room_participants_action_section_direct_chats" = "Beint spjall"; -"room_participants_action_section_devices" = "Tæki"; -"room_participants_action_section_other" = "Annað"; +"room_participants_action_section_devices" = "Setur"; +"room_participants_action_section_other" = "Valkostir"; "room_participants_action_invite" = "Bjóða"; "room_participants_action_leave" = "Fara af spjallrás"; "room_participants_action_remove" = "Fjarlægja úr þessari spjallrás"; @@ -129,10 +129,10 @@ "room_prompt_cancel" = "hætta við allt"; "room_event_action_copy" = "Afrita"; "room_event_action_quote" = "Tilvitnun"; -"room_event_action_redact" = "Ritstýra"; +"room_event_action_redact" = "Fjarlægja"; "room_event_action_more" = "Meira"; "room_event_action_share" = "Deila"; -"room_event_action_permalink" = "Varanlegur tengill"; +"room_event_action_permalink" = "Afrita tengil á skilaboð"; "room_event_action_view_source" = "Skoða frumkóða"; "room_event_action_save" = "Vista"; "room_event_action_resend" = "Endursenda"; @@ -157,10 +157,10 @@ "settings_calls_settings" = "SAMTÖL"; "settings_user_interface" = "NOTANDAVIÐMÓT"; "settings_ignored_users" = "HUNSAÐIR NOTENDUR"; -"settings_contacts" = "TENGILIÐIR Á TÆKI"; +"settings_contacts" = "TENGILIÐIR TÆKIS"; "settings_advanced" = "ÍTARLEGT"; -"settings_other" = "ANNAÐ"; -"settings_devices" = "TÆKI"; +"settings_other" = "Annað"; +"settings_devices" = "SETUR"; "settings_cryptography" = "DULRITUN"; "settings_sign_out" = "Skrá út"; "settings_sign_out_confirmation" = "Ertu viss?"; @@ -190,20 +190,20 @@ "settings_third_party_notices" = "Athugasemdir frá þriðja aðila"; "settings_send_crash_report" = "Senda nafnlausar hrunskýrslur og upplýsingar um notkun"; "settings_clear_cache" = "Hreinsa skyndiminni"; -"settings_change_password" = "Breyta lykilorði"; +"settings_change_password" = "Breyta Matrix-lykilorði"; "settings_old_password" = "eldra lykilorð"; "settings_new_password" = "nýtt lykilorð"; "settings_confirm_password" = "staðfestu lykilorð"; -"settings_fail_to_update_password" = "Mistókst að uppfæra lykilorð"; -"settings_password_updated" = "Lykilorðið þitt hefur verið uppfært"; -"settings_crypto_device_name" = "Heiti tækis: "; -"settings_crypto_device_id" = "\nAuðkenni tækis: "; -"settings_crypto_device_key" = "\nDulritunarlykill tækis: "; +"settings_fail_to_update_password" = "Mistókst að uppfæra Matrix-lykilorð"; +"settings_password_updated" = "Matrix-lykilorðið þitt hefur verið uppfært"; +"settings_crypto_device_name" = "Nafn á setu: "; +"settings_crypto_device_id" = "\nAuðkenni setu: "; +"settings_crypto_device_key" = "\nDulritunarlykill setu:\n"; "settings_crypto_export" = "Flytja út dulritunarlykla"; // Room Details "room_details_title" = "Nánar um spjallrás"; "room_details_people" = "Meðlimir"; -"room_details_files" = "Skrár"; +"room_details_files" = "Innsendingar"; "room_details_settings" = "Stillingar"; "room_details_photo" = "Mynd spjallrásar"; "room_details_room_name" = "Nafn spjallrásar"; @@ -258,8 +258,8 @@ // Call "call_incoming_voice_prompt" = "Innhringing raddsamtals frá %@"; "call_incoming_video_prompt" = "Innhringing myndsamtals frá %@"; -"call_incoming_voice" = "Innhringing..."; -"call_incoming_video" = "Innhringing myndsamtals..."; +"call_incoming_voice" = "Innhringing…"; +"call_incoming_video" = "Innhringing myndsamtals…"; // No VoIP support "no_voip_title" = "Innhringing"; // Bug report @@ -280,7 +280,7 @@ "auth_send_reset_email" = "Senda endurstillingarpóst"; "auth_return_to_login" = "Fara aftur í innskráningargluggann"; "auth_home_server_placeholder" = "URL-slóð (t.d. https://matrix.org)"; -"auth_identity_server_placeholder" = "URL-slóð (t.d. https://matrix.org)"; +"auth_identity_server_placeholder" = "URL-slóð (t.d. https://vector.im)"; "auth_invalid_email" = "Þetta lítur ekki út eins og gilt tölvupóstfang"; "auth_invalid_phone" = "Þetta lítur ekki út eins og gilt símanúmer"; "auth_email_in_use" = "Þetta tölvupóstfang er nú þegar í notkun"; @@ -313,7 +313,7 @@ "room_participants_action_set_moderator" = "Gera að umsjónarmanni"; "room_participants_action_mention" = "Minnst á"; // Chat -"room_jump_to_first_unread" = "Fara í fyrstu ólesin skilaboð"; +"room_jump_to_first_unread" = "Fara í ólesið"; "room_message_placeholder" = "Senda skilaboð (ódulrituð)…"; "room_do_not_have_permission_to_post" = "Þú hefur ekki heimild til að senda skilaboð á þessa spjallrás"; "room_message_short_placeholder" = "Senda skilaboð…"; @@ -327,9 +327,9 @@ "room_event_action_view_encryption" = "Dulritunarupplýsingar"; "room_event_failed_to_send" = "Mistókst að senda"; // Unknown devices -"unknown_devices_alert_title" = "Spjallrás inniheldur óþekkt tæki"; +"unknown_devices_alert_title" = "Spjallrás inniheldur óþekktar setur"; "unknown_devices_answer_anyway" = "Svara samt"; -"unknown_devices_title" = "Óþekkt tæki"; +"unknown_devices_title" = "Óþekktar setur"; // Room Preview "room_preview_invitation_format" = "Þér hefur verið boðið af %@ að taka þátt í þessari spjallrás"; "account_logout_all" = "Skrá út af öllum notandaaðgöngum"; @@ -349,10 +349,10 @@ "room_participants_invited_section" = "BOÐIÐ"; "room_details_access_section" = "Hver hefur aðgang að þessari spjallrás?"; "room_details_advanced_e2e_encryption_enabled" = "Dulritun er virk í þessari spjallrás"; -"e2e_room_key_request_start_verification" = "Hefja sannvottun..."; +"e2e_room_key_request_start_verification" = "Hefja sannvottun…"; "e2e_room_key_request_share_without_verifying" = "Deila án sannvottunar"; "room_creation_appearance_picture" = "Mynd spjalls (valfrjálst)"; -"room_creation_invite_another_user" = "Leita/Bjóða eftir notandaauðkenni, nafni eða tölvupóstfangi"; +"room_creation_invite_another_user" = "Notandaauðkenni, nafn eða tölvupóstfang"; // Room recents "room_recents_directory_section" = "SKRÁ YFIR SPJALLRÁSIR"; "directory_searching_title" = "Leita í yfirlitsskrá.…"; @@ -370,7 +370,7 @@ "room_creation_wait_for_creation" = "Verið er að útbúa spjallrás. Bíddu aðeins."; "room_recents_join_room_prompt" = "Settu inn auðkenni eða samheiti spjallrásar"; "room_participants_invite_another_user" = "Leita/Bjóða eftir notandauðkenni, nafni eða tölvupóstfangi"; -"room_event_action_kick_prompt_reason" = "Ástæða fyrir að sparka þessum notanda"; +"room_event_action_kick_prompt_reason" = "Ástæða fyrir að fjarlægja þennan notanda"; "room_event_action_ban_prompt_reason" = "Ástæða fyrir að banna þennan notanda"; "settings_unignore_user" = "Sýna öll skilaboð frá %@?"; "settings_labs_create_conference_with_jitsi" = "Búa til símafundi með Jitsi"; @@ -389,19 +389,19 @@ "auth_add_email_phone_message" = "Bættu tölvupóstfangi og/eða símanúmeri við notandaaðganginn þinn til að gera öðrum notendum kleift að finna þig. Tölvupóstfang nýtist einnig til að endurstilla lykilorðið þitt."; "auth_add_email_and_phone_message" = "Bættu tölvupóstfangi og símanúmeri við notandaaðganginn þinn til að gera öðrum notendum kleift að finna þig. Tölvupóstfang nýtist einnig til að endurstilla lykilorðið þitt."; "auth_recaptcha_message" = "Þessi heimavefþjónn vill ganga úr skugga um að þú sért ekki vélmenni"; -"auth_reset_password_message" = "Til að endursetja lykilorðið þitt, settu þá inn tölvupóstfangið sem tengt er notandaaðgangnum þínum:"; +"auth_reset_password_message" = "Til að endursetja Matrix-lykilorðið þitt, settu þá inn tölvupóstfangið sem tengt er notandaaðgangnum þínum:"; "auth_reset_password_missing_email" = "Það þarf að setja inn tölvupóstfangið sem tengt er notandaaðgangnum þínum."; "auth_reset_password_email_validation_message" = "Tölvupóstur hefur verið sendur á %@ Þegar þú ert búin/n að fylgja tenglinum sem sá póstur inniheldur, smelltu þá hér fyrir neðan."; "auth_reset_password_error_unauthorized" = "Gat ekki sannprófað tölvupóstfang: gakktu úr skugga um að þú hafir smellt á tengilinn í tölvupóstinum"; -"auth_reset_password_success_message" = "Lykilorðið þitt hefur verið endurstillt.\n\nÞú hefur verið skráður út af öllum tækjum og munt ekki lengur fá ýti-tilkynningar. Til að endurvirkja tilkynningar, þarf að skrá sig aftur inn á hverju tæki fyrir sig."; +"auth_reset_password_success_message" = "Matrix-ykilorðið þitt hefur verið endurstillt.\n\nÞú hefur verið skráður út af öllum tækjum og munt ekki lengur fá ýti-tilkynningar. Til að endurvirkja tilkynningar, þarf að skrá sig aftur inn á hverju tæki fyrir sig."; "directory_search_results" = "%tu niðurstöður fundust fyrir %@"; "directory_search_results_more_than" = ">%tu niðurstöður fundust fyrir %@"; -"contacts_address_book_permission_denied" = "Þú heimilaðir Element ekki aðgang að tengiliðum á tækinu"; +"contacts_address_book_permission_denied" = "Þú heimilaðir %@ ekki aðgang að tengiliðum á tækinu"; "room_participants_remove_prompt_msg" = "Ertu viss um að þú viljir fjarlægja %@ úr þessu spjalli?"; "room_participants_invite_prompt_msg" = "Ertu viss um að þú viljir bjóða %@ á þetta spjall?"; "room_participants_invite_malformed_id" = "Rangt formað auðkenni. Ætti að vera tölvupóstfang eða Matrix-auðkenni á borð við'@sérheiti:lén'"; "room_unsent_messages_notification" = "Skilaboð ekki send."; -"room_unsent_messages_unknown_devices_notification" = "Skilaboð ekki send vegna þess að vart var við óþekkt tæki."; +"room_unsent_messages_unknown_devices_notification" = "Skilaboð ekki send vegna þess að vart var við óþekkta setu."; "room_ongoing_conference_call" = "Símafundur í gangi. Taka þátt með %@ eða %@."; "room_ongoing_conference_call_with_close" = "Símafundur í gangi. Taka þátt með %@ eða %@. %@ því."; "room_conference_call_no_power" = "Þú þarft aðgangsheimildir til að sýsla með símafundi á þessari spjallrás"; @@ -476,11 +476,1015 @@ "widget_integration_room_not_recognised" = "Spjallrás er ekki þekkt."; // Share extension "share_extension_auth_prompt" = "Skráðu inn í aðalforrit til að deila efni"; -"e2e_room_key_request_message_new_device" = "Þú bættir við nýju tæki '%@', sem er að krefjast dulritunarlykla."; -"e2e_room_key_request_message" = "Ósannvottaða tækið þitt '%@' er að krefjast dulritunarlykla."; +"e2e_room_key_request_message_new_device" = "Þú bættir við nýrri setu '%@', sem er að krefjast dulritunarlykla."; +"e2e_room_key_request_message" = "Ósannvottaða setan þín '%@' er að krefjast dulritunarlykla."; // MARK: - MatrixKit + +"select_account" = "Veldu notandaaðgang"; +"login_error_resource_limit_exceeded_contact_button" = "Hafðu samband við kerfisstjóra"; +"register_error_title" = "Nýskráning mistókst"; +"poll_timeline_vote_not_registered_title" = "Atkvæði ekki skráð"; +"poll_timeline_total_final_results" = "Lokaniðurstöður byggðar á %lu atkvæðum"; +"poll_timeline_total_final_results_one_vote" = "Lokaniðurstöður byggðar á 1 atkvæði"; +"poll_timeline_total_votes_not_voted" = "%lu atkvæði greidd. Greiddu atkvæði til að sjá útkomuna"; +"poll_timeline_total_one_vote_not_voted" = "1 atkvæði greitt. Greiddu atkvæði til að sjá útkomuna"; +"poll_timeline_total_votes" = "%lu atkvæði greidd"; +"poll_timeline_votes_count" = "%lu atkvæði"; +"poll_edit_form_update_failure_title" = "Mistókst að uppfæra könnun"; +"poll_edit_form_option_number" = "Valkostur %lu"; +"user_avatar_view_accessibility_hint" = "Skipta um auðkennismynd notanda"; +"leave_space_and_all_rooms_action" = "Yfirgefa allar spjallrásir og svæði"; +"room_intro_cell_information_room_sentence1_part3" = ". "; + +// MARK: - Favourites + +"favourites_empty_view_title" = "Eftirlætisspjallrásir og fólk"; +"home_context_menu_normal_priority" = "Venjulegur forgangur"; +"home_context_menu_make_room" = "Færa í Spjallrásir"; +"home_context_menu_make_dm" = "Færa í Fólk"; + +// MARK: - Home + +"home_empty_view_title" = "Velkomin í %@,\n%@"; +"create_room_type_private" = "Einkaspjallrás"; +"create_room_section_header_type" = "Tegund spjallrásar"; +"create_room_section_header_encryption" = "Dulritun spjallrásar"; +"searchable_directory_search_placeholder" = "Nafn eða auðkenni"; +"biometrics_cant_unlocked_alert_message_login" = "Skráðu þig aftur inn"; +"biometrics_mode_face_id" = "Face ID"; + +// MARK: - Biometrics Protection + +"biometrics_mode_touch_id" = "Touch ID"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN-númer"; +"secrets_setup_recovery_key_storage_alert_title" = "Haltu þessu öruggu"; +"secrets_recovery_with_key_recover_action" = "Nota lykil"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; + +// Session details + +"user_verification_session_details_trusted_title" = "Treyst"; +"user_verification_start_waiting_partner" = "Bíð eftir %@…"; +"room_multiple_typing_notification" = "%@ auk annarra"; +"room_resource_usage_limit_reached_message_contact_3" = " til að fá þessi takmörk hækkuð."; +"room_resource_usage_limit_reached_message_2" = "að sumir notendur munu ekki geta skráð sig inn."; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "Þessi heimaþjónn er kominn fram yfir takmörk á mánaðarlega virkum notendum þannig "; +"room_resource_usage_limit_reached_message_1_default" = "Þessi heimaþjónn er kominn fram yfir takmörk á tilföngum sínum þannig "; +"room_resource_limit_exceeded_message_contact_3" = " til að halda áfram að nota þessa þjónustu."; +"room_resource_limit_exceeded_message_contact_1" = " Endilega "; +"room_predecessor_link" = "Ýttu hér til að sjá eldri skilaboð."; +"room_action_send_file" = "Senda skrá"; +"room_action_send_photo_or_video" = "Senda ljósmynd eða myndskeið"; +"room_event_action_delete_confirmation_message" = "Ertu viss um að þú viljir eyða þessum ósendu skilaboðum?"; +"room_event_action_delete_confirmation_title" = "Eyða ósendum skilaboðum"; +"room_message_reply_to_short_placeholder" = "Senda svar…"; +"room_message_replying_to" = "Svara %@"; +"room_message_unable_open_link_error_message" = "Gat ekki opnað tengilinn."; +"room_message_reply_to_placeholder" = "Senda svar (án dulritunar)…"; +"room_accessiblity_scroll_to_bottom" = "Skruna neðst"; + +// MARK: - Chat + +"room_slide_to_end_group_call" = "Renna til að ljúka símtalinu hjá öllum"; +"room_member_power_level_custom_in" = "Sérsniðið (%@) í %@"; +"room_member_power_level_moderator_in" = "Umsjónarmaður í %@"; +"room_member_power_level_admin_in" = "Stjórnandi í %@"; +"room_participants_security_information_room_not_encrypted_for_dm" = "Skilaboð hér eru ekki enda-í-enda dulrituð."; +"room_participants_filter_room_members_for_dm" = "Sía meðlimi"; +"room_participants_remove_third_party_invite_prompt_msg" = "Ertu viss um að þú viljir afturkalla þetta boð?"; +"room_participants_leave_success" = "Fór af spjallrásinni"; +"room_participants_leave_processing" = "Fer út"; +"room_participants_leave_prompt_msg_for_dm" = "Ertu viss um að þú viljir fara?"; +"ssl_could_not_verify" = "Gat ekki sannreynt auðkenni fjartengds þjóns."; +"ssl_fingerprint_hash" = "Fingrafar (%@):"; +"ssl_remain_offline" = "Hunsa"; +"ssl_logout_account" = "Útskráning"; + +// unrecognized SSL certificate +"ssl_trust" = "Treysta"; +"call_transfer_to_user" = "Færa á %@"; +"call_video_with_user" = "Myndsamtal við %s@"; +"call_voice_with_user" = "Raddsamtal við %@"; +"call_more_actions_dialpad" = "Talnaborð"; +"call_more_actions_transfer" = "Flutningur"; +"call_more_actions_audio_use_device" = "Hátalari tækis"; +"call_more_actions_change_audio_device" = "Skipta um hljóðtæki"; +"call_more_actions_unhold" = "Halda áfram"; +"call_more_actions_hold" = "Bíða"; +"call_holded" = "Þú settir símtalið í bið"; +"call_remote_holded" = "%@ setti símtalið í bið"; +"incoming_voice_call" = "Innhringing raddsamtals"; +"incoming_video_call" = "Innhringing myndsamtals"; +"call_ended" = "Símtali lokið"; +"call_ringing" = "Hringing…"; + +// Settings keys + +// call string +"call_connecting" = "Tengist…"; +"notification_settings_notify_all_other" = "Senda tilkynningar fyrir öll önnur skilaboð/spjallrásir"; +"notification_settings_by_default" = "Sjálfgefið..."; +"notification_settings_other_alerts" = "Aðrar aðvaranir"; +"notification_settings_select_room" = "Veldu spjallrás"; +"notification_settings_custom_sound" = "Sérsniðið hljóð"; +"notification_settings_highlight" = "Áherslulita"; +"notification_settings_never_notify" = "Aldrei láta vita"; +"notification_settings_always_notify" = "Alltaf láta vita"; +"notification_settings_enable_notifications" = "Virkja tilkynningar"; +"settings_title_notifications" = "Tilkynningar"; + +// Settings screen +"settings_title_config" = "Uppsetning"; +"login_error_must_start_http" = "Slóð verður að byrja á http[s]://"; + +// Login Screen +"login_error_already_logged_in" = "Nú þegar innskráð(ur)"; +"ban" = "Banna"; + +/* -*- + Automatic localization for en + + The following key/value pairs were extracted from the android i18n file: + /matrix-sdk/src/main/res/values/strings.xml. +*/ + +"notice_room_invite" = "%@ bauð %@"; +"language_picker_default_language" = "Sjálfgefið (%@)"; + +// Language picker +"language_picker_title" = "Veldu tungumál"; + +// Country picker +"country_picker_title" = "Veldu land"; +"ssl_homeserver_url" = "URL-slóð heimaþjóns: %@"; +"power_level" = "Valdastig"; +"public" = "Opinbert"; +"private" = "Einka"; +"default" = "sjálfgefið"; +"not_supported_yet" = "Ekki stutt ennþá"; +"error" = "Villa"; +"offline" = "aftengdur"; + +// Others +"user_id_title" = "Notandaauðkenni:"; +"e2e_passphrase_create" = "Búa til lykilsetningu"; +"e2e_passphrase_not_match" = "Lykilfrasar verða að stemma"; +"e2e_passphrase_empty" = "Lykilfrasi má ekki vera auður"; +"e2e_passphrase_confirm" = "Staðfestu lykilfrasa"; +"e2e_export" = "Flytja út"; + +// E2E export +"e2e_export_room_keys" = "Flytja út dulritunarlykla spjallrásar"; +"e2e_passphrase_enter" = "Settu inn lykilfrasann"; +"e2e_import" = "Flytja inn"; + +// E2E import +"e2e_import_room_keys" = "Flytja inn dulritunarlykla spjallrásar"; +"format_time_d" = "d"; +"format_time_h" = "klst"; +"format_time_m" = "mín"; + +// Time +"format_time_s" = "sek"; + +// Groups + +// Search +"search_no_results" = "Engar niðurstöður"; +"contact_local_contacts" = "Tengiliðir á tæki"; +"attachment_unsupported_preview_message" = "Þessi skráategund er ekki studd."; +"attachment_e2e_keys_import" = "Flytja inn..."; +"attachment_multiselection_original" = "Raunstærð"; +"attachment_cancel_upload" = "Hætta við innsendinguna?"; +"attachment_cancel_download" = "Hætta við niðurhalið?"; +"attachment_large_with_resolution" = "Stórt %@ (~%@)"; +"attachment_medium_with_resolution" = "Miðlungs %@ (~%@)"; +"attachment_small_with_resolution" = "Lítið %@ (~%@)"; +"attachment_large" = "Stórt (~%@)"; +"attachment_medium" = "Miðlungs (~%@)"; +"attachment_small" = "Lítið (~%@)"; +"attachment_original" = "Raunstærð (%@)"; +"room_member_power_level_prompt" = "Þú getur ekki afturkallað þessa aðgerð, þar sem þú ert að gefa notandanum jafn mikil völd og þú hefur sjálf/ur.\nErtu alveg viss?"; +"message_reply_to_message_to_reply_to_prefix" = "Sem svar til"; +"message_reply_to_sender_sent_their_location" = "hefur deilt staðsetningu sinni."; +"message_reply_to_sender_sent_a_file" = "sendi skrá."; +"message_reply_to_sender_sent_a_voice_message" = "sendi talskilaboð."; +"message_reply_to_sender_sent_an_audio_file" = "sendi hljóðskrá."; +"message_reply_to_sender_sent_a_video" = "sendi myndskeið."; + +// Reply to message +"message_reply_to_sender_sent_an_image" = "sendi mynd."; +"room_no_conference_call_in_encrypted_rooms" = "Símafundir eru ekki studdir í dulrituðum spjallrásum"; +"room_no_power_to_create_conference_call" = "Þú þarft aðgangsheimildir til að bjóða til símafundar á þessari spjallrás"; +"room_left" = "Þú hættir í spjallrásinni"; +"room_error_timeline_event_not_found_title" = "Mistókst að hlaða inn staðsetningu á tímalínu"; +"room_error_join_failed_title" = "Mistókst að taka þátt í spjallrás"; +"room_creation_participants_title" = "Þátttakendur:"; +"room_creation_alias_title" = "Samnefni spjallrásar:"; + +// Room creation +"room_creation_name_title" = "Heiti spjallrásar:"; +"account_error_msisdn_wrong_title" = "Ógilt símanúmer"; +"account_error_email_wrong_description" = "Þetta lítur ekki út eins og gilt tölvupóstfang"; +"account_error_email_wrong_title" = "Ógilt tölvupóstfang"; +"account_msisdn_validation_error" = "Ekki var hægt að sannreyna símanúmer."; +"account_msisdn_validation_message" = "Við höfum sent SMS-skilaboð með virkjunarkóða. Settu þennan kóða inn hér fyrir neðan."; +"account_msisdn_validation_title" = "Sannvottun í bið"; +"account_email_validation_error" = "Tókst ekki að sannreyna tölvupóstfang. Skoðaðu tölvupóstinn þinn og smelltu á tengilinn sem hann inniheldur. Þegar því er lokið skaltu smella á að halda áfram"; +"account_email_validation_message" = "Skoðaðu tölvupóstinn þinn og smelltu á tengilinn sem hann inniheldur. Þegar því er lokið skaltu smella á að halda áfram."; +"account_email_validation_title" = "Sannvottun í bið"; + +// Account +"account_save_changes" = "Vista breytingar"; +"room_event_encryption_verify_ok" = "Sannreyna"; +"room_event_encryption_info_unblock" = "Taka af bannlista"; +"room_event_encryption_info_block" = "Bannlisti"; +"room_event_encryption_info_unverify" = "Afturkalla sannvottun"; +"room_event_encryption_info_verify" = "Sannreyna..."; +"room_event_encryption_info_device_blocked" = "Á bannlista"; +"room_event_encryption_info_device_not_verified" = "EKKI sannreynt"; +"room_event_encryption_info_device_verified" = "Sannreynt"; +"room_event_encryption_info_event_none" = "ekkert"; +"room_event_encryption_info_event_unencrypted" = "ódulritað"; +"device_details_delete_prompt_message" = "Þessi aðgerð krefst viðbótar-auðkenningar.\nTil að halda áfram skaltu setja inn lykilorðið þitt."; +"device_details_delete_prompt_title" = "Auðkenning"; +"device_details_rename_prompt_title" = "Nafn á setu"; +"device_details_last_seen_format" = "%@ @ %@\n"; +"notification_settings_room_rule_title" = "Spjallrás: '%@'"; + +// Settings +"settings" = "Stillingar"; +"room_displayname_all_other_members_left" = "%@ (fór)"; +"room_displayname_two_members" = "%@ og %@"; + +// room display name +"room_displayname_empty_room" = "Tóm spjallrás"; +"notice_in_reply_to" = "Sem svar til"; +"notice_sticker" = "límmerki"; +"notice_encrypted_message" = "Dulrituð skilaboð"; +"notice_room_created_for_dm" = "%@ gekk í hópinn."; +"notice_event_redacted_reason" = " [ástæða: %@]"; +"notice_event_redacted_by" = " eftir %@"; + +// Events formatter +"notice_avatar_changed_too" = "(einnig var skipt um auðkennismynd)"; +"unignore" = "Byrja að fylgjast með á ný"; +"ignore" = "Hunsa"; +"resume_call" = "Halda áfram"; +"end_call" = "Ljúka símtali"; +"answer_call" = "Svara símtali"; +"show_details" = "Sýna ítarlegri upplýsingar"; +"cancel_download" = "Hætta við niðurhal"; +"cancel_upload" = "Hætta við innsendingu"; +"select_all" = "Velja allt"; +"reset_to_default" = "Frumstilla á sjálfgefið"; +"mention" = "Minnst á"; +"start_video_call" = "Hefja myndsamtal"; +"start_voice_call" = "Hefja raddsamtal"; +"start_chat" = "Hefja spjall"; +"submit" = "Senda inn"; +"sign_up" = "Nýskrá"; +"dismiss" = "Hunsa"; +"discard" = "Henda"; +"abort" = "Hætta"; +"yes" = "Já"; + +// Action +"no" = "Nei"; +"login_error_resource_limit_exceeded_message_monthly_active_user" = "Þessi heimaþjónn er kominn fram yfir takmörk á mánaðarlega virkum notendum."; +"login_error_resource_limit_exceeded_message_default" = "Þessi heimaþjónn er kominn fram yfir takmörk á tilföngum sínum."; +"login_desktop_device" = "Einkatölva"; +"login_tablet_device" = "Spjaldtölva"; +"login_mobile_device" = "Farsími"; +"login_invalid_param" = "Ógild færibreyta"; +"login_leave_fallback" = "Hætta við"; +"login_error_login_email_not_yet" = "Tölvupósttengill sem ekki er enn búið að smella á"; +"login_error_user_in_use" = "Þetta notandanafn er þegar í notkun"; +"login_error_limit_exceeded" = "Of margar beiðnir hafa verið sendar"; +"login_error_not_json" = "Inniheldur ekki gilt JSON"; +"login_error_bad_json" = "Gallað JSON"; +"login_error_unknown_token" = "Tiltekið aðgangsteikn þekktist ekki"; +"login_error_forbidden" = "Ótækt notandanafn/lykilorð"; +"login_error_title" = "Innskráning mistókst"; +"login_email_placeholder" = "Tölvupóstfang"; +"login_optional_field" = "valkvætt"; +"login_password_placeholder" = "Lykilorð"; +"login_identity_server_title" = "Slóð á auðkennisþjón:"; +"login_home_server_title" = "Slóð á heimaþjón:"; +"login_server_url_placeholder" = "URL-slóð (t.d. https://matrix.org)"; + +// Login Screen +"login_create_account" = "Búa til notandaaðgang:"; + + +// MARK: - MatrixKit + + +/* *********************** */ +/* iOS specific */ +/* *********************** */ + +"matrix" = "Matrix"; +"location_sharing_settings_toggle_title" = "Virkja deilingu staðsetninga"; +"location_sharing_settings_header" = "Deiling staðsetninga"; +"location_sharing_open_open_street_maps" = "Opna í OpenStreetMap"; +"location_sharing_open_google_maps" = "Opna í Google Maps"; +"location_sharing_open_apple_maps" = "Opna í Apple Maps"; +"location_sharing_invalid_authorization_settings" = "Stillingar"; +"location_sharing_invalid_authorization_not_now" = "Ekki núna"; +"location_sharing_share_action" = "Deila"; +"location_sharing_close_action" = "Loka"; + +// MARK: - Location sharing + +"location_sharing_title" = "Staðsetning"; +"poll_timeline_not_closed_subtitle" = "Endilega reyndu aftur"; +"poll_timeline_not_closed_title" = "Mistókst að ljúka könnun"; +"poll_timeline_total_one_vote" = "1 atkvæði greitt"; +"poll_timeline_total_no_votes" = "Engin atkvæði greidd"; +"poll_timeline_one_vote" = "1 atkvæði"; +"poll_edit_form_poll_type_closed_description" = "Niðurstöður birtast einungis eftir að þú hefur lokað könnuninni"; +"poll_edit_form_poll_type_closed" = "Lokuð könnun"; +"poll_edit_form_poll_type_open_description" = "Kjósendur sjá niðurstöðurnar þegar þeir hafa kosið"; +"poll_edit_form_poll_type_open" = "Opna könnun"; +"poll_edit_form_update_failure_subtitle" = "Endilega reyndu aftur"; +"poll_edit_form_post_failure_subtitle" = "Endilega reyndu aftur"; +"poll_edit_form_post_failure_title" = "Mistókst að birta könnun"; +"poll_edit_form_add_option" = "Bæta við valkosti"; +"poll_edit_form_create_options" = "Búa til valkosti"; +"poll_edit_form_input_placeholder" = "Skrifaðu eitthvað"; +"poll_edit_form_question_or_topic" = "Spurning eða viðfangsefni"; +"poll_edit_form_poll_question_or_topic" = "Spurning eða viðfangsefni könnunar"; +"poll_edit_form_poll_type" = "Tegund könnunar"; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Búa til könnun"; +"version_check_modal_action_title_supported" = "Náði því"; +"voice_message_lock_screen_placeholder" = "Talskilaboð"; +"voice_message_remaining_recording_time" = "%@s fór út"; +"side_menu_app_version" = "Útgáfa %@"; +"side_menu_action_feedback" = "Umsagnir"; +"side_menu_action_help" = "Hjálp"; +"side_menu_action_settings" = "Stillingar"; +"side_menu_action_invite_friends" = "Bjóða vinum"; + +// Mark: - Side menu + +"side_menu_reveal_action_accessibility_label" = "Vinstra spjald"; + +// Mark: - User avatar view + +"user_avatar_view_accessibility_label" = "auðkennismynd"; +"space_avatar_view_accessibility_hint" = "Skipta um táknmynd svæðis"; + +// Mark: Avatar + +"space_avatar_view_accessibility_label" = "auðkennismynd"; +"space_public_join_rule" = "Opinbert svæði"; +"space_private_join_rule" = "Einkasvæði"; +"space_home_show_all_rooms" = "Sýna allar spjallrásir"; +"spaces_coming_soon_title" = "Kemur bráðum"; +"spaces_no_result_found_title" = "Engar niðurstöður fundust"; +"space_tag" = "bil"; +"spaces_suggested_room" = "Tillögur"; +"spaces_explore_rooms" = "Kanna spjallrásir"; +"leave_space_only_action" = "Ekki yfirgefa neinar spjallrásir"; +"leave_space_title" = "Fara út úr %@"; +"spaces_left_panel_title" = "Svæði (Spaces)"; +"spaces_home_space_title" = "Forsíða"; +"space_beta_announce_badge" = "BETA-prófunarútgáfa"; +"room_intro_cell_information_room_with_topic_sentence2" = "Umfjöllunarefni: %@"; + +// Mark: - Room creation introduction cell + +"room_intro_cell_add_participants_action" = "Bæta við fólki"; +"room_avatar_view_accessibility_hint" = "Skipta um auðkennismynd spjallrásar"; + +// Mark: - Room avatar view + +"room_avatar_view_accessibility_label" = "auðkennismynd"; +"home_syncing" = "Samstilli"; +"home_context_menu_leave" = "Fara út"; +"home_context_menu_low_priority" = "Lítill forgangur"; +"home_context_menu_unfavourite" = "Fjarlægja úr eftirlætum"; +"home_context_menu_favourite" = "Eftirlæti"; +"home_context_menu_unmute" = "Kveikja á hljóði"; +"home_context_menu_mute" = "Þagga hljóð"; +"home_context_menu_notifications" = "Tilkynningar"; +"call_transfer_error_title" = "Villa"; +"call_transfer_contacts_all" = "Allt"; +"call_transfer_contacts_recent" = "Nýlegt"; +"call_transfer_dialpad" = "Talnaborð"; +"call_transfer_users" = "Notendur"; + +// MARK: - Call Transfer +"call_transfer_title" = "Flutningur"; + +// MARK: - Dial Pad +"dialpad_title" = "Talnaborð"; +"room_info_list_section_other" = "Annað"; +"room_info_list_several_members" = "%@ meðlimir"; + +// MARK: - Room Info + +"room_info_list_one_member" = "1 meðlimur"; +"create_room_section_header_address" = "Vistfang spjallrásar"; +"create_room_type_public" = "Almenningsspjallrás"; +"create_room_enable_encryption" = "Þvinga dulritun"; +"create_room_placeholder_topic" = "Umfjöllunarefni"; +"key_verification_tile_request_incoming_approval_decline" = "Hafna"; +"key_verification_tile_request_incoming_approval_accept" = "Samþykkja"; +"key_verification_tile_request_status_cancelled" = "Hætt við %@"; +"key_verification_tile_request_status_expired" = "Útrunnið"; +"key_verification_tile_request_status_waiting" = "Bíð…"; + +// MARK: - Key Verification + +"key_verification_bootstrap_not_setup_title" = "Villa"; + +// MARK: Reaction history +"reaction_history_title" = "Viðbrögð"; +"emoji_picker_flags_category" = "Fánar"; +"emoji_picker_symbols_category" = "Tákn"; +"emoji_picker_objects_category" = "Hlutir"; +"emoji_picker_places_category" = "Ferðalög og staðir"; +"emoji_picker_activity_category" = "Starfsemi"; +"emoji_picker_foods_category" = "Mat og drykkur"; +"emoji_picker_nature_category" = "Dýr og náttúra"; +"emoji_picker_people_category" = "Broskarlar og fólk"; + +// MARK: Emoji picker +"emoji_picker_title" = "Viðbrögð"; +"file_upload_error_unsupported_file_type_message" = "Skráartegundin er ekki studd."; + +// MARK: File upload +"file_upload_error_title" = "Hlaða upp skrá"; +"device_verification_emoji_pin" = "Pinni"; +"device_verification_emoji_folder" = "Mappa"; +"device_verification_emoji_headphones" = "Heyrnartól"; +"device_verification_emoji_anchor" = "Akkeri"; +"device_verification_emoji_bell" = "Bjalla"; +"device_verification_emoji_trumpet" = "Trompet"; +"device_verification_emoji_guitar" = "Gítar"; +"device_verification_emoji_ball" = "Bolti"; +"device_verification_emoji_trophy" = "Verðlaun"; +"device_verification_emoji_rocket" = "Eldflaug"; +"device_verification_emoji_aeroplane" = "Flugvél"; +"device_verification_emoji_bicycle" = "Reiðhjól"; +"device_verification_emoji_train" = "Lest"; +"device_verification_emoji_flag" = "Flagg"; +"device_verification_emoji_telephone" = "Sími"; +"device_verification_emoji_hammer" = "Hamar"; +"device_verification_emoji_key" = "Lykill"; +"device_verification_emoji_lock" = "Lás"; +"device_verification_emoji_scissors" = "Skæri"; +"device_verification_emoji_paperclip" = "Bréfaklemma"; +"device_verification_emoji_pencil" = "Blýantur"; +"device_verification_emoji_book" = "Bók"; +"device_verification_emoji_light bulb" = "Ljósapera"; +"device_verification_emoji_gift" = "Gjöf"; +"device_verification_emoji_clock" = "Klukka"; +"device_verification_emoji_hourglass" = "Stundaglas"; +"device_verification_emoji_umbrella" = "Regnhlíf"; +"device_verification_emoji_thumbs up" = "Þumlar upp"; +"device_verification_emoji_santa" = "Jólasveinn"; +"device_verification_emoji_spanner" = "Skrúflykill"; +"device_verification_emoji_glasses" = "Gleraugu"; +"device_verification_emoji_hat" = "Hattur"; +"device_verification_emoji_robot" = "Vélmenni"; +"device_verification_emoji_smiley" = "Broskall"; +"device_verification_emoji_heart" = "Hjarta"; +"device_verification_emoji_cake" = "Kökur"; +"device_verification_emoji_pizza" = "Flatbökur"; +"device_verification_emoji_corn" = "Maís"; +"device_verification_emoji_strawberry" = "Jarðarber"; +"device_verification_emoji_apple" = "Epli"; +"device_verification_emoji_banana" = "Banani"; +"device_verification_emoji_fire" = "Eldur"; +"device_verification_emoji_cloud" = "Ský"; +"device_verification_emoji_moon" = "Tungl"; +"device_verification_emoji_globe" = "Hnöttur"; +"device_verification_emoji_mushroom" = "Sveppur"; +"device_verification_emoji_cactus" = "Kaktus"; +"device_verification_emoji_tree" = "Tré"; +"device_verification_emoji_flower" = "Blóm"; +"device_verification_emoji_butterfly" = "Fiðrildi"; +"device_verification_emoji_octopus" = "Kolkrabbi"; +"device_verification_emoji_fish" = "Fiskur"; +"device_verification_emoji_turtle" = "Skjaldbaka"; +"device_verification_emoji_penguin" = "Mörgæs"; +"device_verification_emoji_rooster" = "Hani"; +"device_verification_emoji_panda" = "Pandabjörn"; +"device_verification_emoji_rabbit" = "Kanína"; +"device_verification_emoji_elephant" = "Fíll"; +"device_verification_emoji_pig" = "Svín"; +"device_verification_emoji_unicorn" = "Einhyrningur"; +"device_verification_emoji_horse" = "Hestur"; +"device_verification_emoji_lion" = "Ljón"; +"device_verification_emoji_cat" = "Köttur"; + +// MARK: Emoji +"device_verification_emoji_dog" = "Hundur"; +"device_verification_verified_got_it_button" = "Náði því"; + +// MARK: Verified + +// Device + +"device_verification_verified_title" = "Sannreynt!"; +"key_verification_manually_verify_device_validate_action" = "Sannreyna"; +"key_verification_manually_verify_device_additional_information" = "Ef þetta samsvarar ekki, getur verið að samskiptin þín séu berskjölduð."; +"key_verification_manually_verify_device_key_title" = "Dulritunarlykill setu"; +"key_verification_manually_verify_device_id_title" = "Auðkenni setu"; +"key_verification_manually_verify_device_name_title" = "Nafn á setu"; +"key_verification_manually_verify_device_instruction" = "Staðfestu með því að bera eftirfarandi saman við 'Stillingar notanda' í hinni setunni þinni:"; + +// MARK: Manually Verify Device + +"key_verification_manually_verify_device_title" = "Sannreyna handvirkt með textaskilaboðum"; +"key_verification_verify_sas_validate_action" = "Þau samsvara"; +"key_verification_verify_sas_cancel_action" = "Þau samsvara ekki"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Nota öryggislykil"; +"key_verification_self_verify_unverified_sessions_alert_validate_action" = "Yfirfara"; +"key_verification_self_verify_current_session_alert_validate_action" = "Sannreyna"; +"key_verification_self_verify_current_session_alert_message" = "Aðrir notendur gætu ekki treyst því."; + +// Current session + +"key_verification_self_verify_current_session_alert_title" = "Sannprófa þessa setu"; +"device_verification_self_verify_start_waiting" = "Bíð…"; +"device_verification_self_verify_start_verify_action" = "Hefja sannvottun"; +"device_verification_self_verify_alert_validate_action" = "Sannreyna"; + +// MARK: Self verification start + +// New login +"device_verification_self_verify_alert_title" = "Ný innskráning. Varst þetta þú?"; +"device_verification_cancelled" = "Hinn aðilinn hætti við sannvottunina."; +"key_verification_this_session_title" = "Sannprófa þessa setu"; + +// MARK: - Device Verification +"key_verification_other_session_title" = "Sannprófa setu"; +"sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "Ég vil ekki dulrituðu skilaboðin mín"; +"sign_out_key_backup_in_progress_alert_title" = "Öryggisafritun dulritunarlykla í gangi. Þú munt tapa dulrituðu skilaboðunum þínum ef þú skráir þig út núna."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Öryggisafrit"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "Skrá út"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "Þú munt missa aðgang að dulrituðu skilaboðunum þínum nema þú takir öryggisafrit af dulritunarlyklum áður en þú skráir þig út."; +"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Ég vil ekki dulrituðu skilaboðin mín"; +"sign_out_existing_key_backup_alert_sign_out_action" = "Skrá út"; + +// MARK: Sign out warning + +"sign_out_existing_key_backup_alert_title" = "Ertu viss um að þú viljir skrá þig út?"; +"key_backup_recover_done_action" = "Lokið"; + +// Success + +"key_backup_recover_success_info" = "Öryggisafrit var endurheimt!"; +"key_backup_recover_from_recovery_key_recover_action" = "Aflæsi ferli"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Settu inn öryggislykil"; +"key_backup_recover_from_passphrase_recover_action" = "Aflæsi ferli"; + +// Recover from private key +"key_backup_recover_from_private_key_info" = "Endurheimti úr öryggisafriti…"; +"key_backup_recover_invalid_recovery_key_title" = "Misræmi í öryggislyklum"; +"key_backup_setup_success_from_recovery_key_made_copy_action" = "Ég hef gert afrit"; +"key_backup_setup_success_from_recovery_key_recovery_key_title" = "Öryggislykill"; +"key_backup_setup_success_from_passphrase_done_action" = "Lokið"; + +// Success + +"key_backup_setup_success_title" = "Tókst!"; +"key_backup_setup_passphrase_confirm_passphrase_valid" = "Frábært!"; +"key_backup_setup_passphrase_confirm_passphrase_title" = "Staðfesta"; +"key_backup_setup_passphrase_passphrase_valid" = "Frábært!"; +"key_backup_setup_intro_manual_export_info" = "(Ítarlegt)"; +"key_backup_setup_intro_setup_action_without_existing_backup" = "Byrja að nota öryggisafrit dulritunarlykla"; + +// Intro + +"key_backup_setup_intro_title" = "Tapaðu aldrei dulrituðum skilaboðum"; +"key_backup_setup_skip_alert_skip_action" = "Sleppa"; +"key_backup_setup_skip_alert_title" = "Ertu viss?"; + + +// MARK: Key backup setup + +"key_backup_setup_title" = "Öryggisafrit af lykli"; + +// Banner + +"secure_backup_setup_banner_title" = "Varið öryggisafrit"; +"secure_key_backup_setup_existing_backup_error_delete_it" = "Eyða því"; + +// MARK: Secure backup setup + +// Intro + +"secure_key_backup_setup_intro_title" = "Verja öryggisafrit"; + +// Re-request confirmation dialog +"rerequest_keys_alert_title" = "Beiðni send"; +"deactivate_account_password_alert_title" = "Gera notandaaðgang óvirkann"; +"deactivate_account_validate_action" = "Gera notandaaðgang óvirkan"; +"deactivate_account_forget_messages_information_part2_emphasize" = "Viðvörun"; +"deactivate_account_informations_part2_emphasize" = "Þessa aðgerð er ekki hægt að afturkalla."; + +// Deactivate account + +"deactivate_account_title" = "Gera notandaaðgang óvirkann"; +"service_terms_modal_information_title_integration_manager" = "Samþættingarstýring"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Auðkennisþjónn"; +"service_terms_modal_decline_button" = "Hafna"; +"service_terms_modal_accept_button" = "Samþykkja"; +"room_widget_permission_room_id_permission" = "Auðkenni spjallrásar"; +"room_widget_permission_widget_id_permission" = "Auðkenni viðmótshluta"; +"room_widget_permission_theme_permission" = "Þemað þitt"; +"room_widget_permission_user_id_permission" = "Notandaauðkennið þitt"; +"room_widget_permission_avatar_url_permission" = "Vefslóð á auðkennismyndina þína"; +"room_widget_permission_display_name_permission" = "Birtingarnafnið þitt"; +"room_widget_permission_creator_info_title" = "Þessum viðmótshluta var bætt við af:"; + +// Room widget permissions +"room_widget_permission_title" = "Hlaða inn viðmótshluta"; +"kick" = "Fjarlægja úr spjalli"; +"num_members_other" = "%@ notendur"; +"num_members_one" = "%@ notandi"; +"membership_ban" = "Bannað/ur"; +"membership_leave" = "Fór út"; +"membership_invite" = "Boðið"; +"create_account" = "Búa til notandaaðgang"; +"login" = "Innskráning"; +"create_room" = "Búa til spjallrás"; + +// actions +"action_logout" = "Útskráning"; +"delete" = "Eyða"; +"share" = "Deila"; +"redact" = "Fjarlægja"; +"resend" = "Endursenda"; +"copy_button_name" = "Afrita"; + +// Room Screen + +// general errors + +// Home Screen + +// Last seen time + +// call events + +/* -*- + Automatic localization for en + + The following key/value pairs were extracted from the android i18n file: + /console/src/main/res/values/strings.xml. +*/ + + +// titles + +// button names +"send" = "Senda"; +"notice_encryption_enabled_ok_by_you" = "Þú kveiktir á enda-í-enda dulritun."; +"notice_room_created_by_you_for_dm" = "Þú gekkst í hópinn."; +"notice_event_redacted_by_you" = " af þér"; +"notice_room_name_removed_by_you" = "Þú fjarlægðir heiti spjallrásar"; +"notice_ended_video_call_by_you" = "Þú laukst símtalinu"; +"notice_answered_video_call_by_you" = "Þú svaraðir símtalinu"; +"notice_placed_video_call_by_you" = "Þú hringdir myndsamtal"; +"notice_placed_voice_call_by_you" = "Þú hringdir raddsamtal"; +"notice_avatar_url_changed_by_you" = "Þú breyttir auðkennismyndinni þinni"; +"notice_room_reject_by_you" = "Þú hafnaðir boðinu"; +"notice_room_join_by_you" = "Þú gekkst í hópinn"; +"notice_room_invite_you" = "%@ bauð þér"; +"notice_conference_call_finished" = "VoIP-símafundi lokið"; +"notice_conference_call_started" = "VoIP-símafundur hafinn"; +"notice_room_reason" = ". Ástæða: %@"; +"notice_room_kick" = "%@ fjarlægði %@"; +"notice_room_leave" = "%@ fór út"; +"notice_room_join" = "%@ gekk í hópinn"; +"notice_room_third_party_invite_for_dm" = "%@ bauð %@"; +"widget_picker_manage_integrations" = "Sýsla með samþættingar…"; + +// Widget Picker +"widget_picker_title" = "Samþættingar"; + +// Widget Integration Manager +"widget_integration_need_to_be_able_to_invite" = "Þú þarft að hafa heimild til að bjóða notendum til að gera þetta."; +"widget_menu_revoke_permission" = "Afturkalla aðgang fyrir mig"; +"widget_menu_open_outside" = "Opna í vafra"; +"widget_menu_refresh" = "Endurlesa"; +"e2e_key_backup_wrong_version_button_settings" = "Stillingar"; +"analytics_prompt_stop" = "Hætta deilingu"; +"analytics_prompt_not_now" = "Ekki núna"; +"analytics_prompt_point_3" = "Þú getur slökkt á þessu hvenær sem er í stillingunum"; +"analytics_prompt_terms_link_upgrade" = "hér"; +"analytics_prompt_terms_link_new_user" = "hér"; +"call_actions_unhold" = "Halda áfram"; +"call_no_stun_server_error_title" = "Símtal mistókst vegna vanstillingar netþjóns"; +"room_does_not_exist" = "%@ er ekki til"; +"event_formatter_message_deleted" = "Skilaboðum eytt"; +"event_formatter_group_call_incoming" = "%@ í %@"; +"event_formatter_group_call_leave" = "Fara út"; +"event_formatter_group_call_join" = "Taka þátt"; +"event_formatter_call_end_call" = "Ljúka símtali"; +"event_formatter_call_retry" = "Reyna aftur"; +"event_formatter_call_answer" = "Svara"; +"event_formatter_call_decline" = "Hafna"; +"event_formatter_call_back" = "Hringja til baka"; +"event_formatter_call_connection_failed" = "Tenging mistókst"; +"event_formatter_call_missed_video" = "Ósvarað myndsímtal"; +"event_formatter_call_you_declined" = "Símtali hafnað"; +"event_formatter_call_incoming_video" = "Innhringing myndsamtals"; +"event_formatter_call_incoming_voice" = "Innhringing raddsamtals"; +"event_formatter_call_has_ended_with_time" = "Símtali lokið • %@"; +"event_formatter_call_has_ended" = "Símtali lokið"; +"event_formatter_call_ringing" = "Hringing…"; +"event_formatter_call_connecting" = "Tengist…"; +"event_formatter_message_edited_mention" = "(breytt)"; + +// Image picker +"image_picker_action_camera" = "Taka ljósmynd"; + +// Media picker +"media_picker_title" = "Margmiðlunarsafn"; +"room_notifs_settings_account_settings" = "Stillingar notandaaðgangs"; +"room_notifs_settings_cancel_action" = "Hætta við"; +"room_notifs_settings_done_action" = "Lokið"; +"room_notifs_settings_none" = "Ekkert"; +"room_notifs_settings_all_messages" = "Öll skilaboð"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Láta mig vita fyrir"; +"room_details_advanced_room_id_for_dm" = "Auðkenni (ID):"; +"room_details_no_local_addresses" = "Þessi spjallrás er ekki með nein staðvær vistföng"; +"room_details_notifs" = "Tilkynningar"; +"room_details_room_name_for_dm" = "Nafn"; +"room_details_photo_for_dm" = "Ljósmynd"; +"room_details_integrations" = "Samþættingar"; +"room_details_title_for_dm" = "Nánar"; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Aftengja samt"; +"identity_server_settings_alert_disconnect_button" = "Aftengja"; +"identity_server_settings_alert_disconnect" = "Aftengjast frá auðkennisþjóninum %@?"; +"identity_server_settings_alert_disconnect_title" = "Aftengja auðkennisþjón"; +"identity_server_settings_disconnect" = "Aftengjast"; +"identity_server_settings_change" = "Skipta um"; +"identity_server_settings_add" = "Bæta við"; + +// Identity server settings +"identity_server_settings_title" = "Auðkennisþjónn"; +"manage_session_not_trusted" = "Ekki treyst"; +"manage_session_name" = "Nafn á setu"; +"security_settings_advanced" = "ÍTARLEGT"; +"security_settings_cryptography" = "DULRITUN"; +"security_settings_crosssigning_reset" = "Endurstilla"; +"security_settings_crosssigning_bootstrap" = "Setja upp"; +"security_settings_crosssigning_info_ok" = "Kross-undirritun er tilbúin til notkunar."; +"security_settings_crosssigning" = "KROSS-UNDIRRITUN"; +"security_settings_secure_backup_delete" = "Eyða öryggisafriti"; +"security_settings_secure_backup_restore" = "Endurheimta úr öryggisafriti"; +"security_settings_secure_backup_reset" = "Endurstilla"; +"security_settings_secure_backup_setup" = "Setja upp"; +"security_settings_secure_backup_info_valid" = "Þessi seta öryggisafritar dulritunarlyklana þína."; +"security_settings_secure_backup_info_checking" = "Athuga…"; +"security_settings_secure_backup_description" = "Taktu öryggisafrit af dulritunarlyklunum þínum ásamt gögnum notandaaðgangsins fari svo að þú missir aðgang að setunum þínum. Dulritunarlyklarnir verða varðir með einstökum öryggislykli."; +"security_settings_secure_backup" = "VARIÐ ÖRYGGISAFRIT"; + +// Security settings +"security_settings_title" = "Öryggi"; +"settings_enable_room_message_bubbles" = "Skilaboðablöðrur"; +"settings_discovery_three_pid_details_revoke_action" = "Afturkalla"; +"settings_discovery_three_pid_details_share_action" = "Deila"; +"settings_discovery_three_pids_management_information_part2" = "Notandastillingar"; +"settings_key_backup_delete_confirmation_prompt_msg" = "Ertu viss? Þú munt tapa dulrituðu skilaboðunum þínum ef dulritunarlyklarnir þínir eru ekki rétt öryggisafritaðir."; +"settings_key_backup_delete_confirmation_prompt_title" = "Eyða öryggisafriti"; +"settings_key_backup_button_delete" = "Eyða öryggisafriti"; +"settings_key_backup_button_restore" = "Endurheimta úr öryggisafriti"; +"settings_key_backup_button_create" = "Byrja að nota öryggisafrit dulritunarlykla"; +"settings_key_backup_info_progress_done" = "Allir lyklar öryggisafritaðir"; +"settings_key_backup_info_progress" = "Öryggisafrita %@ lykla…"; +"settings_key_backup_info_not_valid" = "Þessi seta er ekki að öryggisafrita dulritunarlyklana þína, en þú ert með fyrirliggjandi öryggisafrit sem þú getur endurheimt úr og notað til að halda áfram."; +"settings_key_backup_info_valid" = "Þessi seta öryggisafritar dulritunarlyklana þína."; +"settings_key_backup_info_algorithm" = "Reiknirit: %@"; +"settings_key_backup_info_signout_warning" = "Taktu öryggisafrit af dulritunarlyklunum áður en þú skráir þig út svo þeir tapist ekki."; +"settings_key_backup_info_none" = "Dulritunarlyklarnir þínir eru ekki öryggisafritaðir úr þessari setu."; +"settings_key_backup_info_checking" = "Athuga…"; +"settings_deactivate_my_account" = "Gera notandaaðganginn minn óvirkann"; +"settings_add_3pid_invalid_password_message" = "Ógild auðkenni"; +"settings_add_3pid_password_title_msidsn" = "Bæta við símanúmeri"; +"settings_add_3pid_password_title_email" = "Bæta við tölvupóstfangi"; +"settings_labs_enable_threads" = "Skilaboð í spjallþráðum"; +"settings_labs_enabled_polls" = "Kannanir"; +"settings_integrations_allow_button" = "Sýsla með samþættingar"; +"settings_new_keyword" = "Bæta við nýju stikkorði"; +"settings_your_keywords" = "Stikkorðin þín"; +"settings_room_upgrades" = "Uppfærslur spjallrásar"; +"settings_call_invitations" = "Boð um samtal"; +"settings_room_invitations" = "Boð á spjallrás"; +"settings_messages_containing_keywords" = "Stikkorð"; +"settings_messages_containing_at_room" = "@spjallrás"; +"settings_messages_containing_user_name" = "Notandanafnið mitt"; +"settings_messages_containing_display_name" = "Birtingarnafn mitt"; +"settings_encrypted_group_messages" = "Dulrituð hópskilaboð"; +"settings_group_messages" = "Hópskilaboð"; +"settings_encrypted_direct_messages" = "Dulrituð bein skilaboð"; +"settings_direct_messages" = "Bein skilaboð"; +"settings_notify_me_for" = "Láta mig vita fyrir"; +"settings_mentions_and_keywords" = "Minnst á og stikkorð"; +"settings_default" = "Sjálfgefnar tilkynningar"; +"settings_security" = "ÖRYGGI"; +"settings_three_pids_management_information_part2" = "Uppgötvun"; +"settings_deactivate_account" = "GERA AÐGANG ÓVIRKANN"; +"settings_key_backup" = "ÖRYGGISAFRIT AF LYKLI"; +"settings_about" = "UM HUGBÚNAÐINN"; +"settings_integrations" = "SAMÞÆTTINGAR"; +"settings_identity_server_settings" = "AUÐKENNISÞJÓNN"; +"settings_discovery_settings" = "UPPGÖTVUN"; +"settings_notifications" = "TILKYNNINGAR"; +"settings_links" = "TENGLAR"; +"media_type_accessibility_sticker" = "Límmerki"; +"media_type_accessibility_file" = "Skrá"; +"media_type_accessibility_location" = "Staðsetning"; +"media_type_accessibility_video" = "Myndskeið"; +"media_type_accessibility_audio" = "Hljóð"; +"media_type_accessibility_image" = "Mynd"; +"message_from_a_thread" = "Úr spjallþræði"; +"threads_empty_show_all_threads" = "Birta alla spjallþræði"; +"threads_empty_info_all" = "Spjallþræðir hjálpa til við að halda samræðum við efnið og gerir auðveldara að rekja þær."; +"threads_empty_title" = "Haltu umræðum skipulögðum með spjallþráðum"; +"threads_action_my_threads" = "Spjallþræðirnir mínir"; +"threads_action_all_threads" = "Allir spjallþræðir"; +"threads_title" = "Þræðir"; +"thread_copy_link_to_thread" = "Afrita tengil á spjallþráð"; + +// MARK: Threads +"room_thread_title" = "Þráður"; +"room_join_group_call" = "Taka þátt"; +"room_open_dialpad" = "Talnaborð"; +"room_place_voice_call" = "Raddsamtal"; +"room_accessibility_thread_more" = "Meira"; +"room_accessibility_hangup" = "Hang up"; +"room_accessibility_threads" = "Spjallþræðir"; +"room_accessibility_video_call" = "Myndsamtal"; +"room_accessibility_call" = "Símtal"; +"room_accessibility_upload" = "Senda inn"; +"room_accessibility_integrations" = "Samþættingar"; +"room_accessibility_search" = "Leita"; +"room_message_edits_history_title" = "Breytingar á skilaboðum"; +"room_resource_limit_exceeded_message_contact_2_link" = "hafðu samband við kerfisstjóra þjónustunnar þinnar"; +"room_predecessor_information" = "Þessi spjallrás er framhald af öðru samtali."; +"room_replacement_link" = "Samtalið heldur áfram hér."; +"room_replacement_information" = "Þessari spjallrás hefur verið skipt út og er hún ekki lengur virk."; +"room_action_reply" = "Svara"; +"room_action_send_sticker" = "Senda límmerki"; +"room_action_camera" = "Taka ljósmynd eða myndskeið"; +"room_event_copy_link_info" = "Tengill afritaður á klippispjald."; +"room_event_action_reaction_show_less" = "Sýna minna"; +"room_event_action_reaction_show_all" = "Sýna allt"; +"room_event_action_edit" = "Breyta"; +"room_event_action_reply_in_thread" = "Spjallþráður"; +"room_event_action_reply" = "Svara"; +"room_event_action_view_decrypted_source" = "Skoða afkóðaða upprunaskrá"; +"room_event_action_view_in_room" = "Skoða á spjallrás"; +"room_event_action_forward" = "Áfram"; +"room_event_action_end_poll" = "Ljúka könnun"; +"room_event_action_remove_poll" = "Fjarlægja könnun"; +"room_unsent_messages_cancel_title" = "Eyða ósendum skilaboðum"; +"encrypted_room_message_reply_to_placeholder" = "Senda dulritað svar…"; +"room_message_editing" = "Breytingar"; +"room_member_power_level_short_custom" = "Sérsniðið"; +"room_member_power_level_short_moderator" = "Umsjón"; +"room_member_power_level_short_admin" = "Stjórnandi"; +"room_participants_security_information_room_not_encrypted" = "Skilaboð í þessari spjallrás eru ekki enda-í-enda dulrituð."; +"room_participants_security_loading" = "Hleð inn…"; +"room_participants_action_security_status_loading" = "Hleð inn…"; +"room_participants_action_security_status_warning" = "Aðvörun"; +"room_participants_action_security_status_verify" = "Sannreyna"; +"room_participants_action_security_status_verified" = "Sannreynt"; +"room_participants_action_section_security" = "Öryggi"; +"room_participants_leave_prompt_title_for_dm" = "Fara út"; +"find_your_contacts_button_title" = "Finndu tengiliðina þína"; +"search_filter_placeholder" = "Sía"; +"rooms_empty_view_title" = "Spjallrásir"; +"people_empty_view_title" = "Fólk"; +"room_recents_suggested_rooms_section" = "TILLÖGUR AÐ SPJALLRÁSUM"; +"room_recents_server_notice_section" = "AÐVARANIR KERFIS"; +"social_login_button_title_sign_up" = "Skrá inn með %@"; +"create_room_section_header_topic" = "Umfjöllunarefni spjallrásar (valkvætt)"; +"create_room_placeholder_name" = "Nafn"; +"create_room_section_header_name" = "Nafn spjallrásar"; + +// MARK: - Create Room + +"create_room_title" = "Ný spjallrás"; +"searchable_directory_x_network" = "%@ netkerfi"; + +// MARK: - Searchable Directory View Controller + +"searchable_directory_create_new_room" = "Búa til nýja spjallrás"; +"biometrics_cant_unlocked_alert_message_retry" = "Reyna aftur"; +"biometrics_desetup_disable_button_title_x" = "Gera %@ óvirkt"; +"biometrics_desetup_title_x" = "Gera %@ óvirkt"; +"biometrics_setup_enable_button_title_x" = "Virkja %@"; +"biometrics_setup_title_x" = "Virkja %@"; +"biometrics_settings_enable_x" = "Virkja %@"; +"pin_protection_settings_change_pin" = "Breyta PIN-númeri"; +"pin_protection_settings_enable_pin" = "Virkja PIN-númer"; +"pin_protection_mismatch_error_message" = "Endilega reyndu aftur"; +"pin_protection_reset_alert_action_reset" = "Endursetja"; +"pin_protection_forgot_pin" = "Gleymt PIN-númer"; +"pin_protection_enter_pin" = "Settu inn PIN-númerið þitt"; +"pin_protection_choose_pin_welcome_after_register" = "Velkomin."; + +// MARK: - PIN Protection + +"pin_protection_choose_pin_welcome_after_login" = "Velkomin(n) aftur."; +"major_update_done_action" = "Náði því"; +"major_update_learn_more_action" = "Kanna nánar"; +"secrets_reset_reset_action" = "Endurstilla"; + +// MARK: - Secrets reset + +"secrets_reset_title" = "Frumstilla allt"; +"secrets_setup_recovery_passphrase_confirm_passphrase_title" = "Staðfesta"; +"secrets_setup_recovery_passphrase_validate_action" = "Lokið"; +"secrets_setup_recovery_key_done_action" = "Lokið"; +"secrets_setup_recovery_key_export_action" = "Vista"; +"secrets_setup_recovery_key_loading" = "Hleð inn…"; +"secrets_recovery_with_key_recovery_key_placeholder" = "Settu inn öryggislykil"; +"secrets_recovery_with_key_recovery_key_title" = "Enter"; + +// Recover with key + +"secrets_recovery_with_key_title" = "Öryggislykill"; +"secrets_recovery_with_passphrase_passphrase_title" = "Fara inn"; +"secrets_recovery_reset_action_part_2" = "Frumstilla allt"; +"user_verification_session_details_verify_action_current_user_manually" = "Sannreyna handvirkt með textaskilaboðum"; +"user_verification_session_details_untrusted_title" = "Ekki treyst"; +"user_verification_sessions_list_session_untrusted" = "Ekki treyst"; +"user_verification_sessions_list_session_trusted" = "Treyst"; +"user_verification_sessions_list_table_title" = "Setur"; +"user_verification_sessions_list_user_trust_level_unknown_title" = "Óþekkt"; +"user_verification_sessions_list_user_trust_level_warning_title" = "Aðvörun"; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "Treyst"; + +// MARK: - User verification + +// Start + +"user_verification_start_verify_action" = "Hefja sannvottun"; +"key_verification_tile_conclusion_done_title" = "Sannreynt"; +"social_login_button_title_sign_in" = "Skrá inn með %@"; +"social_login_button_title_continue" = "Halda áfram með %@"; +"social_login_list_title_sign_up" = "Eða"; +"social_login_list_title_sign_in" = "Eða"; + +// Social login + +"social_login_list_title_continue" = "Halda áfram með"; +"auth_softlogout_clear_data_sign_out" = "Skrá út"; +"auth_softlogout_clear_data_sign_out_title" = "Ertu viss?"; +"auth_softlogout_clear_data_button" = "Hreinsa öll gögn"; +"auth_softlogout_sign_in" = "Skrá inn"; +"auth_reset_password_error_not_found" = "Tölvupóstfangið þitt lítur ekki út fyrir að vera tengt við Matrix-auðkenni á þessum heimaþjóni."; +"auth_login_single_sign_on" = "Skrá inn"; +"onboarding_use_case_existing_server_button" = "Tengjast þjóni"; +"onboarding_use_case_skip_button" = "sleppt þessari spurningu"; +"onboarding_use_case_community_messaging" = "Samfélög"; +"onboarding_use_case_work_messaging" = "Teymi"; +"onboarding_use_case_personal_messaging" = "Vinir og fjölskylda"; +"onboarding_splash_page_1_title" = "Eigðu samtölin þín."; +"onboarding_splash_login_button_title" = "Ég er nú þegar með notandaaðgang"; + +// Onboarding +"onboarding_splash_register_button_title" = "Stofna notandaaðgang"; +"accessibility_button_label" = "hnappur"; + +// Accessibility +"accessibility_checkbox_label" = "hakreitur"; +"callbar_return" = "Til baka"; +"callbar_only_single_paused" = "Símtal í bið"; +"ok" = "Í lagi"; +"done" = "Lokið"; +"open" = "Opna"; +"less" = "Minna"; +"more" = "Meira"; +"switch" = "Skipta um"; +"joined" = "Gekk í hópinn"; +"skip" = "Sleppa"; +"close" = "Loka"; +"enable" = "Virkja"; From 3321588b974ad7962c0523c0b994436542690d8f Mon Sep 17 00:00:00 2001 From: libexus Date: Tue, 15 Mar 2022 16:42:31 +0000 Subject: [PATCH 019/135] Translated using Weblate (German) Currently translated at 99.8% (1868 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 433c136ae..4fedaa943 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -56,7 +56,7 @@ "auth_missing_email_or_phone" = "Fehlende E-Mail-Adresse oder Telefon-Nummer"; "auth_password_dont_match" = "Passwörter stimmen nicht überein"; "auth_username_in_use" = "Benutzername bereits verwendet"; -"auth_forgot_password" = "Passwort des Matrix-Kontos vergessen?"; +"auth_forgot_password" = "Passwort deines Matrix-Kontos vergessen?"; "auth_msisdn_validation_title" = "Verifizierung ausstehend"; "auth_msisdn_validation_message" = "Bitte gib unten den Aktivierungs-Code ein, den wir per SMS verschickt haben."; "auth_msisdn_validation_error" = "Telefonnummer kann nicht verifiziert werden."; From d53c90f47cb8919f4aa6526f898bd7474b093abe Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 16 Mar 2022 17:47:41 +0000 Subject: [PATCH 020/135] Update detector to match web implementation(only track decryption attempts), add labs flag, increase rate limit spacing. --- Config/BuildSettings.swift | 2 +- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 + .../UISIAutoReporter/UISIAutoReporter.swift | 22 ++-- .../UISIAutoReporter/UISIDetector.swift | 124 ++++++------------ .../Modules/Settings/SettingsViewController.m | 29 +++- 6 files changed, 84 insertions(+), 98 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 46856b850..4e9f197b9 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -378,7 +378,7 @@ final class BuildSettings: NSObject { static let secretsRecoveryAllowReset = true // MARK: - UISI Autoreporting - static let cryptoUISIAutoReportingEnabled = true + static let cryptoUISIAutoReportingEnabled = false // MARK: - Polls diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 19ea21669..31ae6fd65 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -619,6 +619,7 @@ Tap the + to start adding people."; "settings_labs_enable_ringing_for_group_calls" = "Ring for group calls"; "settings_labs_enabled_polls" = "Polls"; "settings_labs_enable_threads" = "Threaded messaging"; +"settings_labs_enable_auto_report_decryption_errors" = "Auto Report Decryption Errors"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 7b790ae06..fd4fa8c7f 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4919,6 +4919,10 @@ public class VectorL10n: NSObject { public static var settingsLabsE2eEncryptionPromptMessage: String { return VectorL10n.tr("Vector", "settings_labs_e2e_encryption_prompt_message") } + /// Auto Report Decryption Errors + public static var settingsLabsEnableAutoReportDecryptionErrors: String { + return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") + } /// Ring for group calls public static var settingsLabsEnableRingingForGroupCalls: String { return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls") diff --git a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift index 2b2d8ff41..febd2aee3 100644 --- a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift +++ b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift @@ -23,7 +23,6 @@ struct UISIAutoReportData { let roomId: String? let senderKey: String? let deviceId: String? - let source: UISIEventSource? let userId: String? let sessionId: String? } @@ -34,7 +33,6 @@ extension UISIAutoReportData: Codable { case roomId = "room_id" case senderKey = "sender_key" case deviceId = "device_id" - case source case userId = "user_id" case sessionId = "session_id" } @@ -48,13 +46,14 @@ extension UISIAutoReportData: Codable { let sessionId: String } - static let autoRsRequest = "im.vector.auto_rs_request" + private static let autoRsRequest = "im.vector.auto_rs_request" + private static let reportSpacing = 60 private let bugReporter: MXBugReportRestClient private let dispatchQueue = DispatchQueue(label: "io.element.UISIAutoReporter.queue") // Simple in memory cache of already sent report private var alreadyReportedUisi = Set() - private let e2eDetectedSubject = PassthroughSubject() + private let e2eDetectedSubject = PassthroughSubject() private let matchingRSRequestSubject = PassthroughSubject() private var cancellables = Set() private var sessions = [MXSession]() @@ -70,14 +69,14 @@ extension UISIAutoReportData: Codable { super.init() // Simple rate limiting, for any rage-shakes emitted we guarantee a spacing between requests. e2eDetectedSubject - .bufferAndSpace(spacingDelay: 2) + .bufferAndSpace(spacingDelay: Self.reportSpacing) .sink { [weak self] in guard let self = self else { return } self.sendRageShake(source: $0) }.store(in: &cancellables) matchingRSRequestSubject - .bufferAndSpace(spacingDelay: 2) + .bufferAndSpace(spacingDelay: Self.reportSpacing) .sink { [weak self] in guard let self = self else { return } self.sendMatchingRageShake(source: $0) @@ -103,8 +102,7 @@ extension UISIAutoReportData: Codable { return Self.autoRsRequest } - func uisiDetected(source: E2EMessageDetected) { - guard source.source != UISIEventSource.initialSync else { return } + func uisiDetected(source: UISIDetectedMessage) { dispatchQueue.async { let reportInfo = ReportInfo(roomId: source.roomId, sessionId: source.sessionId) let alreadySent = self.alreadyReportedUisi.contains(reportInfo) @@ -120,15 +118,14 @@ extension UISIAutoReportData: Codable { self.matchingRSRequestSubject.send(source) } - func sendRageShake(source: E2EMessageDetected) { - MXLog.debug("dl sendRageShake") + func sendRageShake(source: UISIDetectedMessage) { + MXLog.debug("[UISIAutoReporter] sendRageShake") guard let session = sessions.first else { return } let uisiData = UISIAutoReportData( eventId: source.eventId, roomId: source.roomId, senderKey: source.senderKey, deviceId: source.senderDeviceId, - source: source.source, userId: source.senderUserId, sessionId: source.sessionId ).jsonString ?? "" @@ -174,7 +171,7 @@ extension UISIAutoReportData: Codable { } func sendMatchingRageShake(source: MXEvent) { - MXLog.debug("dl sendMatchingRageShake") + MXLog.debug("[UISIAutoReporter] sendMatchingRageShake") let eventId = source.content["event_id"] as? String let roomId = source.content["room_id"] as? String let sessionId = source.content["session_id"] as? String @@ -189,7 +186,6 @@ extension UISIAutoReportData: Codable { roomId: roomId, senderKey: senderKey, deviceId: deviceId, - source: nil, userId: userId, sessionId: sessionId ).jsonString ?? "" diff --git a/Riot/Managers/UISIAutoReporter/UISIDetector.swift b/Riot/Managers/UISIAutoReporter/UISIDetector.swift index d16dcfd01..ea0561c7e 100644 --- a/Riot/Managers/UISIAutoReporter/UISIDetector.swift +++ b/Riot/Managers/UISIAutoReporter/UISIDetector.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,86 +19,76 @@ import Foundation protocol UISIDetectorDelegate: AnyObject { var reciprocateToDeviceEventType: String { get } - func uisiDetected(source: E2EMessageDetected) + func uisiDetected(source: UISIDetectedMessage) func uisiReciprocateRequest(source: MXEvent) } -enum UISIEventSource: String { - case initialSync = "INITIAL_SYNC" - case incrementalSync = "INCREMENTAL_SYNC" - case pagination = "PAGINATION" -} - -extension UISIEventSource: Equatable, Codable { } - -struct E2EMessageDetected { +struct UISIDetectedMessage { let eventId: String let roomId: String let senderUserId: String let senderDeviceId: String let senderKey: String let sessionId: String - let source: UISIEventSource - static func fromEvent(event: MXEvent, roomId: String, source: UISIEventSource) -> E2EMessageDetected { - return E2EMessageDetected( + static func fromEvent(event: MXEvent) -> UISIDetectedMessage { + return UISIDetectedMessage( eventId: event.eventId ?? "", - roomId: roomId, + roomId: event.roomId, senderUserId: event.sender, senderDeviceId: event.wireContent["device_id"] as? String ?? "", senderKey: event.wireContent["sender_key"] as? String ?? "", - sessionId: event.wireContent["session_id"] as? String ?? "", - source: source + sessionId: event.wireContent["session_id"] as? String ?? "" ) } } -extension E2EMessageDetected: Hashable { - func hash(into hasher: inout Hasher) { - hasher.combine(eventId) - hasher.combine(roomId) - } -} - - class UISIDetector: MXLiveEventListener { weak var delegate: UISIDetectorDelegate? var enabled = false - private var trackedEvents = [String: (E2EMessageDetected, DispatchSourceTimer)]() + var initialSyncCompleted = false + private var trackedUISIs = [String: DispatchSourceTimer]() private let dispatchQueue = DispatchQueue(label: "io.element.UISIDetector.queue") - private static let timeoutSeconds = 30 + private static let gracePeriodSeconds = 30 - - func onLiveEvent(roomId: String, event: MXEvent) { - guard enabled, event.isEncrypted, event.clear == nil else { return } + func onSessionStateChanged(state: MXSessionState) { dispatchQueue.async { - self.handleEventReceived(detectorEvent: E2EMessageDetected.fromEvent(event: event, roomId: roomId, source: .incrementalSync)) + self.initialSyncCompleted = state == .running } } - func onPaginatedEvent(roomId: String, event: MXEvent) { - guard enabled, event.isEncrypted, event.clear == nil else { return } + func onLiveEventDecryptionAttempted(event: MXEvent, result: MXEventDecryptionResult) { + guard enabled, let eventId = event.eventId, let roomId = event.roomId else { return } dispatchQueue.async { - self.handleEventReceived(detectorEvent: E2EMessageDetected.fromEvent(event: event, roomId: roomId, source: .pagination)) - } - } - - func onEventDecrypted(eventId: String, roomId: String, clearEvent: [AnyHashable: Any]) { - guard enabled else { return } - dispatchQueue.async { - self.unTrack(eventId: eventId, roomId: roomId) - } - } - - func onEventDecryptionError(eventId: String, roomId: String, error: Error) { - guard enabled else { return } - dispatchQueue.async { - if let event = self.unTrack(eventId: eventId, roomId: roomId) { - self.triggerUISI(source: event) + let trackedId = Self.trackedEventId(roomId: eventId, eventId: roomId) + + if let timer = self.trackedUISIs[trackedId], + result.clearEvent != nil { + // successfully decrypted during grace period, cancel timer. + self.trackedUISIs[trackedId] = nil + timer.cancel() + return } + + guard self.initialSyncCompleted, + result.clearEvent == nil + else { return } + + // track uisi and report it only if it is not decrypted before grade period ends + let timer = DispatchSource.makeTimerSource(queue: self.dispatchQueue) + timer.schedule(deadline: .now() + .seconds(Self.gracePeriodSeconds)) + timer.setEventHandler { [weak self] in + guard let self = self else { return } + self.trackedUISIs[trackedId] = nil + MXLog.verbose("[UISIDetector] onLiveEventDecryptionAttempted: Timeout on \(eventId)") + self.triggerUISI(source: UISIDetectedMessage.fromEvent(event: event)) + } + self.trackedUISIs[trackedId] = timer + timer.activate() } + } func onLiveToDeviceEvent(event: MXEvent) { @@ -106,45 +96,13 @@ class UISIDetector: MXLiveEventListener { delegate?.uisiReciprocateRequest(source: event) } - private func handleEventReceived(detectorEvent: E2EMessageDetected) { + private func triggerUISI(source: UISIDetectedMessage) { guard enabled else { return } - let trackedId = Self.trackedEventId(roomId: detectorEvent.roomId, eventId: detectorEvent.eventId) - guard trackedEvents[trackedId] == nil else { - MXLog.warning("## UISIDetector: Event \(detectorEvent.eventId) is already tracked") - return - } - // track it and start timer - let timer = DispatchSource.makeTimerSource(queue: dispatchQueue) - timer.schedule(deadline: .now() + .seconds(Self.timeoutSeconds)) - timer.setEventHandler { [weak self] in - guard let self = self else { return } - self.unTrack(eventId: detectorEvent.eventId, roomId: detectorEvent.roomId) - MXLog.verbose("## UISIDetector: Timeout on \(detectorEvent.eventId)") - self.triggerUISI(source: detectorEvent) - } - trackedEvents[trackedId] = (detectorEvent, timer) - timer.activate() - } - - private func triggerUISI(source: E2EMessageDetected) { - guard enabled else { return } - MXLog.info("## UISIDetector: Unable To Decrypt \(source)") + MXLog.info("[UISIDetector] triggerUISI: Unable To Decrypt \(source)") self.delegate?.uisiDetected(source: source) } - - @discardableResult private func unTrack(eventId: String, roomId: String) -> E2EMessageDetected? { - let trackedId = Self.trackedEventId(roomId: roomId, eventId: eventId) - guard let (event, timer) = trackedEvents[trackedId] - else { - return nil - } - trackedEvents[trackedId] = nil - timer.cancel() - return event - } static func trackedEventId(roomId: String, eventId: String) -> String { return "\(roomId)-\(eventId)" } - } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 00088a4f7..e691be737 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -159,7 +159,8 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) { LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0, LABS_ENABLE_THREADS_INDEX, - LABS_ENABLE_MESSAGE_BUBBLES_INDEX + LABS_ENABLE_MESSAGE_BUBBLES_INDEX, + LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS }; typedef NS_ENUM(NSUInteger, SECURITY) @@ -572,6 +573,7 @@ TableViewSectionsDelegate> [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_MESSAGE_BUBBLES_INDEX]; + [sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS]; sectionLabs.headerTitle = [VectorL10n settingsLabs]; if (sectionLabs.hasAnyRows) { @@ -1490,6 +1492,21 @@ TableViewSectionsDelegate> return labelAndSwitchCell; } +- (UITableViewCell *)buildAutoReportDecryptionErrorsCellForTableView:(UITableView*)tableView + atIndexPath:(NSIndexPath*)indexPath +{ + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsLabsEnableAutoReportDecryptionErrors]; + + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.enableUISIAutoReporting; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableAutoReportDecryptionErrors:) forControlEvents:UIControlEventTouchUpInside]; + + return labelAndSwitchCell; +} + #pragma mark - 3Pid Add - (void)showAuthenticationIfNeededForAdding:(MX3PIDMedium)medium withSession:(MXSession*)session completion:(void (^)(NSDictionary* authParams))completion @@ -2462,6 +2479,10 @@ TableViewSectionsDelegate> { cell = [self buildMessageBubblesCellForTableView:tableView atIndexPath:indexPath]; } + else if (row == LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS) + { + cell = [self buildAutoReportDecryptionErrorsCellForTableView:tableView atIndexPath:indexPath]; + } } else if (section == SECTION_TAG_FLAIR) { @@ -3890,6 +3911,12 @@ TableViewSectionsDelegate> [roomDataSourceManager reset]; } + +- (void)toggleEnableAutoReportDecryptionErrors:(UISwitch *)sender +{ + RiotSettings.shared.enableUISIAutoReporting = sender.isOn; +} + #pragma mark - TextField listener - (IBAction)textFieldDidChange:(id)sender From 6ca00b84e73fb66cfb374cb4d279d88e89cfb3c3 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 17 Mar 2022 09:16:11 +0000 Subject: [PATCH 021/135] fix merge --- Podfile | 8 +- Podfile.lock | 9 +- .../Modules/Settings/SettingsViewController.m | 1 + .../SideMenu/SideMenuCoordinator.swift | 25 +- .../Modules/Common/Mock/MockAppScreens.swift | 2 +- .../TemplateRoomsCoordinator.swift | 8 +- .../TemplateRoomChatCoordinator.swift | 2 +- .../MockTemplateRoomChatScreenState.swift | 2 +- .../MatrixSDK/TemplateRoomChatService.swift | 29 +- .../Mock/MockTemplateRoomChatService.swift | 2 + .../TemplateRoomChatModels.swift | 2 +- .../TemplateRoomChat/View/ImageViewer.swift | 294 ++++++++++++++++++ .../View/TemplateRoomChat.swift | 12 +- .../TemplateRoomChatBubbleContentView.swift | 4 +- .../View/TemplateRoomChatBubbleImage.swift | 21 +- .../View/TemplateRoomChatBubbleView.swift | 4 +- RiotSwiftUI/RiotSwiftUIApp.swift | 2 +- 17 files changed, 391 insertions(+), 36 deletions(-) create mode 100644 RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/ImageViewer.swift diff --git a/Podfile b/Podfile index 07fe178d6..fd699f7d6 100644 --- a/Podfile +++ b/Podfile @@ -13,9 +13,9 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixSDKVersion = '= 0.22.6' -# $matrixSDKVersion = :local -# $matrixSDKVersion = { :branch => 'develop'} +# $matrixSDKVersion = '= 0.22.6' + $matrixSDKVersion = :local +#$matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } ######################################## @@ -154,4 +154,4 @@ post_install do |installer| config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)', '-Xcc', '-Wno-nullability-completeness'] end end -end \ No newline at end of file +end diff --git a/Podfile.lock b/Podfile.lock index 1c3e87179..1a3719d2a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -115,8 +115,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.22.6) - - MatrixSDK/JingleCallStack (= 0.22.6) + - MatrixSDK (from `../matrix-ios-sdk/MatrixSDK.podspec`) + - MatrixSDK/JingleCallStack (from `../matrix-ios-sdk/MatrixSDK.podspec`) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -156,7 +156,6 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging - - MatrixSDK - OLMKit - PostHog - ReadMoreTextView @@ -177,6 +176,8 @@ EXTERNAL SOURCES: AnalyticsEvents: :branch: release/swift :git: https://github.com/matrix-org/matrix-analytics-events.git + MatrixSDK: + :path: "../matrix-ios-sdk/MatrixSDK.podspec" CHECKOUT OPTIONS: AnalyticsEvents: @@ -225,6 +226,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 16aaf5e59ec902619fbfd799939f044728a92ab7 +PODFILE CHECKSUM: add4568acff884a72cf19933027bbc5507725185 COCOAPODS: 1.11.2 diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index ed040f53d..4cff1dac3 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2478,6 +2478,7 @@ TableViewSectionsDelegate> else if (row == LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS) { cell = [self buildAutoReportDecryptionErrorsCellForTableView:tableView atIndexPath:indexPath]; + } else if (row == LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX) { MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; diff --git a/Riot/Modules/SideMenu/SideMenuCoordinator.swift b/Riot/Modules/SideMenu/SideMenuCoordinator.swift index 7f5280fd8..962228a0f 100644 --- a/Riot/Modules/SideMenu/SideMenuCoordinator.swift +++ b/Riot/Modules/SideMenu/SideMenuCoordinator.swift @@ -67,6 +67,7 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType { private var createSpaceCoordinator: SpaceCreationCoordinator? private var createRoomCoordinator: CreateRoomCoordinator? private var spaceSettingsCoordinator: Coordinator? + private var chatCoordinator: TemplateRoomsCoordinator? // MARK: Public @@ -238,10 +239,28 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType { } private func showInviteFriends(from sourceView: UIView?) { - let myUserId = self.parameters.userSessionsService.mainUserSession?.userId ?? "" +// let myUserId = self.parameters.userSessionsService.mainUserSession?.userId ?? "" - let inviteFriendsPresenter = InviteFriendsPresenter() - inviteFriendsPresenter.present(for: myUserId, from: self.sideMenuViewController, sourceView: sourceView, animated: true) +// let inviteFriendsPresenter = InviteFriendsPresenter() +// inviteFriendsPresenter.present(for: myUserId, from: self.sideMenuViewController, sourceView: sourceView, animated: true) + + guard let session = self.parameters.userSessionsService.mainUserSession?.matrixSession else { + return + } + let coordinator = TemplateRoomsCoordinator(parameters: TemplateRoomsCoordinatorParameters(session: session)) + coordinator.callback = { [weak self] in + guard let self = self else { return } + + coordinator.toPresentable().dismiss(animated: true) { + self.chatCoordinator = nil + } + } + + let presentable = coordinator.toPresentable() + presentable.presentationController?.delegate = self + toPresentable().present(presentable, animated: true, completion: nil) + coordinator.start() + self.chatCoordinator = coordinator } private func showMenu(forSpaceWithId spaceId: String, from sourceView: UIView?) { diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 3c5909a4c..963b9954a 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -17,7 +17,7 @@ import Foundation /// The static list of mocked screens in RiotSwiftUI -@available(iOS 14.0, *) +@available(iOS 15.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ MockOnboardingCongratulationsScreenState.self, diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift index be453b948..f593eb32d 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift @@ -46,7 +46,7 @@ final class TemplateRoomsCoordinator: Coordinator, Presentable { func start() { - if #available(iOS 14.0, *) { + if #available(iOS 15.0, *) { MXLog.debug("[TemplateRoomsCoordinator] did start.") let rootCoordinator = self.createTemplateRoomListCoordinator() rootCoordinator.start() @@ -71,7 +71,7 @@ final class TemplateRoomsCoordinator: Coordinator, Presentable { // MARK: - Private - @available(iOS 14.0, *) + @available(iOS 15.0, *) private func createTemplateRoomListCoordinator() -> TemplateRoomListCoordinator { let coordinator: TemplateRoomListCoordinator = TemplateRoomListCoordinator(parameters: TemplateRoomListCoordinatorParameters(session: parameters.session)) @@ -88,13 +88,13 @@ final class TemplateRoomsCoordinator: Coordinator, Presentable { return coordinator } - @available(iOS 14.0, *) + @available(iOS 15.0, *) private func createTemplateRoomChatCoordinator(room: MXRoom) -> TemplateRoomChatCoordinator { let coordinator: TemplateRoomChatCoordinator = TemplateRoomChatCoordinator(parameters: TemplateRoomChatCoordinatorParameters(room: room)) return coordinator } - @available(iOS 14.0, *) + @available(iOS 15.0, *) func showTemplateRoomChat(roomId: String) { guard let room = parameters.session.room(withRoomId: roomId) else { MXLog.error("[TemplateRoomsCoordinator] Failed to find room by selected Id.") diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift index d9a5e10f2..8b4d9cb86 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift @@ -38,7 +38,7 @@ final class TemplateRoomChatCoordinator: Coordinator, Presentable { // MARK: - Setup - @available(iOS 14.0, *) + @available(iOS 15.0, *) init(parameters: TemplateRoomChatCoordinatorParameters) { self.parameters = parameters let viewModel = TemplateRoomChatViewModel(templateRoomChatService: TemplateRoomChatService(room: parameters.room)) diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift index 925c5c614..6c7289a27 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift @@ -19,7 +19,7 @@ import SwiftUI /// Using an enum for the screen allows you define the different state cases with /// the relevant associated data for each case. -@available(iOS 14.0, *) +@available(iOS 15.0, *) enum MockTemplateRoomChatScreenState: MockScreenState, CaseIterable { // A case for each state you want to represent // with specific, minimal associated data that will allow you diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift index 6d231f744..3dc571939 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift @@ -16,6 +16,7 @@ import Foundation import Combine +import MatrixSDK @available(iOS 14.0, *) class TemplateRoomChatService: TemplateRoomChatServiceProtocol { @@ -96,20 +97,21 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { return events .filter({ event in event.type == kMXEventTypeStringRoomMessage - && event.content[kMXMessageTypeKey] as? String == kMXMessageTypeText + && (event.content[kMXMessageTypeKey] as? String == kMXMessageTypeText + || event.content[kMXMessageTypeKey] as? String == kMXMessageTypeImage) // TODO: New to our SwiftUI Template? Why not implement another message type like image? }) .compactMap({ event -> TemplateRoomChatMessage? in guard let eventId = event.eventId, - let body = event.content[kMXMessageBodyKey] as? String, let sender = senderForMessage(event: event) else { return nil } + let messageContent = messageContentForEvent(event: event) return TemplateRoomChatMessage( id: eventId, - content: .text(TemplateRoomChatMessageTextContent(body: body)), + content: messageContent, sender: sender, timestamp: Date(timeIntervalSince1970: TimeInterval(event.originServerTs / 1000)) ) @@ -124,4 +126,25 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { let avatarUrl = eventFormatter.senderAvatarUrl(for: event, with: roomState) return TemplateRoomChatMember(id: sender, avatarUrl: avatarUrl, displayName: displayName) } + + private func messageContentForEvent(event: MXEvent) -> TemplateRoomChatMessageContent { + switch event.content[kMXMessageTypeKey] as? String { + case kMXMessageTypeText: + let body = event.content[kMXMessageBodyKey] as? String ?? "" + return .text(TemplateRoomChatMessageTextContent(body: body)) + case kMXMessageTypeImage: + let url: URL +// room.mxSession.mediaManager. + if let contentURL = event.content["url"] as? String, + let info = event.content["info"] as? [String: Any], + let localImagePath = MXMediaManager.cachePath(forMatrixContentURI: contentURL, andType:info["mimetype"] as? String, inFolder: event.roomId) { + url = URL(fileURLWithPath: localImagePath) + } else{ + url = URL(string: "https://cahilldental.ie/wp-content/uploads/2016/10/orionthemes-placeholder-image.png")! + } + return .image(TemplateRoomChatMessageImageContent(url: url)) + default: break + } + fatalError("unsupported event type content") + } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift index 8fa354911..3134b2d29 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift @@ -24,11 +24,13 @@ class MockTemplateRoomChatService: TemplateRoomChatServiceProtocol { static let amadine = TemplateRoomChatMember(id: "@amadine:matrix.org", avatarUrl: "!aaabaa:matrix.org", displayName: "Amadine") static let mathew = TemplateRoomChatMember(id: "@mathew:matrix.org", avatarUrl: "!bbabb:matrix.org", displayName: "Mathew") + static let partyImageUrl = URL(string: "https://img.evbuc.com/https%3A%2F%2Fcdn.evbuc.com%2Fimages%2F184375039%2F474927372937%2F1%2Foriginal.20211111-155142?w=800&auto=format%2Ccompress&q=75&sharp=10&rect=0%2C236%2C4724%2C2362&s=18c17c71af6df1e5e46d630fac923834")! static let mockMessages = [ TemplateRoomChatMessage(id: "!0:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Shall I put it live?")) , sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -3)), TemplateRoomChatMessage(id: "!1:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Yea go for it! ...and then let's head to the pub")), sender: mathew, timestamp: Date(timeIntervalSinceNow: 60)), TemplateRoomChatMessage(id: "!2:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Deal.")), sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -2)), TemplateRoomChatMessage(id: "!3:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Ok, Done. 🍻")), sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -1)), + TemplateRoomChatMessage(id: "!3:matrix.org", content: .image(TemplateRoomChatMessageImageContent(url: partyImageUrl)), sender: mathew, timestamp: Date(timeIntervalSinceNow: 60 * -1)), ] var roomInitializationStatus: CurrentValueSubject var chatMessagesSubject: CurrentValueSubject<[TemplateRoomChatMessage], Never> diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift index e9aa09141..5e5310189 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift @@ -20,7 +20,7 @@ import UIKit /// An image sent as a message. struct TemplateRoomChatMessageImageContent: Equatable { - var image: UIImage + var url: URL } /// The text content of a message sent by a user. diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/ImageViewer.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/ImageViewer.swift new file mode 100644 index 000000000..37c78dabb --- /dev/null +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/ImageViewer.swift @@ -0,0 +1,294 @@ +import SwiftUI +import UIKit + +@available(iOS 13.0, *) +public struct ImageViewer: View { + @Binding var viewerShown: Bool + @Binding var image: Image + @Binding var imageOpt: Image? + @State var caption: Text? + @State var closeButtonTopRight: Bool? + + var aspectRatio: Binding? + + @State var dragOffset: CGSize = CGSize.zero + @State var dragOffsetPredicted: CGSize = CGSize.zero + + public init(image: Binding, viewerShown: Binding, aspectRatio: Binding? = nil, caption: Text? = nil, closeButtonTopRight: Bool? = false) { + _image = image + _viewerShown = viewerShown + _imageOpt = .constant(nil) + self.aspectRatio = aspectRatio + _caption = State(initialValue: caption) + _closeButtonTopRight = State(initialValue: closeButtonTopRight) + } + + public init(image: Binding, viewerShown: Binding, aspectRatio: Binding? = nil, caption: Text? = nil, closeButtonTopRight: Bool? = false) { + _image = .constant(Image(systemName: "")) + _imageOpt = image + _viewerShown = viewerShown + self.aspectRatio = aspectRatio + _caption = State(initialValue: caption) + _closeButtonTopRight = State(initialValue: closeButtonTopRight) + } + + func getImage() -> Image { + if(self.imageOpt == nil) { + return self.image + } + else { + return self.imageOpt ?? Image(systemName: "questionmark.diamond") + } + } + + @ViewBuilder + public var body: some View { + VStack { + if(viewerShown) { + ZStack { + VStack { + HStack { + + if self.closeButtonTopRight == true { + Spacer() + } + + Button(action: { self.viewerShown = false }) { + Image(systemName: "xmark") + .foregroundColor(Color(UIColor.white)) + .font(.system(size: UIFontMetrics.default.scaledValue(for: 24))) + } + + if self.closeButtonTopRight != true { + Spacer() + } + } + + Spacer() + } + .padding() + .zIndex(2) + + VStack { + ZStack { + self.getImage() + .resizable() + .aspectRatio(self.aspectRatio?.wrappedValue, contentMode: .fit) + .offset(x: self.dragOffset.width, y: self.dragOffset.height) + .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) + .pinchToZoom() + .gesture(DragGesture() + .onChanged { value in + self.dragOffset = value.translation + self.dragOffsetPredicted = value.predictedEndTranslation + } + .onEnded { value in + if((abs(self.dragOffset.height) + abs(self.dragOffset.width) > 570) || ((abs(self.dragOffsetPredicted.height)) / (abs(self.dragOffset.height)) > 3) || ((abs(self.dragOffsetPredicted.width)) / (abs(self.dragOffset.width))) > 3) { + withAnimation(.spring()) { + self.dragOffset = self.dragOffsetPredicted + } + self.viewerShown = false + + return + } + withAnimation(.interactiveSpring()) { + self.dragOffset = .zero + } + } + ) + + if(self.caption != nil) { + VStack { + Spacer() + + VStack { + Spacer() + + HStack { + Spacer() + + self.caption + .foregroundColor(.white) + .multilineTextAlignment(.center) + + Spacer() + } + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(red: 0.12, green: 0.12, blue: 0.12, opacity: (1.0 - Double(abs(self.dragOffset.width) + abs(self.dragOffset.height)) / 1000)).edgesIgnoringSafeArea(.all)) + .zIndex(1) + } + .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2))) + .onAppear() { + self.dragOffset = .zero + self.dragOffsetPredicted = .zero + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +@available(iOS 13.0, *) +class PinchZoomView: UIView { + + weak var delegate: PinchZoomViewDelgate? + + private(set) var scale: CGFloat = 0 { + didSet { + delegate?.pinchZoomView(self, didChangeScale: scale) + } + } + + private(set) var anchor: UnitPoint = .center { + didSet { + delegate?.pinchZoomView(self, didChangeAnchor: anchor) + } + } + + private(set) var offset: CGSize = .zero { + didSet { + delegate?.pinchZoomView(self, didChangeOffset: offset) + } + } + + private(set) var isPinching: Bool = false { + didSet { + delegate?.pinchZoomView(self, didChangePinching: isPinching) + } + } + + private var startLocation: CGPoint = .zero + private var location: CGPoint = .zero + private var numberOfTouches: Int = 0 + + init() { + super.init(frame: .zero) + + let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch(gesture:))) + pinchGesture.cancelsTouchesInView = false + addGestureRecognizer(pinchGesture) + } + + required init?(coder: NSCoder) { + fatalError() + } + + @objc private func pinch(gesture: UIPinchGestureRecognizer) { + + switch gesture.state { + case .began: + isPinching = true + startLocation = gesture.location(in: self) + anchor = UnitPoint(x: startLocation.x / bounds.width, y: startLocation.y / bounds.height) + numberOfTouches = gesture.numberOfTouches + + case .changed: + if gesture.numberOfTouches != numberOfTouches { + // If the number of fingers being used changes, the start location needs to be adjusted to avoid jumping. + let newLocation = gesture.location(in: self) + let jumpDifference = CGSize(width: newLocation.x - location.x, height: newLocation.y - location.y) + startLocation = CGPoint(x: startLocation.x + jumpDifference.width, y: startLocation.y + jumpDifference.height) + + numberOfTouches = gesture.numberOfTouches + } + + scale = gesture.scale + + location = gesture.location(in: self) + offset = CGSize(width: location.x - startLocation.x, height: location.y - startLocation.y) + + case .ended, .cancelled, .failed: + withAnimation(.interactiveSpring()) { + isPinching = false + scale = 1.0 + anchor = .center + offset = .zero + } + default: + break + } + } + +} +@available(iOS 13.0, *) +protocol PinchZoomViewDelgate: AnyObject { + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) +} + +@available(iOS 13.0, *) +struct PinchZoom: UIViewRepresentable { + + @Binding var scale: CGFloat + @Binding var anchor: UnitPoint + @Binding var offset: CGSize + @Binding var isPinching: Bool + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> PinchZoomView { + let pinchZoomView = PinchZoomView() + pinchZoomView.delegate = context.coordinator + return pinchZoomView + } + + func updateUIView(_ pageControl: PinchZoomView, context: Context) { } + + class Coordinator: NSObject, PinchZoomViewDelgate { + var pinchZoom: PinchZoom + + init(_ pinchZoom: PinchZoom) { + self.pinchZoom = pinchZoom + } + + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) { + pinchZoom.isPinching = isPinching + } + + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) { + pinchZoom.scale = scale + } + + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) { + pinchZoom.anchor = anchor + } + + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) { + pinchZoom.offset = offset + } + } +} + +@available(iOS 13.0, *) +struct PinchToZoom: ViewModifier { + @State var scale: CGFloat = 1.0 + @State var anchor: UnitPoint = .center + @State var offset: CGSize = .zero + @State var isPinching: Bool = false + + func body(content: Content) -> some View { + content + .scaleEffect(scale, anchor: anchor) + .offset(offset) + .overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching)) + } +} + +@available(iOS 13.0, *) +extension View { + func pinchToZoom() -> some View { + self.modifier(PinchToZoom()) + } +} diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift index c714ff34b..c47c3300a 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift @@ -17,7 +17,7 @@ import SwiftUI import Combine -@available(iOS 14.0, *) +@available(iOS 15.0, *) struct TemplateRoomChat: View { // MARK: - Properties @@ -26,6 +26,8 @@ struct TemplateRoomChat: View { @Environment(\.theme) private var theme: ThemeSwiftUI + @State var fullScreenImage: Image? + // MARK: Public @ObservedObject var viewModel: TemplateRoomChatViewModel.Context @@ -63,6 +65,12 @@ struct TemplateRoomChat: View { } } + + + .onTapGesture { + showImageViewer.toggle() + } + .overlay(ImageViewer(image: $image, viewerShown: self.$showImageViewer)) @ViewBuilder private var roomContent: some View { if case .notInitialized = viewModel.viewState.roomInitializationStatus { @@ -127,7 +135,7 @@ struct TemplateRoomChat: View { // MARK: - Previews -@available(iOS 14.0, *) +@available(iOS 15.0, *) struct TemplateRoomChat_Previews: PreviewProvider { static let stateRenderer = MockTemplateRoomChatScreenState.stateRenderer static var previews: some View { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift index 6efb16b4f..1c9958bd2 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift @@ -16,7 +16,7 @@ import SwiftUI -@available(iOS 14.0, *) +@available(iOS 15.0, *) struct TemplateRoomChatBubbleContentView: View { // MARK: - Properties @@ -44,7 +44,7 @@ struct TemplateRoomChatBubbleContentView: View { // MARK: - Previews -@available(iOS 14.0, *) +@available(iOS 15.0, *) struct TemplateRoomChatBubbleItemView_Previews: PreviewProvider { static var previews: some View { EmptyView() diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift index 16fa077d4..4c178184b 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift @@ -16,7 +16,7 @@ import SwiftUI -@available(iOS 14.0, *) +@available(iOS 15.0, *) struct TemplateRoomChatBubbleImage: View { // MARK: - Properties @@ -24,22 +24,29 @@ struct TemplateRoomChatBubbleImage: View { // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI - + // MARK: Public let imageContent: TemplateRoomChatMessageImageContent - + @State var showImageViewer: Bool = false var body: some View { - EmptyView() + AsyncImage(url: imageContent.url) { image in + image.resizable() + .aspectRatio(contentMode: .fill) + .frame(width: CGFloat(258), height: CGFloat(150)) + .cornerRadius(8) + } placeholder: { + Color.green + } } } // MARK: - Previews -@available(iOS 14.0, *) +@available(iOS 15.0, *) struct TemplateRoomChatBubbleImage_Previews: PreviewProvider { + static let exampleUrl = URL(string: "https://docs-assets.developer.apple.com/published/9c4143a9a48a080f153278c9732c03e7/17400/SwiftUI-Image-waterWheel-resize~dark@2x.png")! static var previews: some View { - EmptyView() - // TODO: New to our SwiftUI Template? Why not implement the image item in the bubble here? + TemplateRoomChatBubbleImage(imageContent: TemplateRoomChatMessageImageContent(url:exampleUrl)) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift index b324a9565..6e5a6808e 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift @@ -16,7 +16,7 @@ import SwiftUI -@available(iOS 14.0, *) +@available(iOS 15.0, *) struct TemplateRoomChatBubbleView: View { // MARK: - Properties @@ -52,7 +52,7 @@ struct TemplateRoomChatBubbleView: View { // MARK: - Previews -@available(iOS 14.0, *) +@available(iOS 15.0, *) struct TemplateRoomChatBubbleView_Previews: PreviewProvider { static let bubble = TemplateRoomChatBubble( id: "111", diff --git a/RiotSwiftUI/RiotSwiftUIApp.swift b/RiotSwiftUI/RiotSwiftUIApp.swift index 902f7327d..b1527da23 100644 --- a/RiotSwiftUI/RiotSwiftUIApp.swift +++ b/RiotSwiftUI/RiotSwiftUIApp.swift @@ -15,7 +15,7 @@ // import SwiftUI -@available(iOS 14.0, *) +@available(iOS 15.0, *) @main /// RiotSwiftUI screens rendered for UI Tests. struct RiotSwiftUIApp: App { From 1966d6b796cb8c624b46bdeb8ca2211e18f2c9cf Mon Sep 17 00:00:00 2001 From: MaximeE Date: Thu, 17 Mar 2022 16:52:22 +0100 Subject: [PATCH 022/135] 5720: Update UI in location sharing View --- .../location_live_icon.imageset/Contents.json | 23 +++++++ .../location_live_icon.png | Bin 0 -> 542 bytes .../location_live_icon@2x.png | Bin 0 -> 1021 bytes .../location_live_icon@3x.png | Bin 0 -> 1659 bytes Riot/Assets/en.lproj/Vector.strings | 8 ++- Riot/Generated/Images.swift | 1 + Riot/Generated/Strings.swift | 14 +++- .../Room/Location/LocationMarkerView.swift | 9 +++ .../Room/Location/LocationMarkerView.xib | 5 +- .../LocationSharingModels.swift | 3 + .../LocationSharingScreenState.swift | 2 +- .../View/LocationSharingOptionButton.swift | 61 +++++++++++++++++ .../LocationSharingOptionButtonIcon.swift | 48 +++++++++++++ .../View/LocationSharingUserMarkerView.swift | 34 +++++++--- .../View/LocationSharingView.swift | 63 +++++++++++++----- .../View/UserLocationAnnotatonView.swift | 2 +- 16 files changed, 241 insertions(+), 32 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@3x.png create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButton.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButtonIcon.swift diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json new file mode 100644 index 000000000..0a20c899a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_live_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_live_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_live_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6aa2e2639f6d228934ebd9356c5fd272b69bcbc9 GIT binary patch literal 542 zcmV+(0^$9MP)18TqmWtqf`KgUg`ao>>I}Ez^mi6S}Yc=$fbMcTiG2L@Eo1s z&H^{DNnZ$FHe@Sr>O@?WO!kaJPo*M>DhmH@b;8GO}rR*(!eU8nFSKXJr zV_}SMn1BzRMNO5)QBIoTYS@mBuOxQiDeyGbIT-N!?~y_Z!l>h($ literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4ed19fb09fefefa280dc517fca543265895993ef GIT binary patch literal 1021 zcmV!1&^00009a7bBm000&x z000&x0ZCFM@Bjb+0drDELIAGL9O(c600d`2O+f$vv5yPrEjmkJIL`w35@ z=HZD~e2xVbLNLa+Ti-U(&{OMtTl-d10R%Z<9(Pli11g$`=zx`P2e#p+Iwa~tsB@Sd zU~Fk3f+G{MEv#q)Jb%O3$#aeA1~7Fr5fhwn!iFY*`zInOh~&I-ZUu zLsxM4Jn>y_#{;5GMZ*TrEe1jz8(G+oZ@C;6ly-JEu&D+#Es)nBVI3f>0G$&wEv21M zaanW;J#rBpiv{A|SqCKVYDnaNY9W6sizx0BS0(7Qh_J#r6gNM8`w?k(27S#q0_C@g zgzi{K-K+9^=Nfpw7ECA_f`=J01sO2vmC&Rwj|9j(xFWjp7#S>dav1mZ#EtWRMemMDvFf#1ltz20T%sgKibR7iH`jeETB$dr5FbX{eJ-2?Tgn##StnS#&~3 zUX{W(w&ee85qzS5k4HMsEkoS-$%ob7#r`$vxh3F*3PX#CO7u~Pe1R+3;C5qmNAZnfhvDQUh_9`30O z_TCOl)_1@o^OSVfo1;*%B#2S<8BkLOxXd+tY3M)&9J)Psf~Fe|qVhDSPS48VtXGOT za2YzbfUg!bsO(=`lWa1)d8SM;RJ(v#u%ZE78c|f=nD%pCosJxq^^C^pM_~&&(a-|E zC7|mB%^_D<4d~XELdD<*9uKqJ=5r^b+>n(+PET+WC@R7}+r~+m?})&doTvP>p=sG6 z>^trk3cXuyvz^R12aK9*hQUpy6C4-#FD2?6_d3sbA0J8?G|TsMJ;RykG%;OA|CsomH$hd$Sx8=A` rYAzaAG+_!o-Ni=c$#3HvlJocrSz_i({_LHs00000NkvXXu0mjfojk!& literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..cabf83a926263e005805a6e7573247084f2cf40a GIT binary patch literal 1659 zcmV->288*EP)H;mZK~w%wT_8?6K*TN(I}3<%2aufwe8m-% ztx&u#aNrO9qtAPI@*{b`5y1T*2%2cmo;^<-ST1JfiTio!%8@I#u3Q`(94u+iSGazX zpUjmh_1#yl^30Wrzvj-BBii$b41z1qTKZ}seW=MrTZF6Rn*8WCDcwjvryWw?;JOG) zSMHr&H#9O%7wOYW^48F#a^)AN*FB}+DnDbLigti&H+SW-wo1g@l_{EyJ?nKKlNRXn zno?lzlHiTfh`9D%Ms&W$<YTb$=XoQP?UN$A*f5 zHSl%^%HQUPKE|0x->f*o!ue^I&o^Wj=>FKds0y@QpaxNP0y7S!Q8^Z!C2%-N7=m{Z zdTE`iWEryN^1-~H&hHdlyO`+p-8x&G}Vf@C^WM}Y3XYqD+723+RYDJ{-n|1+Y?|>v@`o$$U9m1B$kIgU`2y8>%6iP;S~soT7uyneHli6b zV@yU7xa%(1#&xp;*w5Z9p{^CsOUieQv!IUw*|>JyfeE;{0!kuxCKMD2+uTyc*i}j? z4E2ed$O84f3drY*EPAeZXkLu+_6wjO%(BP;+sGp73q4qEA{}|svA%Of4h`bgKp<1j zfpW=NR->LHj^{G6&X~2IO^S~7t$^Imnou)X{{(cnQ5wid53WlW-2b9@;upe*J{zvJ zyqEYMopkpjKV@Co5ci%da-v6Tz;6zO))6=0WkyaqA80tDI=AkCv==ctk_G(@&ZDev z*>>M&HwLWVBnfdy|p>Q5S2s^CF%H|!^rDUr)J9WFO-HQz~}5&X|v{udJs-s zIj4AbvhzduhPoqz)HVaYr8G+NvpAMxDX!yZ< ze(q{!O-pJAxfwJNFH+o7&6rkE8ndPLmpLA};Zhfk@DhwyU(rh;^s*$Q1lYeD>3dUI z8?y9mSs`9;AVBp^ta43@K(itT=g1|tCr$n6WC+;8ke!4}6(3?4A4WYeFS8Z!e=;>0;g6fnLeJhbsI=@r@%zFO* z55*dZE59n|^e1}8?kOj&MjvA|pPo?%ocufPkA+nq#@lIB>*B;LYb{&=!8rqe&)%m$ zG#Vxf+;{ZbkKHU6hH@TB1I?me09G&Y>>qRihxbksBC;^RMR zWNZ}zbWuaWX)dahOET&ga!Eq`mr5LdvXuw7QQzu?8j#ud(5QG+vj@7hxq{K+LPHmAO$ugh{{e2eRAwe6TYCTi002ovPDHLk FV1h-*4hH}L literal 0 HcmV?d00001 diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index af5aa32be..931f07e75 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2062,8 +2062,6 @@ Tap the + to start adding people."; "location_sharing_close_action" = "Close"; -"location_sharing_share_action" = "Share"; - "location_sharing_post_failure_title" = "We couldn’t send your location"; "location_sharing_post_failure_subtitle" = "%@ could not send your location. Please try again later."; @@ -2088,6 +2086,12 @@ Tap the + to start adding people."; "location_sharing_settings_toggle_title" = "Enable location sharing"; +"location_sharing_live_share_title" = "Share live location"; + +"location_sharing_static_share_title" = "Send my current location"; + +"location_sharing_pin_drop_share_title" = "Send this location"; + // MARK: - MatrixKit diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index df3b02a27..d64969d94 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -169,6 +169,7 @@ internal class Asset: NSObject { internal static let videoCall = ImageAsset(name: "video_call") internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon") internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") + internal static let locationLiveIcon = ImageAsset(name: "location_live_icon") internal static let locationMarkerIcon = ImageAsset(name: "location_marker_icon") internal static let locationShareIcon = ImageAsset(name: "location_share_icon") internal static let locationUserMarker = ImageAsset(name: "location_user_marker") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ca1a6125a..193cc718d 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2731,6 +2731,10 @@ public class VectorL10n: NSObject { public static var locationSharingInvalidAuthorizationSettings: String { return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings") } + /// Share live location + public static var locationSharingLiveShareTitle: String { + return VectorL10n.tr("Vector", "location_sharing_live_share_title") + } /// %@ could not load the map. Please try again later. public static func locationSharingLoadingMapErrorTitle(_ p1: String) -> String { return VectorL10n.tr("Vector", "location_sharing_loading_map_error_title", p1) @@ -2751,6 +2755,10 @@ public class VectorL10n: NSObject { public static var locationSharingOpenOpenStreetMaps: String { return VectorL10n.tr("Vector", "location_sharing_open_open_street_maps") } + /// Send this location + public static var locationSharingPinDropShareTitle: String { + return VectorL10n.tr("Vector", "location_sharing_pin_drop_share_title") + } /// %@ could not send your location. Please try again later. public static func locationSharingPostFailureSubtitle(_ p1: String) -> String { return VectorL10n.tr("Vector", "location_sharing_post_failure_subtitle", p1) @@ -2767,9 +2775,9 @@ public class VectorL10n: NSObject { public static var locationSharingSettingsToggleTitle: String { return VectorL10n.tr("Vector", "location_sharing_settings_toggle_title") } - /// Share - public static var locationSharingShareAction: String { - return VectorL10n.tr("Vector", "location_sharing_share_action") + /// Send my current location + public static var locationSharingStaticShareTitle: String { + return VectorL10n.tr("Vector", "location_sharing_static_share_title") } /// Location public static var locationSharingTitle: String { diff --git a/Riot/Modules/Room/Location/LocationMarkerView.swift b/Riot/Modules/Room/Location/LocationMarkerView.swift index caf101a1b..c7c3eb59a 100644 --- a/Riot/Modules/Room/Location/LocationMarkerView.swift +++ b/Riot/Modules/Room/Location/LocationMarkerView.swift @@ -20,14 +20,23 @@ import Mapbox class LocationMarkerView: MGLAnnotationView, NibLoadable { + @IBOutlet private var markerBackground: UIImageView! @IBOutlet private var avatarView: UserAvatarView! + private static var usernameColorGenerator = UserNameColorGenerator() + private let theme: Theme = ThemeService.shared().theme + override func awakeFromNib() { super.awakeFromNib() translatesAutoresizingMaskIntoConstraints = false } func setAvatarData(_ avatarData: AvatarViewDataProtocol) { + Self.usernameColorGenerator.defaultColor = theme.colors.primaryContent + Self.usernameColorGenerator.userNameColors = theme.colors.namesAndAvatars + let image = Asset.Images.locationUserMarker.image.withRenderingMode(.alwaysTemplate) + markerBackground.image = image + markerBackground.tintColor = Self.usernameColorGenerator.color(from: avatarData.matrixItemId) avatarView.fill(with: avatarData) } } diff --git a/Riot/Modules/Room/Location/LocationMarkerView.xib b/Riot/Modules/Room/Location/LocationMarkerView.xib index 837db5503..a71c32515 100644 --- a/Riot/Modules/Room/Location/LocationMarkerView.xib +++ b/Riot/Modules/Room/Location/LocationMarkerView.xib @@ -1,9 +1,9 @@ - + - + @@ -65,6 +65,7 @@ + diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index cb9843a46..069719e24 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -54,6 +54,9 @@ struct LocationSharingViewState: BindableState { /// Map annotation to focus on var highlightedAnnotation: UserLocationAnnotation? + /// Indicates whether the user has moved around the map to drop a pin somewhere other than their current location + var isPinDropSharing: Bool = false + var showLoadingIndicator: Bool = false /// True to indicate to show and follow current user location diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift index 208b7c2da..c5aa78c86 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift @@ -36,7 +36,7 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable { let mapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")! let viewModel = LocationSharingViewModel(mapStyleURL: mapStyleURL, - avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Alice"), + avatarData: AvatarInput(mxContentUri: "", matrixItemId: "alice:matrix.org", displayName: "Alice"), location: location) return ([viewModel], AnyView(LocationSharingView(context: viewModel.context) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButton.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButton.swift new file mode 100644 index 000000000..9247da882 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButton.swift @@ -0,0 +1,61 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct LocationSharingOptionButton: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + let text: String + let action: () -> (Void) + @ViewBuilder var content: Content + + var body: some View { + Button(action: action) { + HStack(spacing: 18) { + content + .frame(width: 40, height: 40) + Text(text) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + } + } + } +} + +@available(iOS 14.0, *) +struct LocationSharingOptionButton_Previews: PreviewProvider { + static var previews: some View { + VStack { + LocationSharingOptionButton(text: "Share my current location") { + + } content: { + LocationSharingUserMarkerView(isMarker: false, avatarData: AvatarInput(mxContentUri: "", matrixItemId: "test", displayName: "Nicolas")) + } + LocationSharingOptionButton(text: "Share live location") { + + } content: { + LocationSharingOptionButtonIcon(fillColor: Color.purple, image: Asset.Images.locationLiveIcon.image) + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButtonIcon.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButtonIcon.swift new file mode 100644 index 000000000..5cb69a2e3 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButtonIcon.swift @@ -0,0 +1,48 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct LocationSharingOptionButtonIcon: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + let fillColor: Color + let image: UIImage + + var body: some View { + ZStack { + Circle() + .fill(fillColor) + .frame(width: 40, height: 40) + Image(uiImage: image) + .renderingMode(.template) + .foregroundColor(Color.white) + } + } +} + +@available(iOS 14.0, *) +struct LocationSharingOptionButtonIcon_Previews: PreviewProvider { + static var previews: some View { + LocationSharingOptionButtonIcon(fillColor: Color.green, image: Asset.Images.locationMarkerIcon.image) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift index 26bc101d3..361e76fcf 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift @@ -27,19 +27,35 @@ struct LocationSharingUserMarkerView: View { @State private var frame: CGRect = .zero + private var usernameColorGenerator: UserNameColorGenerator { + let usernameColorGenerator = UserNameColorGenerator() + let theme = ThemeService.shared().theme + usernameColorGenerator.defaultColor = theme.textPrimaryColor + usernameColorGenerator.userNameColors = theme.userNameColors + return usernameColorGenerator + } + // MARK: Public + let isMarker: Bool let avatarData: AvatarInputProtocol var body: some View { + let fillColor: Color = Color(usernameColorGenerator.color(from:avatarData.matrixItemId)) ZStack { - Image(uiImage: Asset.Images.locationUserMarker.image) - AvatarImage(avatarData: avatarData, size: .large) - .offset(y: -1.5) + Circle() + .fill(fillColor) + .frame(width: 40, height: 40) + if isMarker { + Rectangle() + .rotation(Angle(degrees: 45)) + .fill(fillColor) + .frame(width: 7, height: 7) + .offset(x: 0, y: 19) + } + AvatarImage(avatarData: avatarData, size: .small) } .background(ViewFrameReader(frame: $frame)) - .padding(.bottom, frame.height) - .accentColor(theme.colors.accent) } } @@ -49,9 +65,11 @@ struct LocationSharingUserMarkerView: View { struct LocationSharingUserMarkerView_Previews: PreviewProvider { static var previews: some View { let avatarData = AvatarInput(mxContentUri: "", - matrixItemId: "", + matrixItemId: "test", displayName: "Alice") - - LocationSharingUserMarkerView(avatarData: avatarData) + VStack(alignment: .center, spacing: 15) { + LocationSharingUserMarkerView(isMarker: true, avatarData: avatarData) + LocationSharingUserMarkerView(isMarker: false, avatarData: avatarData) + } } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift index 317c20b02..f3f7e417e 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -32,16 +32,22 @@ struct LocationSharingView: View { var body: some View { NavigationView { - ZStack(alignment: .bottom) { - LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL, - annotations: context.viewState.annotations, - highlightedAnnotation: context.viewState.highlightedAnnotation, - userAvatarData: context.viewState.userAvatarData, - showsUserLocation: context.viewState.showsUserLocation, - userLocation: $context.userLocation, - errorSubject: context.viewState.errorSubject) - .ignoresSafeArea() - MapCreditsView() + VStack { + ZStack(alignment: .bottom) { + LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL, + annotations: context.viewState.annotations, + highlightedAnnotation: context.viewState.highlightedAnnotation, + userAvatarData: context.viewState.userAvatarData, + showsUserLocation: context.viewState.showsUserLocation, + userLocation: $context.userLocation, + errorSubject: context.viewState.errorSubject) + .ignoresSafeArea() + MapCreditsView() + } + if context.viewState.shareButtonVisible { + buttonsView + .cornerRadius(5) + } } .toolbar { ToolbarItem(placement: .navigationBarLeading) { @@ -63,11 +69,6 @@ struct LocationSharingView: View { .accessibilityIdentifier("LocationSharingView.shareButton") } .disabled(!context.viewState.shareButtonEnabled) - } else { - Button(VectorL10n.locationSharingShareAction, action: { - context.send(viewAction: .share) - }) - .disabled(!context.viewState.shareButtonEnabled) } } } @@ -84,6 +85,38 @@ struct LocationSharingView: View { .navigationViewStyle(StackNavigationViewStyle()) } + var buttonsView: some View { + VStack(alignment: .leading, spacing: 15) { + if !context.viewState.isPinDropSharing { + LocationSharingOptionButton(text: VectorL10n.locationSharingStaticShareTitle) { + context.send(viewAction: .share) + } content: { + LocationSharingUserMarkerView(isMarker: false, avatarData: context.viewState.userAvatarData) + } + .disabled(!context.viewState.shareButtonEnabled) + // Disable for now until live location sharing is done + if BuildSettings.liveLocationSharingEnabled { + LocationSharingOptionButton(text: VectorL10n.locationSharingLiveShareTitle) { + // TODO: - Start live location sharing + } content: { + LocationSharingOptionButtonIcon(fillColor: Color.purple, image: Asset.Images.locationLiveIcon.image) + } + .disabled(!context.viewState.shareButtonEnabled) + } + } else { + LocationSharingOptionButton(text: VectorL10n.locationSharingPinDropShareTitle) { + // TODO: - Pin drop sharing action + } content: { + LocationSharingOptionButtonIcon(fillColor: + theme.colors.primaryContent, image: Asset.Images.locationMarkerIcon.image) + } + .disabled(!context.viewState.shareButtonEnabled) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + @ViewBuilder private var activityIndicator: some View { if context.viewState.showLoadingIndicator { diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift index 6835ca50c..e713508bd 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift @@ -45,7 +45,7 @@ class UserLocationAnnotatonView: MGLUserLocationAnnotationView { private func addUserMarkerView(with avatarData: AvatarInputProtocol) { - guard let avatarImageView = UIHostingController(rootView: LocationSharingUserMarkerView(avatarData: avatarData)).view else { + guard let avatarImageView = UIHostingController(rootView: LocationSharingUserMarkerView(isMarker: true, avatarData: avatarData)).view else { return } From 82fe71c6a16833f81805672501ebf23cf54ce727 Mon Sep 17 00:00:00 2001 From: MaximeE Date: Thu, 17 Mar 2022 16:56:53 +0100 Subject: [PATCH 023/135] 5720: Changelog --- changelog.d/5720.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5720.change diff --git a/changelog.d/5720.change b/changelog.d/5720.change new file mode 100644 index 000000000..0b0eeee50 --- /dev/null +++ b/changelog.d/5720.change @@ -0,0 +1 @@ +Location Sharing: Update UI on location sharing view From 613fab79260e4c06a76477f87372ad6de53cfc3a Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 18 Mar 2022 12:57:47 +0300 Subject: [PATCH 024/135] Add strings --- Riot/Assets/en.lproj/Vector.strings | 3 +++ Riot/Generated/Strings.swift | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index af5aa32be..bd0769889 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -470,6 +470,9 @@ Tap the + to start adding people."; "threads_empty_tip" = "Tip: Tap a message and use “Thread” to start one."; "threads_empty_show_all_threads" = "Show all threads"; "message_from_a_thread" = "From a thread"; +"threads_notice_title" = "Threads no longer experimental 🎉"; +"threads_notice_information" = "All threads created during the experimental period will now be rendered as regular replies.

This will be a one-off transition, as threads are now part of the Matrix specification."; +"threads_notice_done" = "Got it"; "media_type_accessibility_image" = "Image"; "media_type_accessibility_audio" = "Audio"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ca1a6125a..91b8eb759 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7567,6 +7567,18 @@ public class VectorL10n: NSObject { public static var threadsEmptyTitle: String { return VectorL10n.tr("Vector", "threads_empty_title") } + /// Got it + public static var threadsNoticeDone: String { + return VectorL10n.tr("Vector", "threads_notice_done") + } + /// All threads created during the experimental period will now be rendered as regular replies.

This will be a one-off transition, as threads are now part of the Matrix specification. + public static var threadsNoticeInformation: String { + return VectorL10n.tr("Vector", "threads_notice_information") + } + /// Threads no longer experimental 🎉 + public static var threadsNoticeTitle: String { + return VectorL10n.tr("Vector", "threads_notice_title") + } /// Threads public static var threadsTitle: String { return VectorL10n.tr("Vector", "threads_title") From 1b8c50a94fc3c4f3472d87c347671a0d30117a28 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 18 Mar 2022 13:21:06 +0300 Subject: [PATCH 025/135] Add notice displayed setting --- Riot/Managers/Settings/RiotSettings.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 71f44b1d8..7b6d94b4f 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -347,7 +347,10 @@ final class RiotSettings: NSObject { @UserDefault(key: "hideSpaceBetaAnnounce", defaultValue: false, storage: defaults) var hideSpaceBetaAnnounce - + + @UserDefault(key: "threadsNoticeDisplayed", defaultValue: false, storage: defaults) + var threadsNoticeDisplayed + // MARK: - Version check @UserDefault(key: "versionCheckNextDisplayDateTimeInterval", defaultValue: 0.0, storage: defaults) From fc362e431ad025c59b63731c45077d864add6c43 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 18 Mar 2022 13:21:24 +0300 Subject: [PATCH 026/135] Add HTML rendering for labels --- Riot/Categories/UILabel.swift | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Riot/Categories/UILabel.swift b/Riot/Categories/UILabel.swift index ed1a7946d..4ea171d21 100644 --- a/Riot/Categories/UILabel.swift +++ b/Riot/Categories/UILabel.swift @@ -38,4 +38,38 @@ extension UILabel { self.preferredMaxLayoutWidth = width } } + + /// Sets an HTML string into the receiver. Does not support custom fonts but considers receiver's font size. + /// - Parameter htmlText: HTML text to be rendered. + @objc func setHTMLFromString(_ htmlText: String) { + let htmlTemplate = """ + + + + + + + \(htmlText) + + + """ + + guard let data = htmlTemplate.data(using: .utf8), + let attributedString = try? NSAttributedString( + data: data, + options: [.documentType: NSAttributedString.DocumentType.html], + documentAttributes: nil + ) else { + return + } + + self.attributedText = attributedString + } + } From ecb4fd07a803e09b4b67bfaa669ca65629c90d92 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 18 Mar 2022 13:22:47 +0300 Subject: [PATCH 027/135] Implement notice screen --- Riot/Generated/Storyboards.swift | 5 + .../ThreadsNoticeViewController.storyboard | 109 ++++++++++++++ .../Notice/ThreadsNoticeViewController.swift | 142 ++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 Riot/Modules/Threads/Notice/ThreadsNoticeViewController.storyboard create mode 100644 Riot/Modules/Threads/Notice/ThreadsNoticeViewController.swift diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift index ca9af8f55..ac4781afb 100644 --- a/Riot/Generated/Storyboards.swift +++ b/Riot/Generated/Storyboards.swift @@ -294,6 +294,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: ThreadListViewController.self) } + internal enum ThreadsNoticeViewController: StoryboardType { + internal static let storyboardName = "ThreadsNoticeViewController" + + internal static let initialScene = InitialSceneType(storyboard: ThreadsNoticeViewController.self) + } internal enum UserVerificationSessionStatusViewController: StoryboardType { internal static let storyboardName = "UserVerificationSessionStatusViewController" diff --git a/Riot/Modules/Threads/Notice/ThreadsNoticeViewController.storyboard b/Riot/Modules/Threads/Notice/ThreadsNoticeViewController.storyboard new file mode 100644 index 000000000..a8c1e4453 --- /dev/null +++ b/Riot/Modules/Threads/Notice/ThreadsNoticeViewController.storyboard @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Threads/Notice/ThreadsNoticeViewController.swift b/Riot/Modules/Threads/Notice/ThreadsNoticeViewController.swift new file mode 100644 index 000000000..16fd75f75 --- /dev/null +++ b/Riot/Modules/Threads/Notice/ThreadsNoticeViewController.swift @@ -0,0 +1,142 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +class ThreadsNoticeViewController: UIViewController { + + // MARK: Outlets + + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var separatorLineView: UIView! + @IBOutlet private weak var informationLabel: UILabel! + @IBOutlet private weak var doneButton: UIButton! + + // MARK: Private + + private var theme: Theme! + + // MARK: Public + + @objc var didTapDoneButton: (() -> Void)? + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + self.setupViews() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Hide back button + self.navigationItem.setHidesBackButton(true, animated: animated) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return self.theme.statusBarStyle + } + + // MARK: - Setup + + @objc class func instantiate() -> ThreadsNoticeViewController { + let viewController = StoryboardScene.ThreadsNoticeViewController.initialScene.instantiate() + viewController.theme = ThemeService.shared().theme + return viewController + } + + // MARK: - Private + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, + selector: #selector(themeDidChange), + name: .themeServiceDidChangeTheme, + object: nil) + } + + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } + + private func setupViews() { + self.vc_removeBackTitle() + + self.titleLabel.text = VectorL10n.threadsNoticeTitle + self.informationLabel.setHTMLFromString(VectorL10n.threadsNoticeInformation) + self.doneButton.setTitle(VectorL10n.threadsNoticeDone, for: .normal) + } + + // MARK: - Actions + + @IBAction private func doneButtonAction(_ sender: UIButton) { + self.didTapDoneButton?() + } + +} + +// MARK: - Themable + +extension ThreadsNoticeViewController: Themable { + + func update(theme: Theme) { + self.theme = theme + + self.view.backgroundColor = theme.colors.background + + if let navigationBar = self.navigationController?.navigationBar { + theme.applyStyle(onNavigationBar: navigationBar) + } + + self.titleLabel.textColor = theme.textPrimaryColor + self.separatorLineView.backgroundColor = theme.colors.system + self.informationLabel.textColor = theme.textPrimaryColor + + self.doneButton.vc_setBackgroundColor(theme.tintColor, for: .normal) + self.doneButton.setTitleColor(theme.baseTextPrimaryColor, for: .normal) + } + +} + +// MARK: - SlidingModalPresentable + +extension ThreadsNoticeViewController: SlidingModalPresentable { + + func allowsDismissOnBackgroundTap() -> Bool { + return false + } + + func layoutHeightFittingWidth(_ width: CGFloat) -> CGFloat { + guard let view = ThreadsNoticeViewController.instantiate().view else { + return 0 + } + + view.widthAnchor.constraint(equalToConstant: width).isActive = true + view.setNeedsLayout() + view.layoutIfNeeded() + + let fittingSize = CGSize(width: width, height: UIView.layoutFittingCompressedSize.height) + + return view.systemLayoutSizeFitting(fittingSize).height + + UIWindow().safeAreaInsets.top + + UIWindow().safeAreaInsets.bottom + } + +} From 54f2228b75e1e285f5509e77f60885c532dbbb6e Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 18 Mar 2022 13:23:26 +0300 Subject: [PATCH 028/135] Display threads notice if not displayed before --- Riot/Modules/Room/RoomViewController.m | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 12fb57ec8..d0b361c2c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -203,6 +203,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController; @property (nonatomic, strong) RoomParticipantsInviteCoordinatorBridgePresenter *participantsInvitePresenter; @property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsBridgePresenter; +@property (nonatomic, strong) SlidingModalPresenter *threadsNoticeModalPresenter; @property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded; @property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden; @property (nonatomic, getter=isMissedDiscussionsBadgeHidden) BOOL missedDiscussionsBadgeHidden; @@ -634,6 +635,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } self.showSettingsInitially = NO; + + if (!RiotSettings.shared.threadsNoticeDisplayed) + { + [self showThreadsNotice]; + } } - (void)viewDidDisappear:(BOOL)animated @@ -6741,6 +6747,35 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; #pragma mark - Threads +- (void)showThreadsNotice +{ + if (!self.threadsNoticeModalPresenter) + { + self.threadsNoticeModalPresenter = [SlidingModalPresenter new]; + } + + [self.threadsNoticeModalPresenter dismissWithAnimated:NO completion:nil]; + + ThreadsNoticeViewController *threadsNoticeVC = [ThreadsNoticeViewController instantiate]; + + MXWeakify(self); + + threadsNoticeVC.didTapDoneButton = ^{ + + MXStrongifyAndReturnIfNil(self); + + [self.threadsNoticeModalPresenter dismissWithAnimated:YES completion:^{ + RiotSettings.shared.threadsNoticeDisplayed = YES; + }]; + }; + + [self.threadsNoticeModalPresenter present:threadsNoticeVC + from:self.presentedViewController?:self + animated:YES + options:SlidingModalPresenter.SpanningOption + completion:nil]; +} + - (void)openThreadWithId:(NSString *)threadId { if (self.threadsBridgePresenter) From d3f0fe900f5a106a562ed7b360948ee4cb48ccf2 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 18 Mar 2022 13:24:10 +0300 Subject: [PATCH 029/135] Add changelog --- changelog.d/5770.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5770.feature diff --git a/changelog.d/5770.feature b/changelog.d/5770.feature new file mode 100644 index 000000000..c46dd12ea --- /dev/null +++ b/changelog.d/5770.feature @@ -0,0 +1 @@ +RoomViewController: Display threads notice if not displayed before. From 1fbd92d5d5fecd2aafec054a66fa4b530b2914ae Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 18 Mar 2022 13:32:18 +0300 Subject: [PATCH 030/135] Display notice for threads enabled users only --- Riot/Modules/Room/RoomViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index d0b361c2c..9bb1da705 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -636,7 +636,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.showSettingsInitially = NO; - if (!RiotSettings.shared.threadsNoticeDisplayed) + if (!RiotSettings.shared.threadsNoticeDisplayed && RiotSettings.shared.enableThreads) { [self showThreadsNotice]; } From 9ae87a196955a502ac84c4f99fdc4796b7ce9fc9 Mon Sep 17 00:00:00 2001 From: iaiz Date: Fri, 18 Mar 2022 10:14:49 +0000 Subject: [PATCH 031/135] Translated using Weblate (Spanish) Currently translated at 100.0% (7 of 7 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/es/ --- Riot/Assets/es.lproj/InfoPlist.strings | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/es.lproj/InfoPlist.strings b/Riot/Assets/es.lproj/InfoPlist.strings index db8c99db1..1f83ea895 100644 --- a/Riot/Assets/es.lproj/InfoPlist.strings +++ b/Riot/Assets/es.lproj/InfoPlist.strings @@ -1,7 +1,8 @@ // Permissions usage explanations "NSCameraUsageDescription" = "La cámara se usa para sacar fotos, vídeos y hacer videollamadas."; "NSPhotoLibraryUsageDescription" = "La biblioteca de fotos se usa para enviar fotos y vídeos."; -"NSMicrophoneUsageDescription" = "El micrófono se usa para grabar vídeos y realizar llamadas."; -"NSContactsUsageDescription" = "Para mostrarte cuáles de tus contactos ya utilizan Matrix, Element puede enviar las direcciones de correo electrónico y números telefónicos de tu agenda de contactos a tu Servidor de Identidad de Matrix. En los casos que se puede, tu información personal se cifra antes de ser enviada - por favor consulta la política de privacidad de tu Servidor de Identidad."; +"NSMicrophoneUsageDescription" = "Element necesita usar tu micrófono para hacer y recibir llamadas y grabar vídeos y mensajes de voz."; +"NSContactsUsageDescription" = "Element te mostrará tus contactos para que les puedas invitar a una conversación."; "NSFaceIDUsageDescription" = "Face ID se usa para acceder a tu aplicación."; "NSCalendarsUsageDescription" = "Mostrar tus reuniones en la aplicación."; +"NSLocationWhenInUseUsageDescription" = "Cuando compartes tu ubicación con otras personas, Element necesita acceso para que puedan verla en el mapa."; From 5e2b6baf3443eda128fff803a69a4126bc322876 Mon Sep 17 00:00:00 2001 From: iaiz Date: Fri, 18 Mar 2022 10:13:04 +0000 Subject: [PATCH 032/135] Translated using Weblate (Spanish) Currently translated at 100.0% (49 of 49 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/es/ --- Riot/Assets/es.lproj/Localizable.strings | 57 ++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/Riot/Assets/es.lproj/Localizable.strings b/Riot/Assets/es.lproj/Localizable.strings index dbac976df..6bd52f85a 100644 --- a/Riot/Assets/es.lproj/Localizable.strings +++ b/Riot/Assets/es.lproj/Localizable.strings @@ -70,3 +70,60 @@ /* New message indicator on a room */ "MESSAGE_IN_X" = "Mensaje en %@"; "MESSAGE_PROTECTED" = "Nuevo mensaje"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (llamada en grupo)"; + +/* A user added a Jitsi call to a room */ +"GROUP_CALL_STARTED" = "Llamada en grupo empezada"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ ha actualizado su perfil"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ ha cambiado su foto de perfil"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ ha cambiado su nombre"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ ha cambiado su nombre a %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ ha enviado una reacción"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ ha reaccionado %@"; + +/* New file message from a specific person, not referencing a room. */ +"LOCATION_FROM_USER" = "%@ ha compartido su ubicación"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ ha enviado un archivo %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ ha enviado un mensaje de voz"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ ha enviado un audio %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ ha enviado un vídeo"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ ha enviado una imagen"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ ha respondido en %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ ha respondido"; +/** General **/ + +"NOTIFICATION" = "Notificación"; From 4631fb96897a5631a00d20e594f717405c941239 Mon Sep 17 00:00:00 2001 From: iaiz Date: Fri, 18 Mar 2022 10:20:39 +0000 Subject: [PATCH 033/135] Translated using Weblate (Spanish) Currently translated at 51.8% (971 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/es/ --- Riot/Assets/es.lproj/Vector.strings | 85 +++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/Riot/Assets/es.lproj/Vector.strings b/Riot/Assets/es.lproj/Vector.strings index 39c7fd78c..402de1fe3 100644 --- a/Riot/Assets/es.lproj/Vector.strings +++ b/Riot/Assets/es.lproj/Vector.strings @@ -49,7 +49,7 @@ "auth_optional_phone_placeholder" = "Número telefónico (opcional)"; "auth_phone_placeholder" = "Número telefónico"; "auth_repeat_password_placeholder" = "Repite la contraseña"; -"auth_repeat_new_password_placeholder" = "Confirma tu contraseña nueva"; +"auth_repeat_new_password_placeholder" = "Confirma la nueva contraseña de tu cuenta de Matrix"; "auth_home_server_placeholder" = "URL (ej. https://matrix.org)"; "auth_identity_server_placeholder" = "URL (ej.: https://matrix.org)"; "auth_invalid_login_param" = "Nombre de usuario y/o contraseña incorrectos"; @@ -115,7 +115,7 @@ "auth_missing_phone" = "Falta el número telefónico"; "auth_missing_email_or_phone" = "Falta la dirección de correo electrónico o el número telefónico"; "auth_untrusted_id_server" = "El servidor de identidad no es de confianza"; -"auth_forgot_password" = "¿Olvidaste tu contraseña?"; +"auth_forgot_password" = "¿Has olvidado la contraseña de tu cuenta de Matrix?"; "auth_email_not_found" = "No se pudo enviar el correo electrónico: No se encontró esta dirección de correo electrónico"; "auth_email_validation_message" = "Por favor consulta tu correo electrónico para continuar con el registro"; "auth_msisdn_validation_title" = "Verificación Pendiente"; @@ -231,7 +231,7 @@ "room_event_action_redact" = "Eliminar"; "room_event_action_more" = "Más"; "room_event_action_share" = "Compartir"; -"room_event_action_permalink" = "Enlace Permanente"; +"room_event_action_permalink" = "Copiar enlace al mensaje"; "room_event_action_view_source" = "Ver Fuente"; "room_event_action_view_decrypted_source" = "Ver Fuente Descifrada"; "room_event_action_report" = "Reportar contenido"; @@ -293,7 +293,7 @@ "settings_ignored_users" = "USUARIOS IGNORADOS"; "settings_contacts" = "CONTACTOS LOCALES"; "settings_advanced" = "AVANZADO"; -"settings_other" = "OTRO"; +"settings_other" = "OTROS"; "settings_labs" = "LABORATORIOS"; "settings_flair" = "Mostrar insignia donde esté permitido"; "settings_devices" = "DISPOSITIVOS"; @@ -1036,3 +1036,80 @@ // New "notice_room_join_rule_invite" = "%@ ha hecho que la sala solo sea accesible por invitación."; "resume_call" = "Volver a la llamada"; +"settings_key_backup_info_checking" = "Comprobando…"; +"settings_messages_containing_display_name" = "Mi nombre"; +"settings_three_pids_management_information_part2" = "Descubrir"; +"threads_empty_tip" = "Consejo: toca un mensaje y dale a «Hilo» para empezar uno nuevo."; +"threads_empty_info_my" = "Responde en un hilo que ya exista o toca un mensaje y dale a «Hilo» para empezar uno nuevo."; +"threads_empty_info_all" = "Los hilos ayudan a que las conversaciones no se desvíen y sean fáciles de seguir."; +"threads_empty_title" = "Mantén las conversaciones organizadas usando los hilos"; +"thread_copy_link_to_thread" = "Copiar enlace al hilo"; +"room_event_copy_link_info" = "Enlace copiado al portapapeles."; +"room_event_action_delete_confirmation_message" = "¿Seguro que quieres borrar este no enviado?"; +"room_participants_security_information_room_not_encrypted_for_dm" = "Estos mensajes no están cifrados de extremo a extremo."; +"store_promotional_text" = "Aplicación para chatear y colaborar que respeta tu privacidad y funciona con una red abierta. Descentralizada, para que puedas mantener el control. Sin minería de datos, puertas traseras ni acceso de terceros."; +"settings_call_invitations" = "Llamadas"; +"settings_room_invitations" = "Invitaciones a salas"; +"settings_messages_containing_keywords" = "Palabras clave"; +"settings_messages_containing_user_name" = "Tu nombre de usuario"; +"settings_messages_containing_at_room" = "@room"; +"settings_security" = "SEGURIDAD"; +"settings_three_pids_management_information_part3" = "."; +"settings_about" = "ACERCA DE"; +"settings_notifications" = "NOTIFICACIONES"; +"settings_links" = "ENLACES"; +"settings_sending_media" = "ENVÍA IMÁGENES Y VÍDEOS"; +"threads_empty_show_all_threads" = "Ver todos los hilos"; +"message_from_a_thread" = "Desde un hilo"; +"threads_action_my_threads" = "Mis hilos"; +"threads_action_all_threads" = "Todos los hilos"; +"threads_title" = "Hilos"; + +// MARK: Threads +"room_thread_title" = "Hilo"; +"room_no_privileges_to_create_group_call" = "Tienes que ser un administrador o moderador para empezar una llamada."; +"room_accessibility_thread_more" = "Más"; +"room_accessibility_threads" = "Hilos"; +"room_message_edits_history_title" = "Ediciones de mensajes"; +"room_event_action_reaction_history" = "Historial de reacciones"; +"room_event_action_reply_in_thread" = "Hilo"; +"room_event_action_view_in_room" = "Ver en la sala"; +"room_event_action_forward" = "Reenviar"; +"room_event_action_end_poll" = "Terminar encuesta"; +"room_event_action_remove_poll" = "Quitar encuesta"; +"room_participants_action_security_status_complete_security" = "Completar la seguridad"; +"room_participants_leave_success" = "Salió de la sala"; +"find_your_contacts_footer" = "Puedes desactivar esto cuando quieres desde ajustes."; +"find_your_contacts_button_title" = "Encuentra tus contactos"; +"find_your_contacts_message" = "Deja que %@ te muestre tus contactos, para que puedas empezar a hablar rápidamente con quienes más te relacionas."; +"contacts_address_book_permission_denied_alert_message" = "Para activar los contactos, ve a los ajustes de tu dispositivo."; +"contacts_address_book_permission_denied_alert_title" = "Contactos desactivados"; +"search_filter_placeholder" = "Filtrar"; +"room_recents_suggested_rooms_section" = "SALAS SUGERIDAS"; +"onboarding_use_case_existing_server_button" = "Conectar a un servidor"; +"onboarding_use_case_existing_server_message" = "¿Te quieres unir a un servidor que ya existe?"; +"onboarding_use_case_skip_button" = "saltar esta pregunta"; +/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ +"onboarding_use_case_not_sure_yet" = "¿Todavía no lo sabes? Puedes %@"; +"onboarding_use_case_community_messaging" = "Comunidades"; +"onboarding_use_case_work_messaging" = "Equipos"; +"onboarding_use_case_personal_messaging" = "Familia y amigos"; +"onboarding_use_case_message" = "Te vamos a ayudar a conectarte."; +"onboarding_use_case_title" = "¿Con quién vas a hablar más?"; +"onboarding_splash_page_4_message" = "Element también funciona genial en el trabajo. Usado por algunas de las organizaciones más seguras."; +"onboarding_splash_page_4_title_no_pun" = "Mensajería para tu equipo."; +"onboarding_splash_page_3_message" = "Cifrado de extremo a extremo, y no hace falta que nos des tu número de móvil. Sin anuncios ni minería de datos."; +"onboarding_splash_page_3_title" = "Mensajería segura."; +"onboarding_splash_page_2_message" = "Decide cómo quieres guardar tus conversaciones, te damos el control y la independencia. Gracias a Matrix."; +"onboarding_splash_page_2_title" = "Tú mandas."; +"onboarding_splash_page_1_message" = "Comunicación segura e independiente que te ofrece el mismo nivel de privacidad que las conversaciones en persona dentro de tu casa."; +"onboarding_splash_page_1_title" = "Toma el control de tus conversaciones."; +"onboarding_splash_login_button_title" = "Ya tengo cuenta"; + +// Onboarding +"onboarding_splash_register_button_title" = "Crear una cuenta"; +"accessibility_button_label" = "botón"; +"done" = "Terminado"; +"open" = "Abrir"; +"joined" = "Se unió"; +"enable" = "Activar"; From 47c11df407d5e5874fca06fa331b4cc7edd75e92 Mon Sep 17 00:00:00 2001 From: Genbuchan Date: Fri, 18 Mar 2022 17:58:12 +0000 Subject: [PATCH 034/135] Translated using Weblate (Japanese) Currently translated at 74.2% (1389 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ --- Riot/Assets/ja.lproj/Vector.strings | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 9a3211902..2955fcd4e 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -1477,7 +1477,7 @@ "notification_settings_always_notify" = "常に通知"; "notification_settings_never_notify" = "決して通知しない"; "notification_settings_word_to_match" = "一致する単語"; -"notification_settings_highlight" = "Highlight"; +"notification_settings_highlight" = "ハイライト"; "notification_settings_custom_sound" = "カスタムサウンド"; "notification_settings_per_room_notifications" = "1ルームあたりの通知"; "notification_settings_per_sender_notifications" = "送信者ごとの通知"; @@ -1600,3 +1600,9 @@ "settings_sending_media" = "画像と動画の送信"; "invite_friends_share_text" = "%@ での連絡先: %@"; "side_menu_action_invite_friends" = "招待する"; +"call_more_actions_change_audio_device" = "オーディオデバイスを変更"; +"call_more_actions_dialpad" = "ダイヤルパッド"; +"onboarding_splash_login_button_title" = "既にアカウントを持っています"; + +// Onboarding +"onboarding_splash_register_button_title" = "アカウントを作成"; From 157522843ccb5fc940115ba7bce14075b2778eb8 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 18 Mar 2022 16:20:04 +0000 Subject: [PATCH 035/135] Translated using Weblate (Slovak) Currently translated at 100.0% (1871 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 1c0474721..1c27c3981 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -490,7 +490,7 @@ "unknown_devices_verify" = "Overiť…"; "media_type_accessibility_sticker" = "Nálepka"; "media_type_accessibility_file" = "Súbor"; -"media_type_accessibility_location" = "Miesto"; +"media_type_accessibility_location" = "Poloha"; "media_type_accessibility_video" = "Video"; "media_type_accessibility_audio" = "Audio"; "media_type_accessibility_image" = "Obrázok"; From 51fbb7b8587ce79848582c8af53af1290591fd4e Mon Sep 17 00:00:00 2001 From: Linerly Date: Sun, 20 Mar 2022 07:41:05 +0000 Subject: [PATCH 036/135] Translated using Weblate (Indonesian) Currently translated at 100.0% (1871 of 1871 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 3d8478f45..50b6eba23 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -298,7 +298,7 @@ "emoji_picker_flags_category" = "Bendera"; "emoji_picker_symbols_category" = "Simbol"; "emoji_picker_objects_category" = "Benda"; -"emoji_picker_activity_category" = "Aktifitas"; +"emoji_picker_activity_category" = "Aktivitas"; // MARK: Emoji picker "emoji_picker_title" = "Reaksi"; From c9334eb5e99bba8f71b5838f2c3af7b2a6a0a9ba Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 21 Mar 2022 10:39:50 +0000 Subject: [PATCH 037/135] Revert "fix merge" This reverts commit 3c26c9a7da6f161076ce68c8b51ba52adf34fc13. --- Podfile | 8 +- Podfile.lock | 9 +- .../Modules/Settings/SettingsViewController.m | 1 - .../SideMenu/SideMenuCoordinator.swift | 25 +- .../Modules/Common/Mock/MockAppScreens.swift | 2 +- .../TemplateRoomsCoordinator.swift | 8 +- .../TemplateRoomChatCoordinator.swift | 2 +- .../MockTemplateRoomChatScreenState.swift | 2 +- .../MatrixSDK/TemplateRoomChatService.swift | 29 +- .../Mock/MockTemplateRoomChatService.swift | 2 - .../TemplateRoomChatModels.swift | 2 +- .../TemplateRoomChat/View/ImageViewer.swift | 294 ------------------ .../View/TemplateRoomChat.swift | 12 +- .../TemplateRoomChatBubbleContentView.swift | 4 +- .../View/TemplateRoomChatBubbleImage.swift | 21 +- .../View/TemplateRoomChatBubbleView.swift | 4 +- RiotSwiftUI/RiotSwiftUIApp.swift | 2 +- 17 files changed, 36 insertions(+), 391 deletions(-) delete mode 100644 RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/ImageViewer.swift diff --git a/Podfile b/Podfile index fd699f7d6..07fe178d6 100644 --- a/Podfile +++ b/Podfile @@ -13,9 +13,9 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -# $matrixSDKVersion = '= 0.22.6' - $matrixSDKVersion = :local -#$matrixSDKVersion = { :branch => 'develop'} +$matrixSDKVersion = '= 0.22.6' +# $matrixSDKVersion = :local +# $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } ######################################## @@ -154,4 +154,4 @@ post_install do |installer| config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)', '-Xcc', '-Wno-nullability-completeness'] end end -end +end \ No newline at end of file diff --git a/Podfile.lock b/Podfile.lock index 1a3719d2a..1c3e87179 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -115,8 +115,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (from `../matrix-ios-sdk/MatrixSDK.podspec`) - - MatrixSDK/JingleCallStack (from `../matrix-ios-sdk/MatrixSDK.podspec`) + - MatrixSDK (= 0.22.6) + - MatrixSDK/JingleCallStack (= 0.22.6) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -156,6 +156,7 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging + - MatrixSDK - OLMKit - PostHog - ReadMoreTextView @@ -176,8 +177,6 @@ EXTERNAL SOURCES: AnalyticsEvents: :branch: release/swift :git: https://github.com/matrix-org/matrix-analytics-events.git - MatrixSDK: - :path: "../matrix-ios-sdk/MatrixSDK.podspec" CHECKOUT OPTIONS: AnalyticsEvents: @@ -226,6 +225,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: add4568acff884a72cf19933027bbc5507725185 +PODFILE CHECKSUM: 16aaf5e59ec902619fbfd799939f044728a92ab7 COCOAPODS: 1.11.2 diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 4cff1dac3..ed040f53d 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2478,7 +2478,6 @@ TableViewSectionsDelegate> else if (row == LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS) { cell = [self buildAutoReportDecryptionErrorsCellForTableView:tableView atIndexPath:indexPath]; - } else if (row == LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX) { MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; diff --git a/Riot/Modules/SideMenu/SideMenuCoordinator.swift b/Riot/Modules/SideMenu/SideMenuCoordinator.swift index 962228a0f..7f5280fd8 100644 --- a/Riot/Modules/SideMenu/SideMenuCoordinator.swift +++ b/Riot/Modules/SideMenu/SideMenuCoordinator.swift @@ -67,7 +67,6 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType { private var createSpaceCoordinator: SpaceCreationCoordinator? private var createRoomCoordinator: CreateRoomCoordinator? private var spaceSettingsCoordinator: Coordinator? - private var chatCoordinator: TemplateRoomsCoordinator? // MARK: Public @@ -239,28 +238,10 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType { } private func showInviteFriends(from sourceView: UIView?) { -// let myUserId = self.parameters.userSessionsService.mainUserSession?.userId ?? "" + let myUserId = self.parameters.userSessionsService.mainUserSession?.userId ?? "" -// let inviteFriendsPresenter = InviteFriendsPresenter() -// inviteFriendsPresenter.present(for: myUserId, from: self.sideMenuViewController, sourceView: sourceView, animated: true) - - guard let session = self.parameters.userSessionsService.mainUserSession?.matrixSession else { - return - } - let coordinator = TemplateRoomsCoordinator(parameters: TemplateRoomsCoordinatorParameters(session: session)) - coordinator.callback = { [weak self] in - guard let self = self else { return } - - coordinator.toPresentable().dismiss(animated: true) { - self.chatCoordinator = nil - } - } - - let presentable = coordinator.toPresentable() - presentable.presentationController?.delegate = self - toPresentable().present(presentable, animated: true, completion: nil) - coordinator.start() - self.chatCoordinator = coordinator + let inviteFriendsPresenter = InviteFriendsPresenter() + inviteFriendsPresenter.present(for: myUserId, from: self.sideMenuViewController, sourceView: sourceView, animated: true) } private func showMenu(forSpaceWithId spaceId: String, from sourceView: UIView?) { diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 963b9954a..3c5909a4c 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -17,7 +17,7 @@ import Foundation /// The static list of mocked screens in RiotSwiftUI -@available(iOS 15.0, *) +@available(iOS 14.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ MockOnboardingCongratulationsScreenState.self, diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift index f593eb32d..be453b948 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift @@ -46,7 +46,7 @@ final class TemplateRoomsCoordinator: Coordinator, Presentable { func start() { - if #available(iOS 15.0, *) { + if #available(iOS 14.0, *) { MXLog.debug("[TemplateRoomsCoordinator] did start.") let rootCoordinator = self.createTemplateRoomListCoordinator() rootCoordinator.start() @@ -71,7 +71,7 @@ final class TemplateRoomsCoordinator: Coordinator, Presentable { // MARK: - Private - @available(iOS 15.0, *) + @available(iOS 14.0, *) private func createTemplateRoomListCoordinator() -> TemplateRoomListCoordinator { let coordinator: TemplateRoomListCoordinator = TemplateRoomListCoordinator(parameters: TemplateRoomListCoordinatorParameters(session: parameters.session)) @@ -88,13 +88,13 @@ final class TemplateRoomsCoordinator: Coordinator, Presentable { return coordinator } - @available(iOS 15.0, *) + @available(iOS 14.0, *) private func createTemplateRoomChatCoordinator(room: MXRoom) -> TemplateRoomChatCoordinator { let coordinator: TemplateRoomChatCoordinator = TemplateRoomChatCoordinator(parameters: TemplateRoomChatCoordinatorParameters(room: room)) return coordinator } - @available(iOS 15.0, *) + @available(iOS 14.0, *) func showTemplateRoomChat(roomId: String) { guard let room = parameters.session.room(withRoomId: roomId) else { MXLog.error("[TemplateRoomsCoordinator] Failed to find room by selected Id.") diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift index 8b4d9cb86..d9a5e10f2 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift @@ -38,7 +38,7 @@ final class TemplateRoomChatCoordinator: Coordinator, Presentable { // MARK: - Setup - @available(iOS 15.0, *) + @available(iOS 14.0, *) init(parameters: TemplateRoomChatCoordinatorParameters) { self.parameters = parameters let viewModel = TemplateRoomChatViewModel(templateRoomChatService: TemplateRoomChatService(room: parameters.room)) diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift index 6c7289a27..925c5c614 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift @@ -19,7 +19,7 @@ import SwiftUI /// Using an enum for the screen allows you define the different state cases with /// the relevant associated data for each case. -@available(iOS 15.0, *) +@available(iOS 14.0, *) enum MockTemplateRoomChatScreenState: MockScreenState, CaseIterable { // A case for each state you want to represent // with specific, minimal associated data that will allow you diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift index 3dc571939..6d231f744 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift @@ -16,7 +16,6 @@ import Foundation import Combine -import MatrixSDK @available(iOS 14.0, *) class TemplateRoomChatService: TemplateRoomChatServiceProtocol { @@ -97,21 +96,20 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { return events .filter({ event in event.type == kMXEventTypeStringRoomMessage - && (event.content[kMXMessageTypeKey] as? String == kMXMessageTypeText - || event.content[kMXMessageTypeKey] as? String == kMXMessageTypeImage) + && event.content[kMXMessageTypeKey] as? String == kMXMessageTypeText // TODO: New to our SwiftUI Template? Why not implement another message type like image? }) .compactMap({ event -> TemplateRoomChatMessage? in guard let eventId = event.eventId, + let body = event.content[kMXMessageBodyKey] as? String, let sender = senderForMessage(event: event) else { return nil } - let messageContent = messageContentForEvent(event: event) return TemplateRoomChatMessage( id: eventId, - content: messageContent, + content: .text(TemplateRoomChatMessageTextContent(body: body)), sender: sender, timestamp: Date(timeIntervalSince1970: TimeInterval(event.originServerTs / 1000)) ) @@ -126,25 +124,4 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { let avatarUrl = eventFormatter.senderAvatarUrl(for: event, with: roomState) return TemplateRoomChatMember(id: sender, avatarUrl: avatarUrl, displayName: displayName) } - - private func messageContentForEvent(event: MXEvent) -> TemplateRoomChatMessageContent { - switch event.content[kMXMessageTypeKey] as? String { - case kMXMessageTypeText: - let body = event.content[kMXMessageBodyKey] as? String ?? "" - return .text(TemplateRoomChatMessageTextContent(body: body)) - case kMXMessageTypeImage: - let url: URL -// room.mxSession.mediaManager. - if let contentURL = event.content["url"] as? String, - let info = event.content["info"] as? [String: Any], - let localImagePath = MXMediaManager.cachePath(forMatrixContentURI: contentURL, andType:info["mimetype"] as? String, inFolder: event.roomId) { - url = URL(fileURLWithPath: localImagePath) - } else{ - url = URL(string: "https://cahilldental.ie/wp-content/uploads/2016/10/orionthemes-placeholder-image.png")! - } - return .image(TemplateRoomChatMessageImageContent(url: url)) - default: break - } - fatalError("unsupported event type content") - } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift index 3134b2d29..8fa354911 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift @@ -24,13 +24,11 @@ class MockTemplateRoomChatService: TemplateRoomChatServiceProtocol { static let amadine = TemplateRoomChatMember(id: "@amadine:matrix.org", avatarUrl: "!aaabaa:matrix.org", displayName: "Amadine") static let mathew = TemplateRoomChatMember(id: "@mathew:matrix.org", avatarUrl: "!bbabb:matrix.org", displayName: "Mathew") - static let partyImageUrl = URL(string: "https://img.evbuc.com/https%3A%2F%2Fcdn.evbuc.com%2Fimages%2F184375039%2F474927372937%2F1%2Foriginal.20211111-155142?w=800&auto=format%2Ccompress&q=75&sharp=10&rect=0%2C236%2C4724%2C2362&s=18c17c71af6df1e5e46d630fac923834")! static let mockMessages = [ TemplateRoomChatMessage(id: "!0:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Shall I put it live?")) , sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -3)), TemplateRoomChatMessage(id: "!1:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Yea go for it! ...and then let's head to the pub")), sender: mathew, timestamp: Date(timeIntervalSinceNow: 60)), TemplateRoomChatMessage(id: "!2:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Deal.")), sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -2)), TemplateRoomChatMessage(id: "!3:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Ok, Done. 🍻")), sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -1)), - TemplateRoomChatMessage(id: "!3:matrix.org", content: .image(TemplateRoomChatMessageImageContent(url: partyImageUrl)), sender: mathew, timestamp: Date(timeIntervalSinceNow: 60 * -1)), ] var roomInitializationStatus: CurrentValueSubject var chatMessagesSubject: CurrentValueSubject<[TemplateRoomChatMessage], Never> diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift index 5e5310189..e9aa09141 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift @@ -20,7 +20,7 @@ import UIKit /// An image sent as a message. struct TemplateRoomChatMessageImageContent: Equatable { - var url: URL + var image: UIImage } /// The text content of a message sent by a user. diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/ImageViewer.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/ImageViewer.swift deleted file mode 100644 index 37c78dabb..000000000 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/ImageViewer.swift +++ /dev/null @@ -1,294 +0,0 @@ -import SwiftUI -import UIKit - -@available(iOS 13.0, *) -public struct ImageViewer: View { - @Binding var viewerShown: Bool - @Binding var image: Image - @Binding var imageOpt: Image? - @State var caption: Text? - @State var closeButtonTopRight: Bool? - - var aspectRatio: Binding? - - @State var dragOffset: CGSize = CGSize.zero - @State var dragOffsetPredicted: CGSize = CGSize.zero - - public init(image: Binding, viewerShown: Binding, aspectRatio: Binding? = nil, caption: Text? = nil, closeButtonTopRight: Bool? = false) { - _image = image - _viewerShown = viewerShown - _imageOpt = .constant(nil) - self.aspectRatio = aspectRatio - _caption = State(initialValue: caption) - _closeButtonTopRight = State(initialValue: closeButtonTopRight) - } - - public init(image: Binding, viewerShown: Binding, aspectRatio: Binding? = nil, caption: Text? = nil, closeButtonTopRight: Bool? = false) { - _image = .constant(Image(systemName: "")) - _imageOpt = image - _viewerShown = viewerShown - self.aspectRatio = aspectRatio - _caption = State(initialValue: caption) - _closeButtonTopRight = State(initialValue: closeButtonTopRight) - } - - func getImage() -> Image { - if(self.imageOpt == nil) { - return self.image - } - else { - return self.imageOpt ?? Image(systemName: "questionmark.diamond") - } - } - - @ViewBuilder - public var body: some View { - VStack { - if(viewerShown) { - ZStack { - VStack { - HStack { - - if self.closeButtonTopRight == true { - Spacer() - } - - Button(action: { self.viewerShown = false }) { - Image(systemName: "xmark") - .foregroundColor(Color(UIColor.white)) - .font(.system(size: UIFontMetrics.default.scaledValue(for: 24))) - } - - if self.closeButtonTopRight != true { - Spacer() - } - } - - Spacer() - } - .padding() - .zIndex(2) - - VStack { - ZStack { - self.getImage() - .resizable() - .aspectRatio(self.aspectRatio?.wrappedValue, contentMode: .fit) - .offset(x: self.dragOffset.width, y: self.dragOffset.height) - .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) - .pinchToZoom() - .gesture(DragGesture() - .onChanged { value in - self.dragOffset = value.translation - self.dragOffsetPredicted = value.predictedEndTranslation - } - .onEnded { value in - if((abs(self.dragOffset.height) + abs(self.dragOffset.width) > 570) || ((abs(self.dragOffsetPredicted.height)) / (abs(self.dragOffset.height)) > 3) || ((abs(self.dragOffsetPredicted.width)) / (abs(self.dragOffset.width))) > 3) { - withAnimation(.spring()) { - self.dragOffset = self.dragOffsetPredicted - } - self.viewerShown = false - - return - } - withAnimation(.interactiveSpring()) { - self.dragOffset = .zero - } - } - ) - - if(self.caption != nil) { - VStack { - Spacer() - - VStack { - Spacer() - - HStack { - Spacer() - - self.caption - .foregroundColor(.white) - .multilineTextAlignment(.center) - - Spacer() - } - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(red: 0.12, green: 0.12, blue: 0.12, opacity: (1.0 - Double(abs(self.dragOffset.width) + abs(self.dragOffset.height)) / 1000)).edgesIgnoringSafeArea(.all)) - .zIndex(1) - } - .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2))) - .onAppear() { - self.dragOffset = .zero - self.dragOffsetPredicted = .zero - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - -@available(iOS 13.0, *) -class PinchZoomView: UIView { - - weak var delegate: PinchZoomViewDelgate? - - private(set) var scale: CGFloat = 0 { - didSet { - delegate?.pinchZoomView(self, didChangeScale: scale) - } - } - - private(set) var anchor: UnitPoint = .center { - didSet { - delegate?.pinchZoomView(self, didChangeAnchor: anchor) - } - } - - private(set) var offset: CGSize = .zero { - didSet { - delegate?.pinchZoomView(self, didChangeOffset: offset) - } - } - - private(set) var isPinching: Bool = false { - didSet { - delegate?.pinchZoomView(self, didChangePinching: isPinching) - } - } - - private var startLocation: CGPoint = .zero - private var location: CGPoint = .zero - private var numberOfTouches: Int = 0 - - init() { - super.init(frame: .zero) - - let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch(gesture:))) - pinchGesture.cancelsTouchesInView = false - addGestureRecognizer(pinchGesture) - } - - required init?(coder: NSCoder) { - fatalError() - } - - @objc private func pinch(gesture: UIPinchGestureRecognizer) { - - switch gesture.state { - case .began: - isPinching = true - startLocation = gesture.location(in: self) - anchor = UnitPoint(x: startLocation.x / bounds.width, y: startLocation.y / bounds.height) - numberOfTouches = gesture.numberOfTouches - - case .changed: - if gesture.numberOfTouches != numberOfTouches { - // If the number of fingers being used changes, the start location needs to be adjusted to avoid jumping. - let newLocation = gesture.location(in: self) - let jumpDifference = CGSize(width: newLocation.x - location.x, height: newLocation.y - location.y) - startLocation = CGPoint(x: startLocation.x + jumpDifference.width, y: startLocation.y + jumpDifference.height) - - numberOfTouches = gesture.numberOfTouches - } - - scale = gesture.scale - - location = gesture.location(in: self) - offset = CGSize(width: location.x - startLocation.x, height: location.y - startLocation.y) - - case .ended, .cancelled, .failed: - withAnimation(.interactiveSpring()) { - isPinching = false - scale = 1.0 - anchor = .center - offset = .zero - } - default: - break - } - } - -} -@available(iOS 13.0, *) -protocol PinchZoomViewDelgate: AnyObject { - func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) - func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) - func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) - func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) -} - -@available(iOS 13.0, *) -struct PinchZoom: UIViewRepresentable { - - @Binding var scale: CGFloat - @Binding var anchor: UnitPoint - @Binding var offset: CGSize - @Binding var isPinching: Bool - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - func makeUIView(context: Context) -> PinchZoomView { - let pinchZoomView = PinchZoomView() - pinchZoomView.delegate = context.coordinator - return pinchZoomView - } - - func updateUIView(_ pageControl: PinchZoomView, context: Context) { } - - class Coordinator: NSObject, PinchZoomViewDelgate { - var pinchZoom: PinchZoom - - init(_ pinchZoom: PinchZoom) { - self.pinchZoom = pinchZoom - } - - func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) { - pinchZoom.isPinching = isPinching - } - - func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) { - pinchZoom.scale = scale - } - - func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) { - pinchZoom.anchor = anchor - } - - func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) { - pinchZoom.offset = offset - } - } -} - -@available(iOS 13.0, *) -struct PinchToZoom: ViewModifier { - @State var scale: CGFloat = 1.0 - @State var anchor: UnitPoint = .center - @State var offset: CGSize = .zero - @State var isPinching: Bool = false - - func body(content: Content) -> some View { - content - .scaleEffect(scale, anchor: anchor) - .offset(offset) - .overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching)) - } -} - -@available(iOS 13.0, *) -extension View { - func pinchToZoom() -> some View { - self.modifier(PinchToZoom()) - } -} diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift index c47c3300a..c714ff34b 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift @@ -17,7 +17,7 @@ import SwiftUI import Combine -@available(iOS 15.0, *) +@available(iOS 14.0, *) struct TemplateRoomChat: View { // MARK: - Properties @@ -26,8 +26,6 @@ struct TemplateRoomChat: View { @Environment(\.theme) private var theme: ThemeSwiftUI - @State var fullScreenImage: Image? - // MARK: Public @ObservedObject var viewModel: TemplateRoomChatViewModel.Context @@ -65,12 +63,6 @@ struct TemplateRoomChat: View { } } - - - .onTapGesture { - showImageViewer.toggle() - } - .overlay(ImageViewer(image: $image, viewerShown: self.$showImageViewer)) @ViewBuilder private var roomContent: some View { if case .notInitialized = viewModel.viewState.roomInitializationStatus { @@ -135,7 +127,7 @@ struct TemplateRoomChat: View { // MARK: - Previews -@available(iOS 15.0, *) +@available(iOS 14.0, *) struct TemplateRoomChat_Previews: PreviewProvider { static let stateRenderer = MockTemplateRoomChatScreenState.stateRenderer static var previews: some View { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift index 1c9958bd2..6efb16b4f 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift @@ -16,7 +16,7 @@ import SwiftUI -@available(iOS 15.0, *) +@available(iOS 14.0, *) struct TemplateRoomChatBubbleContentView: View { // MARK: - Properties @@ -44,7 +44,7 @@ struct TemplateRoomChatBubbleContentView: View { // MARK: - Previews -@available(iOS 15.0, *) +@available(iOS 14.0, *) struct TemplateRoomChatBubbleItemView_Previews: PreviewProvider { static var previews: some View { EmptyView() diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift index 4c178184b..16fa077d4 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift @@ -16,7 +16,7 @@ import SwiftUI -@available(iOS 15.0, *) +@available(iOS 14.0, *) struct TemplateRoomChatBubbleImage: View { // MARK: - Properties @@ -24,29 +24,22 @@ struct TemplateRoomChatBubbleImage: View { // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI - + // MARK: Public let imageContent: TemplateRoomChatMessageImageContent - @State var showImageViewer: Bool = false + var body: some View { - AsyncImage(url: imageContent.url) { image in - image.resizable() - .aspectRatio(contentMode: .fill) - .frame(width: CGFloat(258), height: CGFloat(150)) - .cornerRadius(8) - } placeholder: { - Color.green - } + EmptyView() } } // MARK: - Previews -@available(iOS 15.0, *) +@available(iOS 14.0, *) struct TemplateRoomChatBubbleImage_Previews: PreviewProvider { - static let exampleUrl = URL(string: "https://docs-assets.developer.apple.com/published/9c4143a9a48a080f153278c9732c03e7/17400/SwiftUI-Image-waterWheel-resize~dark@2x.png")! static var previews: some View { - TemplateRoomChatBubbleImage(imageContent: TemplateRoomChatMessageImageContent(url:exampleUrl)) + EmptyView() + // TODO: New to our SwiftUI Template? Why not implement the image item in the bubble here? } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift index 6e5a6808e..b324a9565 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift @@ -16,7 +16,7 @@ import SwiftUI -@available(iOS 15.0, *) +@available(iOS 14.0, *) struct TemplateRoomChatBubbleView: View { // MARK: - Properties @@ -52,7 +52,7 @@ struct TemplateRoomChatBubbleView: View { // MARK: - Previews -@available(iOS 15.0, *) +@available(iOS 14.0, *) struct TemplateRoomChatBubbleView_Previews: PreviewProvider { static let bubble = TemplateRoomChatBubble( id: "111", diff --git a/RiotSwiftUI/RiotSwiftUIApp.swift b/RiotSwiftUI/RiotSwiftUIApp.swift index b1527da23..902f7327d 100644 --- a/RiotSwiftUI/RiotSwiftUIApp.swift +++ b/RiotSwiftUI/RiotSwiftUIApp.swift @@ -15,7 +15,7 @@ // import SwiftUI -@available(iOS 15.0, *) +@available(iOS 14.0, *) @main /// RiotSwiftUI screens rendered for UI Tests. struct RiotSwiftUIApp: App { From 12d0ba3ca822974d2872030770ef89637d268a12 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 21 Mar 2022 10:41:10 +0000 Subject: [PATCH 038/135] Actually fix merge. --- Riot/Modules/Settings/SettingsViewController.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index ed040f53d..4cff1dac3 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2478,6 +2478,7 @@ TableViewSectionsDelegate> else if (row == LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS) { cell = [self buildAutoReportDecryptionErrorsCellForTableView:tableView atIndexPath:indexPath]; + } else if (row == LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX) { MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; From 737a7d85c4d7128e6758c64850e94cc572902a37 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 21 Mar 2022 14:45:54 +0000 Subject: [PATCH 039/135] Fix documentation and update big report client init. --- Riot/Categories/MXBugReportRestClient+Riot.swift | 5 +---- Riot/Managers/Settings/RiotSettings.swift | 2 +- Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift | 5 ++--- Riot/Managers/UISIAutoReporter/UISIDetector.swift | 9 +++++++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Riot/Categories/MXBugReportRestClient+Riot.swift b/Riot/Categories/MXBugReportRestClient+Riot.swift index fd3fe313e..93eff76aa 100644 --- a/Riot/Categories/MXBugReportRestClient+Riot.swift +++ b/Riot/Categories/MXBugReportRestClient+Riot.swift @@ -21,10 +21,7 @@ import GBDeviceInfo extension MXBugReportRestClient { @objc static func vc_bugReportRestClient(appName: String) -> MXBugReportRestClient { - guard let client = MXBugReportRestClient(bugReportEndpoint: BuildSettings.bugReportEndpointUrlString) else { - fatalError("Could not create MXBugReportRestClient") - } - + let client = MXBugReportRestClient(bugReportEndpoint: BuildSettings.bugReportEndpointUrlString) // App info client.appName = appName client.version = AppDelegate.theDelegate().appVersion diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index b1536b05a..f90021ba3 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -147,7 +147,7 @@ final class RiotSettings: NSObject { @UserDefault(key: "enableThreads", defaultValue: false, storage: defaults) var enableThreads - /// Indicates if threads enabled in the timeline. + /// Indicates if auto reporting of decryption errors is enabled @UserDefault(key: UserDefaultsKeys.enableUISIAutoReporting, defaultValue: BuildSettings.cryptoUISIAutoReportingEnabled, storage: defaults) var enableUISIAutoReporting diff --git a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift index febd2aee3..ca64cace5 100644 --- a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift +++ b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift @@ -39,6 +39,8 @@ extension UISIAutoReportData: Codable { } @available(iOS 14.0, *) +/// Listens for failed decryption events and silently sends reports RageShake server. +/// Also requests that message senders send a matching report to have both sides of the interaction. @objcMembers class UISIAutoReporter: NSObject, UISIDetectorDelegate { struct ReportInfo: Hashable { @@ -97,7 +99,6 @@ extension UISIAutoReportData: Codable { return detector }() - var reciprocateToDeviceEventType: String { return Self.autoRsRequest } @@ -180,7 +181,6 @@ extension UISIAutoReportData: Codable { let senderKey = source.content["sender_key"] as? String let matchingIssue = source.content["recipient_rageshake"] as? String ?? "" - let uisiData = UISIAutoReportData( eventId: eventId, roomId: roomId, @@ -206,7 +206,6 @@ extension UISIAutoReportData: Codable { ) } - func add(_ session: MXSession) { sessions.append(session) detector.enabled = enabled diff --git a/Riot/Managers/UISIAutoReporter/UISIDetector.swift b/Riot/Managers/UISIAutoReporter/UISIDetector.swift index ea0561c7e..47c587a62 100644 --- a/Riot/Managers/UISIAutoReporter/UISIDetector.swift +++ b/Riot/Managers/UISIAutoReporter/UISIDetector.swift @@ -43,6 +43,8 @@ struct UISIDetectedMessage { } } +/// Detects decryption errors that occur and don't recover within a grace period. +/// see `UISIDetectorDelegate` for listening to detections. class UISIDetector: MXLiveEventListener { weak var delegate: UISIDetectorDelegate? @@ -88,7 +90,6 @@ class UISIDetector: MXLiveEventListener { self.trackedUISIs[trackedId] = timer timer.activate() } - } func onLiveToDeviceEvent(event: MXEvent) { @@ -96,13 +97,17 @@ class UISIDetector: MXLiveEventListener { delegate?.uisiReciprocateRequest(source: event) } + // MARK: - Private + private func triggerUISI(source: UISIDetectedMessage) { guard enabled else { return } MXLog.info("[UISIDetector] triggerUISI: Unable To Decrypt \(source)") self.delegate?.uisiDetected(source: source) } - static func trackedEventId(roomId: String, eventId: String) -> String { + // MARK: - Static + + private static func trackedEventId(roomId: String, eventId: String) -> String { return "\(roomId)-\(eventId)" } } From 621ffc4b9aa9dbfeea2f2f180e449d5a3020a6f6 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 21 Mar 2022 15:01:20 +0000 Subject: [PATCH 040/135] cleanup marks and make a couple of functions private. --- .../UISIAutoReporter/UISIAutoReporter.swift | 40 ++++++++++++------- .../UISIAutoReporter/UISIDetector.swift | 2 + 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift index ca64cace5..a29dcf2b9 100644 --- a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift +++ b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift @@ -38,9 +38,10 @@ extension UISIAutoReportData: Codable { } } -@available(iOS 14.0, *) + /// Listens for failed decryption events and silently sends reports RageShake server. /// Also requests that message senders send a matching report to have both sides of the interaction. +@available(iOS 14.0, *) @objcMembers class UISIAutoReporter: NSObject, UISIDetectorDelegate { struct ReportInfo: Hashable { @@ -48,6 +49,8 @@ extension UISIAutoReportData: Codable { let sessionId: String } + // MARK: - Properties + private static let autoRsRequest = "im.vector.auto_rs_request" private static let reportSpacing = 60 @@ -66,6 +69,8 @@ extension UISIAutoReportData: Codable { } } + // MARK: - Setup + override init() { self.bugReporter = MXBugReportRestClient.vc_bugReportRestClient(appName: BuildSettings.bugReportUISIId) super.init() @@ -103,6 +108,8 @@ extension UISIAutoReportData: Codable { return Self.autoRsRequest } + // MARK: - Public + func uisiDetected(source: UISIDetectedMessage) { dispatchQueue.async { let reportInfo = ReportInfo(roomId: source.roomId, sessionId: source.sessionId) @@ -114,12 +121,27 @@ extension UISIAutoReportData: Codable { } } + func add(_ session: MXSession) { + sessions.append(session) + detector.enabled = enabled + session.eventStreamService.add(eventStreamListener: detector) + } + + func remove(_ session: MXSession) { + if let index = sessions.firstIndex(of: session) { + sessions.remove(at: index) + } + session.eventStreamService.remove(eventStreamListener: detector) + } + func uisiReciprocateRequest(source: MXEvent) { guard source.type == Self.autoRsRequest else { return } self.matchingRSRequestSubject.send(source) } - func sendRageShake(source: UISIDetectedMessage) { + // MARK: - Private + + private func sendRageShake(source: UISIDetectedMessage) { MXLog.debug("[UISIAutoReporter] sendRageShake") guard let session = sessions.first else { return } let uisiData = UISIAutoReportData( @@ -171,7 +193,7 @@ extension UISIAutoReportData: Codable { }) } - func sendMatchingRageShake(source: MXEvent) { + private func sendMatchingRageShake(source: MXEvent) { MXLog.debug("[UISIAutoReporter] sendMatchingRageShake") let eventId = source.content["event_id"] as? String let roomId = source.content["room_id"] as? String @@ -206,16 +228,4 @@ extension UISIAutoReportData: Codable { ) } - func add(_ session: MXSession) { - sessions.append(session) - detector.enabled = enabled - session.eventStreamService.add(eventStreamListener: detector) - } - - func remove(_ session: MXSession) { - if let index = sessions.firstIndex(of: session) { - sessions.remove(at: index) - } - session.eventStreamService.remove(eventStreamListener: detector) - } } diff --git a/Riot/Managers/UISIAutoReporter/UISIDetector.swift b/Riot/Managers/UISIAutoReporter/UISIDetector.swift index 47c587a62..5a701230f 100644 --- a/Riot/Managers/UISIAutoReporter/UISIDetector.swift +++ b/Riot/Managers/UISIAutoReporter/UISIDetector.swift @@ -55,6 +55,8 @@ class UISIDetector: MXLiveEventListener { private let dispatchQueue = DispatchQueue(label: "io.element.UISIDetector.queue") private static let gracePeriodSeconds = 30 + // MARK: - Public + func onSessionStateChanged(state: MXSessionState) { dispatchQueue.async { self.initialSyncCompleted = state == .running From 1e35243c716a8fac4d3fd325f6e187d9e5611152 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 21 Mar 2022 15:22:39 +0000 Subject: [PATCH 041/135] Add matchingIssue to sender report and fix description, was missing "(sender)" --- Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift index a29dcf2b9..893dbbe29 100644 --- a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift +++ b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift @@ -201,7 +201,12 @@ extension UISIAutoReportData: Codable { let deviceId = source.content["device_id"] as? String let userId = source.content["user_id"] as? String let senderKey = source.content["sender_key"] as? String - let matchingIssue = source.content["recipient_rageshake"] as? String ?? "" + let matchingIssue = source.content["recipient_rageshake"] as? String + + var description = "Auto-reporting decryption error (sender)" + if let matchingIssue = matchingIssue { + description += "\nRecipient rageshake: \(matchingIssue)" + } let uisiData = UISIAutoReportData( eventId: eventId, @@ -213,7 +218,7 @@ extension UISIAutoReportData: Codable { ).jsonString ?? "" self.bugReporter.vc_sendBugReport( - description: "Auto-reporting decryption error", + description: description, sendLogs: true, sendCrashLog: true, additionalLabels: [ @@ -223,7 +228,7 @@ extension UISIAutoReportData: Codable { ], customFields: [ "auto_uisi": uisiData, - "recipient_rageshake": matchingIssue + "recipient_rageshake": matchingIssue ?? "" ] ) } From b1eb70abdd36751529a0fa8673492f6b812eaa03 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 21 Mar 2022 17:08:54 +0000 Subject: [PATCH 042/135] Add OnboardingCelebrationScreen and EffectsSceneView. --- .../Contents.json | 15 +++ .../onboarding_celebration_icon.svg | 5 + Riot/Assets/en.lproj/Untranslated.strings | 4 + Riot/Generated/Images.swift | 1 + Riot/Generated/UntranslatedStrings.swift | 12 ++ .../Onboarding/OnboardingCoordinator.swift | 34 ++++-- .../Modules/Common/Mock/MockAppScreens.swift | 1 + .../OnboardingCelebrationCoordinator.swift | 65 ++++++++++ .../EffectsScene/ConfettiScene.scn | Bin 0 -> 11790 bytes .../EffectsScene/EffectsScene.swift | 78 ++++++++++++ .../EffectsScene/EffectsSceneView.swift | 34 ++++++ ...MockOnboardingCelebrationScreenState.swift | 46 ++++++++ .../OnboardingCelebrationModels.swift | 31 +++++ .../OnboardingCelebrationViewModel.swift | 48 ++++++++ ...boardingCelebrationViewModelProtocol.swift | 24 ++++ .../UI/OnboardingCelebrationUITests.swift | 23 ++++ .../OnboardingCelebrationViewModelTests.swift | 24 ++++ .../View/OnboardingCelebrationScreen.swift | 111 ++++++++++++++++++ 18 files changed, 549 insertions(+), 7 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Onboarding/onboarding_celebration_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Onboarding/onboarding_celebration_icon.imageset/onboarding_celebration_icon.svg create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/Coordinator/OnboardingCelebrationCoordinator.swift create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/ConfettiScene.scn create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/EffectsScene.swift create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/EffectsSceneView.swift create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationModels.swift create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModel.swift create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModelProtocol.swift create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/Test/Unit/OnboardingCelebrationViewModelTests.swift create mode 100644 RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift diff --git a/Riot/Assets/Images.xcassets/Onboarding/onboarding_celebration_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Onboarding/onboarding_celebration_icon.imageset/Contents.json new file mode 100644 index 000000000..ad5edffb0 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Onboarding/onboarding_celebration_icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "onboarding_celebration_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Onboarding/onboarding_celebration_icon.imageset/onboarding_celebration_icon.svg b/Riot/Assets/Images.xcassets/Onboarding/onboarding_celebration_icon.imageset/onboarding_celebration_icon.svg new file mode 100644 index 000000000..6282b808b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Onboarding/onboarding_celebration_icon.imageset/onboarding_celebration_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings index 51087b0b1..dd3e74084 100644 --- a/Riot/Assets/en.lproj/Untranslated.strings +++ b/Riot/Assets/en.lproj/Untranslated.strings @@ -35,4 +35,8 @@ "onboarding_avatar_message" = "You can change this anytime."; "onboarding_avatar_accessibility_label" = "Profile picture"; +"onboarding_celebration_title" = "You’re all set!"; +"onboarding_celebration_message" = "Your preferences have been saved."; +"onboarding_celebration_button" = "Let's go"; + "image_picker_action_files" = "Choose from files"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 469b4b1e7..3398cc3b5 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -126,6 +126,7 @@ internal class Asset: NSObject { internal static let onboardingSplashScreenPage4Dark = ImageAsset(name: "OnboardingSplashScreenPage4Dark") internal static let onboardingAvatarCamera = ImageAsset(name: "onboarding_avatar_camera") internal static let onboardingAvatarEdit = ImageAsset(name: "onboarding_avatar_edit") + internal static let onboardingCelebrationIcon = ImageAsset(name: "onboarding_celebration_icon") internal static let onboardingCongratulationsIcon = ImageAsset(name: "onboarding_congratulations_icon") internal static let onboardingUseCaseCommunity = ImageAsset(name: "onboarding_use_case_community") internal static let onboardingUseCaseCommunityDark = ImageAsset(name: "onboarding_use_case_community_dark") diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift index 8be6e7646..515dd7e75 100644 --- a/Riot/Generated/UntranslatedStrings.swift +++ b/Riot/Generated/UntranslatedStrings.swift @@ -26,6 +26,18 @@ public extension VectorL10n { static var onboardingAvatarTitle: String { return VectorL10n.tr("Untranslated", "onboarding_avatar_title") } + /// Let's go + static var onboardingCelebrationButton: String { + return VectorL10n.tr("Untranslated", "onboarding_celebration_button") + } + /// Your preferences have been saved. + static var onboardingCelebrationMessage: String { + return VectorL10n.tr("Untranslated", "onboarding_celebration_message") + } + /// You’re all set! + static var onboardingCelebrationTitle: String { + return VectorL10n.tr("Untranslated", "onboarding_celebration_title") + } /// Take me home static var onboardingCongratulationsHomeButton: String { return VectorL10n.tr("Untranslated", "onboarding_congratulations_home_button") diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift index b9c99ad94..56d523944 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift @@ -391,14 +391,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { private func displayNameCoordinator(_ coordinator: OnboardingDisplayNameCoordinator, didCompleteWith userSession: UserSession) { if shouldShowAvatarScreen { showAvatarScreen(for: userSession) - return - } else if Analytics.shared.shouldShowAnalyticsPrompt { - showAnalyticsPrompt(for: userSession.matrixSession) - return + } else { + showCelebrationScreen(for: userSession) } - - onboardingFinished = true - completeIfReady() } /// Show the avatar personalization screen for new users using the supplied user session. @@ -431,6 +426,31 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { /// Displays the next view in the flow after the avatar screen. @available(iOS 14.0, *) private func avatarCoordinator(_ coordinator: OnboardingAvatarCoordinator, didCompleteWith userSession: UserSession) { + showCelebrationScreen(for: userSession) + } + + @available(iOS 14.0, *) + private func showCelebrationScreen(for userSession: UserSession) { + MXLog.debug("[OnboardingCoordinator] showCelebrationScreen") + + let parameters = OnboardingCelebrationCoordinatorParameters(userSession: userSession) + let coordinator = OnboardingCelebrationCoordinator(parameters: parameters) + + coordinator.completion = { [weak self, weak coordinator] userSession in + guard let self = self, let coordinator = coordinator else { return } + self.celebrationCoordinator(coordinator, didCompleteWith: userSession) + } + + add(childCoordinator: coordinator) + coordinator.start() + + navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in + self?.remove(childCoordinator: coordinator) + } + } + + @available(iOS 14.0, *) + private func celebrationCoordinator(_ coordinator: OnboardingCelebrationCoordinator, didCompleteWith userSession: UserSession) { if Analytics.shared.shouldShowAnalyticsPrompt { showAnalyticsPrompt(for: userSession.matrixSession) return diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 7201176c3..cae73c224 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -20,6 +20,7 @@ import Foundation @available(iOS 14.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ + MockOnboardingCelebrationScreenState.self, MockOnboardingAvatarScreenState.self, MockOnboardingDisplayNameScreenState.self, MockOnboardingCongratulationsScreenState.self, diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/Coordinator/OnboardingCelebrationCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/Coordinator/OnboardingCelebrationCoordinator.swift new file mode 100644 index 000000000..98d4fb9f7 --- /dev/null +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/Coordinator/OnboardingCelebrationCoordinator.swift @@ -0,0 +1,65 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct OnboardingCelebrationCoordinatorParameters { + let userSession: UserSession +} + +@available(iOS 14.0, *) +final class OnboardingCelebrationCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: OnboardingCelebrationCoordinatorParameters + private let onboardingCelebrationHostingController: VectorHostingController + private var onboardingCelebrationViewModel: OnboardingCelebrationViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((UserSession) -> Void)? + + // MARK: - Setup + + init(parameters: OnboardingCelebrationCoordinatorParameters) { + self.parameters = parameters + + let viewModel = OnboardingCelebrationViewModel() + let view = OnboardingCelebrationScreen(viewModel: viewModel.context) + onboardingCelebrationViewModel = viewModel + onboardingCelebrationHostingController = VectorHostingController(rootView: view) + onboardingCelebrationHostingController.enableNavigationBarScrollEdgeAppearance = true + } + + // MARK: - Public + func start() { + MXLog.debug("[OnboardingCelebrationCoordinator] did start.") + onboardingCelebrationViewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[OnboardingCelebrationCoordinator] OnboardingCelebrationViewModel did complete with result: \(result).") + self.completion?(self.parameters.userSession) + } + } + + func toPresentable() -> UIViewController { + return self.onboardingCelebrationHostingController + } +} diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/ConfettiScene.scn b/RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/ConfettiScene.scn new file mode 100644 index 0000000000000000000000000000000000000000..47b13d49934b94a2e2856078737f64f30544008d GIT binary patch literal 11790 zcmcIq2Y8d!*T45o(j-kv(?Ow4lcrg18QpZSx(kKwbkjkaylq3X$Y?1tUJ+52OxYk5 zL>YpLOi`hrAXK&>Dq2JY6ct4rpzxhHY0{$n{eRE*JYV`Wx%a+zocEk_&pqdNZoOHr zv)ZDgFCdHv#6V2MLLP{XJS*faq{XT;87t*#i>65jwGz3_WUi2#>L-vIo7ETLS?lWr z0#!!-Sh9`Orgir^i#Ui|VlkO)6y7NqiFk;Q;?YQyg~lNbDnlgFAtRcKo9EIcX5S)aQ@o=1jSK~GKCHy*m3m?FT@L_xne~qu>Z}1I#6W_w$ z;_vWn{5}2w|A_D4pHL)HpmKZ{|BC;_e-WruZnW$5UkdsnFVqKlBLVV3zQ_;xqrNBr z^+WwpAPPc46pTbjj6#qEg(4}EAvsK=L}4f#eg~j|Xb>6SSV+>SGOIOYG1;I8ZUG-v&_PRf~Xi_T&)8i2G)tI2NBkfp`BmGCwe8$Ek149PNT zU5ipoN-j-92yvvDlM$i_S;`%#(G656H6qtprc&r%nX`hB01Hy7WLau{_fr`)^dcGp z0!=`PCn1YH*Xz`CTS!iB6L{Mu6 zrPgWmq^iwoBMqDe&-U`%(uA<1=dY#4Al&P~g4Qic0sxzn?NfkNK zPJ*$_F~WvW>$7QFRHnA*973uS^iwx9zyMO4-qyWavvs6i3mZ7lCBSTpQ<-{=5|9R+ zl|KG5?H0#GC04K>HCCI2v}u~M)f$`0QYY~5r4GI5>iZa7#imw=xdc*N3uZK_Esed2 z^tYNCY=zzWWX&?t8V&~E$2B0wXmi^AaaI#3ov|?wHY^+n4vx@N0YHP33pEY8Nu=ZGpzUm})odZvT6mgfY}5mZ z0@sYWx&{)CL}Mkqno;Yd-%UuSj~d zy@|GPTRLB^PSDpyfl4>I=Ur=DtqwY?T{XIo`;mhQ)oP7~)RRsvsB|+N z)aNli_tri)2~Ih~8SR?hEk`$NdvVC6hBb~w=$COCct5knWF{@PwhWWeW&s|7V+QtI zuWM|gRl%(-eytWAI3OjaOww#?O1FdFgO2vqX~9}{YjLk--Mj||*!60Qi;v?RC#6fn zeR_7!DCeo!Yu~trxM!_xRa*>pvum|FLBL}?a_b^E7cP$cZFTolz_iE4rWv71^TVk* z-o197vnBBFJJvmwTexuS(XLOu9^9frD7!h=zuV7pD`3wfw~=t^uX|b$L4$>~8cBVR zGv?McdMS6$QoYG!wpRBR1hA$T2lPF9StscK*n0y>b_47T@V5j6N0#!7^RRvFTzS&b zBTMyn_7pHVly`S`rzZuoeACb{9v;h5mQKE!>L~1ANq<&RUXOK^rCJWbV>r|UI2@`! zSC%^Xl4mN^WvLAm{LoXt$n}oG`M9`LC~!C&S!%c6AWN05+I|Qc&%sf14SkKSgF6&i z-v(+*^(pY_qpWwuADCb;Ssv{Fb=={duX;S};JAX0%{+cTkyzn${y+BW<1O&<_4Dr= z01iTi$>4OGt)=;@;!)|2CU9UKt~!`g=wY|oOa{lscQkhU%>{yp=*Z|H(aA~CamleU z36ki@#Q2z)#Dw_7Xu2{a-t|lpQy>V7j*N+pPl}I8h>wk-yGFynxaj!AAyA1=jENnR zkPsg?1Uy-apjhYo2l@mFgG=&NIhjCKPDX}+4ZbL|z=uB?A^zxGozX-9KMk zO|w~R(9-3jP=3C}PC}VM>)@;w9oY)y3s9EB9?(JgcPRTCNVOFqCZBG%k(wqbM?;x! zDJjl?@^H{Veq*n4eXp{OoCI_tlwmTrS!nwy3D-nOVv>`SB%{ezJ!!KIE(GJCwrC}^ zuBeS|2sz)G9))~q-b#SU2{FkD34>!Jf&K2!zdm&L`u74YTW62EMGl$afX<$_p0Pb` zrgnsq?g4KW_O#WnL}>4`2=)K8r%k>dA@7+8?K$1+eFA8GX|mbOL!+WvTU#S_q$ZMP zsQdF@JGguP=R&XbBI)(qypv>-4QjjICZUIpHVtn6;n0`FO zs9pz8uv>1q+%iP(yz-D6PQ8aRJa>FQ_C+j!Dq+<%O6cZ{;u49*ZfT**uqWs*7V-oL z#2+9JF~rj0;B3c2ygUpd+$;!!3Lx~XglJ6zp_BnxApDsE2+b@AG8RHmu?m8SO%OEf zfFNNn1OCc234hwy|fiSF*RW-(#O+UuXZ$;c-GZv7Bs9 z701Y#!CA)H%6XS_j&pjUHO7*JpvU$DWwZ-d**B4&D_wnl!*(axuw$JoFtNZNfbFR-1-dt~mcba#t_hj#7 z-n+a{d*2pt1xi7NKrNUiSS{EuxFopeHzHDEmZRzYIT(-!p!j{NDGw?(g9r=AYv~!GEs*YyPMF@AUQU8{fCA@1(w~`@Yk+ zD}WK849E%42P_D9GvH#t{eGf;Y5mB4v-|Dncdp-W{e}Hg`;-0W^xxV4 zky*4-bVPJp93V~=PY^E^zbn2K;ukV9L>IC&`1tkC{G-pxF+#Zl7CWR(!8YO$*kn`QxVRhzmw^+sAm+Jv<2X+NY# zr(4o@r~j5QG-GPUp-e{RsLVN;r?PyrO0!mFeUUBAZp_}6eP`5=QBRFJG@3Pf?C3?K zFXf1GG&x&x?uBv8%>@l{+BUl6xSJ$QzTlB=7TlMZPhAZviUEDOgf) zr7)~;V&U6G9z}&ktBbxVjwzl}d`#u1s#9%M{aTV`nSH-R>tJ++3Z(P>67sp+%9#TE4`eIF3&7_(SYlCX_weQq< z*Qx7vjc1K7AHQ|{pXvg2yZT;zPW_ttJDLp5a?N+zRP9pjEpi08n7q+2qG55v&BhUp zOB!!Ar8F&Tx~#j*YlZ=x(+Xl4F zZo4r#d-BUqF`v>r)iEV>%F|P>PEDJ-VH%!RKdocBZ2HXU*Qrs|t517A-Td@NGooiK zp7F~w<5#sbuM#rN6vb z_u_}k;+MU&+;h2odDn{E75i4oS1w%n=c=YvAFobdy>m_Qn%Qf9TdQ4rZe8lS9WRMq zn)}lI^}6++Y#6m+PrI^x*+!3zEgQetRJQ5Z%SkV9*&MWa?&iNf zwPv^9?m2t#p0+)A_BQS9+E=;n?EW$PI}Qvxu>0-Ew_iCJdT{kSecxH|uIIZm-+lDn z1;0Bfi|(mC*Ih)y%7%U#Y(O z?3(7`b~-(LUD{N01wv%dHFe$5ZzKfLv0 z>W`gwD(+nW$@0^qpXc2Ty!*;8@xOH3E4=r`uZCYA{5I$Jz~5i}Bk7N0_sj3!c+mPM z`_C1BMf`Q(;n;_tKQcUebS^{^Dt!`+{a6IlE|Wi2cSF}OTp0!^t|KmLSIQy9;{%x! zIf`&N5jlWk7Xf-*1z>M8;HJ~SZCDRD<9YNKq<1hRcs#K;?u+|@V<7Zu^wN8^dQo2U zy%u_{_1f&U&1)C!pMdw`a%;k!b=SAo9w!2P1Kq6Y!GF~@^+t39-9njVfc!%33b-M+ z(f8;F2sM8MhvX;pGrEg@LHEEl`3+F@BJ>Bkj~)PC`6rZV^ztwCuo6H8fY;S}00#Oy zGAU_Vwb=&gE|tArt7~z*?pEl!}RoW`E$yh5e5 zNoUjw2JpQtfS)>Y7ydL@Xw9~m481U`aaxldKtdIy z%K!rngja3I(bEy2(E$JzXf%2|jS|9?E)0x@fb{tQ`~sVMuS&O9Xdnql7sf$mqmZ-| z0$!=FXwW$u0hI;(u-TessHc(ravhB@H#9(!4#cBwG3m6{0!WGFc7tsKZz-*oTrP+5 zrTpLr05-@2xgI;{NZ0F(Mu*B`F%B6AkMp{ftQ|{GX72`PFDaJQx)nRkVo|qk#8NCr znXu&?RcALP4y&MZPI|SG+=!Jp+&R}M(qtfQmbUJc3myQD_n`IbseaB~(}W}2>HLUu zj%XYMY@=s+EP2t6W9i)0KOZ4(azH{i@7bR)` zEvo`K6-o?Ql`jQ}cnls3!AKs?#|5|$7eQE}!X>yAm*H|;0U=5i9*3)O4a&u}xDEmq zHLiyZp+P3B#UyTk0Hz7+@C4k9^{`zG5YCve8BfF(Y{fPRZCY?Eo`l=*Wc(C_IaBd8 zJRP=C2MWg&ej3k!z-K1?qIed54mOk@ejd+;+FaOJes~_FuKnN;voK27yH599jjbyH=wa{X!?@k8RG(c+3>dM(xYfTMq7`slO=#?ez-b3yU znrUh^I`hS#T9CJBqE|3C=`<=j@e24UY-u{(P2;CD-F1Sf|3=wp*oVDHs}qF$%lhd$ zHJDK4zx3#p%?5J@@2S?O>+P0a%~e3M&gdYx)=|u{I!-}Ic}&h$1HK;qPmy94}e(KQ;KheV;Nm9$h@A!!8YCge0+#u{=AAm3i{^K;m6XER(;AdQXm+2+?X zle=$)hE6{~Y8jyUe^Stc`G@`szg@?g;IUWN8Ev($84DU3tRyT!Cq3wUCSd2?Ssy23 za^S%3&Wr@9?SR%o1>~l0OVdsO^wwJCP~j>o&CBk75&uGxTF^_GK^?mZrA10##>o1|J}ha=HnqS@8BFvG(JJO<>o_$W zFObF@1!n*8YaxH%PVONfOoxiNIiYeS-RKSFyg%Y1pqpVmD(CGON3ZXa#DITcO}@!u zQ0v{ik){N$TqRA3i;I({rXnOgY=*BCD^!QeE_nh}5D373C4D?X`^e7DkEHa)4QcAG zxHyjAqwbG1?GRkyKvalyB%~XpDf2ozDT#DsZs$h=4{5sZ)H=v9q!(=Ka?FubbX%H6 zuaZj9Q^J7mW5Idkti|gp;mFBwoD}VNEy}!t*8`~3jyK{>_+`8qzXDeJRREc`;%%q{ z`@-SE1?$-XPD~Mg4Qge0Cn|@7o;E}cphdKFvUPgUuxijhN7Bvd@`2qdfxA1fKR`G8 z>A(gU3??Ip9{dOrejHtC0jnGTK2xPZXLin6Yfzi%#3`f&jgU4fZG~A?B{Zg71A>DL zm(>LMJ#$+nsGKw1OJPdNl}?`y9z24}DEJNhCM*K)?D8mFOSj`)^a1{)NB_=~-EN*l zx_Po6l?%LjmUcN7Z{v4R{RVuHQf$EQQcC*bh7VlafOjK%E}ytMci?lVey6}2AHhfQ z`}hNV3?B!S?*u-HJMl;O6h4j5;ImX16;4G^1E_)2AZjobNkvi7R16hM#ZmFp5PJA| ze8KgH)cAtyAAd==QVCR|`;QGJ*ME|u6$tco`S+?eqo&ZL*U_G+KiE~fUQ0K*(~7kJ z-_z_=DYwJjOp<16j*%YV9!=*5z(QN|z)I8Cf%^809<75NO<#wj+kKtwfG@*7x0RSG zOU-|8EQOm3&{OQ}SqQU|mKL%AP7!c<^(7`VxY>=5(_KQl)(x;fVO+1*>GlO33wIMV z3T`}s9VDHb*;)%k1NLo!%TUf$1upu`Z8`LH3Yf6FiGE#&V{7Gfr|8B5$7PS*Y8PTtvgl?=#Cu{6PFd{K+5ylJ{bGGlCfkMl>UtF_uxtsAn`Y)-!f8_Aw4KE&v>VlX0Jk znS7>@8O#(jW0)hEW0`r(e5MM3`7&k&lVlp0lbBC2r!pz#4CYMcM&>5wYs@3e6Uz@k|5SqoSTS&LaqSTC~Hu{N+avR?Ms?(w!q zm&bi}06T&m$sW#Dv76b?ve&R*XCGvrW#8p^ag>~RPCBQMQ^&DzrT{R%k+YX`0-*V; zoIkj1u0L1Gjp8PAO97sLn!A+S&fUj7!R_My?&<3(_Kfw+_pI@3@m%S-$Md-7*PcJ| zn7k0)FkT*yHSap_2JaW%J>Ek;n=b(1K7=0* z;Qc`UAbuo2il4+!JCG^eElc85aZ-zdS_LUBl#!HK&cIord<$&Tly}2C!xUjLVWY#whuOpCK%$`|>~h#o;V9fMTp1n~9vvPVA&-cO pNRG&g7!{EdF*f3zhz}w EffectsScene? { + guard let scene = EffectsScene(named: Constants.confettiSceneName) else { return nil } + + let colors: [[Float]] = theme.colors.namesAndAvatars.compactMap { $0.floatComponents } + + if let particles = scene.rootNode.childNode(withName: Constants.particlesNodeName, recursively: false)?.particleSystems?.first { + // The particles need a non-zero color variation for the handler to affect the color + particles.particleColorVariation = SCNVector4(x: 0, y: 0, z: 0, w: 0.1) + + // Add a handler to customize the color of the particles. + particles.handle(.birth, forProperties: [.color]) { data, dataStride, indices, count in + for index in 0.. SCNView { + SCNView(frame: .zero) + } + + func updateUIView(_ sceneView: SCNView, context: Context) { + sceneView.scene = scene + sceneView.backgroundColor = .clear + } +} diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift new file mode 100644 index 000000000..b2c5e2321 --- /dev/null +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift @@ -0,0 +1,46 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +@available(iOS 14.0, *) +enum MockOnboardingCelebrationScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case confetti + + /// The associated screen + var screenType: Any.Type { + OnboardingCelebrationScreen.self + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let viewModel = OnboardingCelebrationViewModel() + + // can simulate service and viewModel actions here if needs be. + + return ( + [self, viewModel], + AnyView(OnboardingCelebrationScreen(viewModel: viewModel.context) + .addDependency(MockAvatarService.example)) + ) + } +} diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationModels.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationModels.swift new file mode 100644 index 000000000..986425b1c --- /dev/null +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationModels.swift @@ -0,0 +1,31 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: View model + +enum OnboardingCelebrationViewModelResult { + case complete +} + +// MARK: View + +struct OnboardingCelebrationViewState: BindableState { } + +enum OnboardingCelebrationViewAction { + case complete +} diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModel.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModel.swift new file mode 100644 index 000000000..e0cedbfe4 --- /dev/null +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModel.swift @@ -0,0 +1,48 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14, *) +typealias OnboardingCelebrationViewModelType = StateStoreViewModel +@available(iOS 14, *) +class OnboardingCelebrationViewModel: OnboardingCelebrationViewModelType, OnboardingCelebrationViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((OnboardingCelebrationViewModelResult) -> Void)? + + // MARK: - Setup + + init() { + super.init(initialViewState: OnboardingCelebrationViewState()) + } + + // MARK: - Public + + override func process(viewAction: OnboardingCelebrationViewAction) { + switch viewAction { + case .complete: + completion?(.complete) + } + } +} diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModelProtocol.swift new file mode 100644 index 000000000..5417dfc98 --- /dev/null +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModelProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol OnboardingCelebrationViewModelProtocol { + + var completion: ((OnboardingCelebrationViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + var context: OnboardingCelebrationViewModelType.Context { get } +} diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift new file mode 100644 index 000000000..994bfa56b --- /dev/null +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift @@ -0,0 +1,23 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class OnboardingCelebrationUITests: MockScreenTest { + // Nothing to test as the view is completely static +} diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/Test/Unit/OnboardingCelebrationViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/Unit/OnboardingCelebrationViewModelTests.swift new file mode 100644 index 000000000..ababafee4 --- /dev/null +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/Unit/OnboardingCelebrationViewModelTests.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class OnboardingCelebrationViewModelTests: XCTestCase { + // Nothing to test as there is no mutable state +} diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift new file mode 100644 index 000000000..2da7cee66 --- /dev/null +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift @@ -0,0 +1,111 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SceneKit + +@available(iOS 14.0, *) +struct OnboardingCelebrationScreen: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + private var horizontalPadding: CGFloat { + horizontalSizeClass == .regular ? 50 : 16 + } + + // MARK: Public + + @ObservedObject var viewModel: OnboardingCelebrationViewModel.Context + + // MARK: Views + + var body: some View { + GeometryReader { geometry in + VStack { + ScrollView(showsIndicators: false) { + mainContent + .padding(.top, 106) + .padding(.horizontal, horizontalPadding) + .frame(maxWidth: OnboardingConstants.maxContentWidth) + } + .frame(maxWidth: .infinity) + + buttons + .frame(maxWidth: OnboardingConstants.maxContentWidth) + .padding(.horizontal, horizontalPadding) + .padding(.bottom, 24) + .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) + } + .frame(maxWidth: .infinity) + } + .overlay(effects.ignoresSafeArea()) + .background(theme.colors.background.ignoresSafeArea()) + .accentColor(theme.colors.accent) + } + + /// The main content of the view to be shown in a scroll view. + var mainContent: some View { + VStack(spacing: 8) { + Image(Asset.Images.onboardingCelebrationIcon.name) + .resizable() + .scaledToFit() + .frame(width: 90) + .foregroundColor(theme.colors.accent) + .background(Circle().foregroundColor(.white).padding(2)) + .padding(.bottom, 42) + + Text(VectorL10n.onboardingCelebrationTitle) + .font(theme.fonts.title2B) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.primaryContent) + + Text(VectorL10n.onboardingCelebrationMessage) + .font(theme.fonts.subheadline) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.secondaryContent) + } + } + + /// The action buttons shown at the bottom of the view. + var buttons: some View { + VStack { + Button { viewModel.send(viewAction: .complete) } label: { + Text(VectorL10n.onboardingCelebrationButton) + .font(theme.fonts.body) + } + .buttonStyle(PrimaryActionButtonStyle()) + } + } + + var effects: some View { + EffectsSceneView(scene: EffectsScene.confetti(with: theme)) + } +} + +// MARK: - Previews + +@available(iOS 15.0, *) +struct OnboardingCelebration_Previews: PreviewProvider { + static let stateRenderer = MockOnboardingCelebrationScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} From 591dbb58293edf2bd36e41656b39e4b5c38d537b Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 21 Mar 2022 17:33:24 +0000 Subject: [PATCH 043/135] Add effects to the congratulations screen when personalisation is disabled. Improve the API of the EffectsView to make it more swifty. --- .../EffectsScene/ConfettiScene.scn | Bin .../EffectsScene/EffectsScene.swift | 0 .../EffectsScene/EffectsView.swift} | 34 ++++++++++++++++-- .../View/OnboardingCelebrationScreen.swift | 2 +- .../OnboardingCongratulationsScreen.swift | 8 +++++ 5 files changed, 40 insertions(+), 4 deletions(-) rename RiotSwiftUI/Modules/{Onboarding/Celebration => Common}/EffectsScene/ConfettiScene.scn (100%) rename RiotSwiftUI/Modules/{Onboarding/Celebration => Common}/EffectsScene/EffectsScene.swift (100%) rename RiotSwiftUI/Modules/{Onboarding/Celebration/EffectsScene/EffectsSceneView.swift => Common/EffectsScene/EffectsView.swift} (57%) diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/ConfettiScene.scn b/RiotSwiftUI/Modules/Common/EffectsScene/ConfettiScene.scn similarity index 100% rename from RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/ConfettiScene.scn rename to RiotSwiftUI/Modules/Common/EffectsScene/ConfettiScene.scn diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/EffectsScene.swift b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift similarity index 100% rename from RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/EffectsScene.swift rename to RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/EffectsSceneView.swift b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsView.swift similarity index 57% rename from RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/EffectsSceneView.swift rename to RiotSwiftUI/Modules/Common/EffectsScene/EffectsView.swift index efa08d6fb..aa868f90c 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/EffectsScene/EffectsSceneView.swift +++ b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsView.swift @@ -20,15 +20,43 @@ import SceneKit @available(iOS 14.0, *) /// A SwiftUI wrapper around `SCNView`, that unlike `SceneView` allows the /// scene to have a transparent background and be rendered on top of other views. -struct EffectsSceneView: UIViewRepresentable { - let scene: SCNScene? +struct EffectsView: UIViewRepresentable { + + // MARK: - Private + + @Environment(\.theme) private var theme + + // MARK: - Public + + enum EffectsType { + /// A confetti drop effect from the top centre of the screen. + case confetti + /// No effect will be shown. + case none + } + + /// The type of effects to be shown in the view. + var effectsType: EffectsType = .none + + // MARK: - Lifecycle func makeUIView(context: Context) -> SCNView { SCNView(frame: .zero) } func updateUIView(_ sceneView: SCNView, context: Context) { - sceneView.scene = scene + sceneView.scene = makeScene() sceneView.backgroundColor = .clear } + + // MARK: - Private + + private func makeScene() -> EffectsScene? { + switch effectsType { + case .confetti: + return EffectsScene.confetti(with: theme) + case .none: + return nil + } + } } diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift index 2da7cee66..3c51957cd 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift @@ -96,7 +96,7 @@ struct OnboardingCelebrationScreen: View { } var effects: some View { - EffectsSceneView(scene: EffectsScene.confetti(with: theme)) + EffectsView(effectsType: .confetti) } } diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift index 244eba3a1..420abc906 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift @@ -54,6 +54,7 @@ struct OnboardingCongratulationsScreen: View { maxHeight: OnboardingConstants.maxContentHeight) .frame(maxWidth: .infinity, maxHeight: .infinity) } + .overlay(effects.ignoresSafeArea()) .background(theme.colors.accent.ignoresSafeArea()) .accentColor(.white) .navigationBarHidden(true) @@ -120,6 +121,13 @@ struct OnboardingCongratulationsScreen: View { .buttonStyle(PrimaryActionButtonStyle(customColor: .white)) .accessibilityIdentifier("homeButton") } + + @ViewBuilder + var effects: some View { + if viewModel.viewState.personalizationDisabled { + EffectsView(effectsType: .confetti) + } + } } // MARK: - Previews From ad3b4e7cc9e9bbadef1e54873609517cdcf398fc Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 22 Mar 2022 10:59:31 +0000 Subject: [PATCH 044/135] Ensure the login button is always visible --- .../AuthenticationViewController.h | 2 - .../AuthenticationViewController.m | 11 -- .../AuthenticationViewController.xib | 150 +++++++++--------- changelog.d/5875.bugfix | 1 + 4 files changed, 78 insertions(+), 86 deletions(-) create mode 100644 changelog.d/5875.bugfix diff --git a/Riot/Modules/Authentication/AuthenticationViewController.h b/Riot/Modules/Authentication/AuthenticationViewController.h index dd76303a4..f023490c0 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.h +++ b/Riot/Modules/Authentication/AuthenticationViewController.h @@ -31,8 +31,6 @@ @property (weak, nonatomic) IBOutlet UIButton *skipButton; @property (weak, nonatomic) IBOutlet UIButton *forgotPasswordButton; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *submitButtonMinLeadingConstraint; - @property (weak, nonatomic) IBOutlet UIView *serverOptionsContainer; @property (weak, nonatomic) IBOutlet UIButton *customServersTickButton; @property (weak, nonatomic) IBOutlet UIView *customServersContainer; diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index c8507dc5e..c8c95e2dd 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -1058,17 +1058,6 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; } self.forgotPasswordButton.hidden = !showForgotPasswordButton; - - // Adjust minimum leading constraint of the submit button - if (self.forgotPasswordButton.isHidden) - { - self.submitButtonMinLeadingConstraint.constant = 19; - } - else - { - CGRect frame = self.forgotPasswordButton.frame; - self.submitButtonMinLeadingConstraint.constant = frame.origin.x + frame.size.width + 10; - } } - (void)afterSetPinFlowCompletedWithCredentials:(MXCredentials*)credentials diff --git a/Riot/Modules/Authentication/AuthenticationViewController.xib b/Riot/Modules/Authentication/AuthenticationViewController.xib index c779ced89..c90f28e22 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.xib +++ b/Riot/Modules/Authentication/AuthenticationViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -44,7 +44,6 @@ - @@ -91,7 +90,7 @@
+ + + + + + + + + + + + + + +
diff --git a/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift b/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift new file mode 100644 index 000000000..3a177fb63 --- /dev/null +++ b/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift @@ -0,0 +1,178 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +class ThreadsBetaViewController: UIViewController { + + // MARK: Constants + + private enum Constants { + static let learnMoreLink = "https://element.io" + } + + // MARK: Outlets + + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var separatorLineView: UIView! + @IBOutlet private weak var informationTextView: UITextView! { + didSet { + informationTextView.contentInset = .zero + informationTextView.textContainerInset = .zero + informationTextView.textContainer.lineFragmentPadding = 0 + informationTextView.scrollsToTop = false + informationTextView.showsVerticalScrollIndicator = false + informationTextView.showsHorizontalScrollIndicator = false + informationTextView.isEditable = false + informationTextView.isScrollEnabled = false + } + } + @IBOutlet private weak var enableButton: UIButton! + @IBOutlet private weak var cancelButton: UIButton! + + // MARK: Private + + private var theme: Theme! + + // MARK: Public + + @objc var didTapEnableButton: (() -> Void)? + @objc var didTapCancelButton: (() -> Void)? + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + self.setupViews() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Hide back button + self.navigationItem.setHidesBackButton(true, animated: animated) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return self.theme.statusBarStyle + } + + // MARK: - Setup + + @objc class func instantiate() -> ThreadsBetaViewController { + let viewController = StoryboardScene.ThreadsBetaViewController.initialScene.instantiate() + viewController.theme = ThemeService.shared().theme + return viewController + } + + // MARK: - Private + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, + selector: #selector(themeDidChange), + name: .themeServiceDidChangeTheme, + object: nil) + } + + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } + + private func setupViews() { + self.vc_removeBackTitle() + + self.enableButton.setTitle(VectorL10n.threadsBetaEnable, for: .normal) + self.cancelButton.setTitle(VectorL10n.threadsBetaCancel, for: .normal) + + self.titleLabel.text = VectorL10n.threadsBetaTitle + guard let font = self.informationTextView.font else { + return + } + let attributedString = NSMutableAttributedString(string: VectorL10n.threadsBetaInformation, + attributes: [.font: font]) + let link = NSAttributedString(string: VectorL10n.threadsBetaInformationLink, + attributes: [.link: Constants.learnMoreLink, + .font: font]) + attributedString.append(link) + self.informationTextView.attributedText = attributedString + } + + // MARK: - Actions + + @IBAction private func enableButtonAction(_ sender: UIButton) { + self.didTapEnableButton?() + } + + @IBAction private func cancelButtonAction(_ sender: UIButton) { + self.didTapCancelButton?() + } + +} + +// MARK: - Themable + +extension ThreadsBetaViewController: Themable { + + func update(theme: Theme) { + self.theme = theme + + self.view.backgroundColor = theme.colors.background + + if let navigationBar = self.navigationController?.navigationBar { + theme.applyStyle(onNavigationBar: navigationBar) + } + + self.titleLabel.textColor = theme.textPrimaryColor + self.separatorLineView.backgroundColor = theme.colors.system + self.informationTextView.textColor = theme.textPrimaryColor + + self.enableButton.vc_setBackgroundColor(theme.tintColor, for: .normal) + self.enableButton.setTitleColor(theme.baseTextPrimaryColor, for: .normal) + self.cancelButton.vc_setBackgroundColor(.clear, for: .normal) + self.cancelButton.setTitleColor(theme.tintColor, for: .normal) + } + +} + +// MARK: - SlidingModalPresentable + +extension ThreadsBetaViewController: SlidingModalPresentable { + + func allowsDismissOnBackgroundTap() -> Bool { + return false + } + + func layoutHeightFittingWidth(_ width: CGFloat) -> CGFloat { + guard let view = ThreadsNoticeViewController.instantiate().view else { + return 0 + } + + view.widthAnchor.constraint(equalToConstant: width).isActive = true + view.setNeedsLayout() + view.layoutIfNeeded() + + let fittingSize = CGSize(width: width, height: UIView.layoutFittingCompressedSize.height) + + return view.systemLayoutSizeFitting(fittingSize).height + + UIWindow().safeAreaInsets.top + + UIWindow().safeAreaInsets.bottom + } + +} From 221f13f80cf87db037ad8ed3024a3b8a604de94e Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 4 Apr 2022 16:59:12 +0300 Subject: [PATCH 115/135] Display opt-in screen if threads disabled --- Riot/Modules/Room/RoomViewController.m | 50 ++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 136169871..eb6dba222 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6400,7 +6400,7 @@ static CGSize kThreadListBarButtonItemImageSize; BOOL showMoreOption = (event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForStates) || (!event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForMessages); - BOOL showThreadOption = RiotSettings.shared.enableThreads && !self.roomDataSource.threadId && !event.threadId; + BOOL showThreadOption = !self.roomDataSource.threadId && !event.threadId; NSMutableArray *items = [NSMutableArray arrayWithCapacity:5]; @@ -6762,7 +6762,14 @@ static CGSize kThreadListBarButtonItemImageSize; [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; - [self openThreadWithId:event.eventId]; + if (RiotSettings.shared.enableThreads) + { + [self openThreadWithId:event.eventId]; + } + else + { + [self showThreadsBeta]; + } }; return item; @@ -6813,6 +6820,45 @@ static CGSize kThreadListBarButtonItemImageSize; completion:nil]; } +- (void)showThreadsBeta +{ + if (!self.threadsNoticeModalPresenter) + { + self.threadsNoticeModalPresenter = [SlidingModalPresenter new]; + } + + [self.threadsNoticeModalPresenter dismissWithAnimated:NO completion:nil]; + + ThreadsBetaViewController *threadsBetaVC = [ThreadsBetaViewController instantiate]; + + MXWeakify(self); + + threadsBetaVC.didTapEnableButton = ^{ + MXStrongifyAndReturnIfNil(self); + + [self.threadsNoticeModalPresenter dismissWithAnimated:YES completion:^{ + RiotSettings.shared.enableThreads = YES; + MXSDKOptions.sharedInstance.enableThreads = YES; + [self cancelEventSelection]; + [self.roomDataSource reload]; + }]; + }; + + threadsBetaVC.didTapCancelButton = ^{ + MXStrongifyAndReturnIfNil(self); + + [self.threadsNoticeModalPresenter dismissWithAnimated:YES completion:^{ + [self cancelEventSelection]; + }]; + }; + + [self.threadsNoticeModalPresenter present:threadsBetaVC + from:self.presentedViewController?:self + animated:YES + options:SlidingModalPresenter.SpanningOption + completion:nil]; +} + - (void)openThreadWithId:(NSString *)threadId { if (self.threadsBridgePresenter) From 9b772d074a992a9a359e56dede7ec61d0486fe39 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 4 Apr 2022 16:59:51 +0300 Subject: [PATCH 116/135] Add changelog --- changelog.d/5772.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5772.change diff --git a/changelog.d/5772.change b/changelog.d/5772.change new file mode 100644 index 000000000..46210ef01 --- /dev/null +++ b/changelog.d/5772.change @@ -0,0 +1 @@ +RoomViewController: Enable thread menu option and display opt-in screen if threads disabled. From 974482f627d97c108b247626af2426d62d4011ad Mon Sep 17 00:00:00 2001 From: MaximeE Date: Mon, 4 Apr 2022 16:33:55 +0200 Subject: [PATCH 117/135] 5858: Factorise some code according to PR comments --- .../LocationSharingCoordinator.swift | 8 ++--- ...otation.swift => LocationAnnotation.swift} | 29 ++++++++++++------- .../LocationSharingModels.swift | 7 ++--- .../LocationSharingViewModel.swift | 16 ++++++---- .../Unit/LocationSharingViewModelTests.swift | 5 ++-- .../View/LocationSharingMapView.swift | 10 ++++--- .../View/UserLocationAnnotatonView.swift | 19 ++++++------ 7 files changed, 52 insertions(+), 42 deletions(-) rename RiotSwiftUI/Modules/Room/LocationSharing/{UserLocationAnnotation.swift => LocationAnnotation.swift} (75%) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift index 885a4bdf7..aa2512fa4 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -33,14 +33,12 @@ extension MXEventAssetType { func locationSharingCoordinateType() -> LocationSharingCoordinateType { let coordinateType: LocationSharingCoordinateType switch self { - case .user: + case .user, .generic: coordinateType = .user case .pin: coordinateType = .pin - case .generic: - coordinateType = .generic @unknown default: - coordinateType = .generic + coordinateType = .user } return coordinateType } @@ -54,8 +52,6 @@ extension LocationSharingCoordinateType { eventAssetType = .user case .pin: eventAssetType = .pin - case .generic: - eventAssetType = .generic } return eventAssetType } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/UserLocationAnnotation.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift similarity index 75% rename from RiotSwiftUI/Modules/Room/LocationSharing/UserLocationAnnotation.swift rename to RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift index 898c19ced..ad276883d 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/UserLocationAnnotation.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift @@ -17,26 +17,33 @@ import Foundation import Mapbox -class UserLocationAnnotation: NSObject, MGLAnnotation { +class LocationAnnotation: NSObject, MGLAnnotation { + + // MARK: - Properties + + let coordinate: CLLocationCoordinate2D + + // MARK: - Setup + + init(coordinate: CLLocationCoordinate2D) { + + self.coordinate = coordinate + } +} + +class UserLocationAnnotation: LocationAnnotation { // MARK: - Properties let avatarData: AvatarInputProtocol - let coordinate: CLLocationCoordinate2D - - let coordinateType: LocationSharingCoordinateType - // MARK: - Setup init(avatarData: AvatarInputProtocol, - coordinate: CLLocationCoordinate2D, - coordinateType: LocationSharingCoordinateType) { - - self.coordinate = coordinate + coordinate: CLLocationCoordinate2D) { + self.avatarData = avatarData - self.coordinateType = coordinateType - super.init() + super.init(coordinate: coordinate) } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index b4679701b..346b458a1 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -23,7 +23,6 @@ import CoreLocation enum LocationSharingCoordinateType { case user case pin - case generic } enum LocationSharingViewAction { @@ -55,13 +54,13 @@ struct LocationSharingViewState: BindableState { let userAvatarData: AvatarInputProtocol /// Shared annotation to display existing location - let sharedAnnotation: UserLocationAnnotation? + let sharedAnnotation: LocationAnnotation? /// Map annotations to display on map - var annotations: [UserLocationAnnotation] + var annotations: [LocationAnnotation] /// Map annotation to focus on - var highlightedAnnotation: UserLocationAnnotation? + var highlightedAnnotation: LocationAnnotation? /// Indicates whether the user has moved around the map to drop a pin somewhere other than their current location var isPinDropSharing: Bool { diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index e503881ea..1fb1eb5fd 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -37,14 +37,20 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil, coordinateType: LocationSharingCoordinateType, isLiveLocationSharingEnabled: Bool = false) { - var sharedAnnotation: UserLocationAnnotation? - var annotations: [UserLocationAnnotation] = [] - var highlightedAnnotation: UserLocationAnnotation? + var sharedAnnotation: LocationAnnotation? + var annotations: [LocationAnnotation] = [] + var highlightedAnnotation: LocationAnnotation? var showsUserLocation: Bool = false // Displaying an existing location if let sharedCoordinate = location { - let sharedLocationAnnotation = UserLocationAnnotation(avatarData: avatarData, coordinate: sharedCoordinate, coordinateType: coordinateType) + let sharedLocationAnnotation: LocationAnnotation + switch coordinateType { + case .user: + sharedLocationAnnotation = UserLocationAnnotation(avatarData: avatarData, coordinate: sharedCoordinate) + case .pin: + sharedLocationAnnotation = LocationAnnotation(coordinate: sharedCoordinate) + } annotations.append(sharedLocationAnnotation) highlightedAnnotation = sharedLocationAnnotation @@ -80,7 +86,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie case .share: // Share existing location if let location = state.sharedAnnotation?.coordinate { - completion?(.share(latitude: location.latitude, longitude: location.longitude, coordinateType: .generic)) + completion?(.share(latitude: location.latitude, longitude: location.longitude, coordinateType: .user)) return } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift index 8ed2aa8a0..2ba7899ec 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift @@ -78,10 +78,9 @@ class LocationSharingViewModelTests: XCTestCase { viewModel.completion = { result in switch result { - case .share(let latitude, let longitude, let coordinateType): + case .share(let latitude, let longitude, _): XCTAssertEqual(latitude, viewModel.context.viewState.sharedAnnotation?.coordinate.latitude) XCTAssertEqual(longitude, viewModel.context.viewState.sharedAnnotation?.coordinate.longitude) - XCTAssertEqual(coordinateType, viewModel.context.viewState.sharedAnnotation?.coordinateType) expectation.fulfill() case .cancel: XCTFail() @@ -124,6 +123,6 @@ class LocationSharingViewModelTests: XCTestCase { private func buildViewModel(withLocation: Bool) -> LocationSharingViewModel { LocationSharingViewModel(mapStyleURL: URL(string: "http://empty.com")!, avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""), - location: (withLocation ? CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) : nil), coordinateType: .generic) + location: (withLocation ? CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) : nil), coordinateType: .user) } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift index 3b22852bb..10545affb 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -33,10 +33,10 @@ struct LocationSharingMapView: UIViewRepresentable { let tileServerMapURL: URL /// Map annotations - let annotations: [UserLocationAnnotation] + let annotations: [LocationAnnotation] /// Map annotation to focus on - let highlightedAnnotation: UserLocationAnnotation? + let highlightedAnnotation: LocationAnnotation? /// Current user avatar data, used to replace current location annotation view with the user avatar let userAvatarData: AvatarInputProtocol? @@ -117,10 +117,12 @@ extension LocationSharingMapView { func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { if let userLocationAnnotation = annotation as? UserLocationAnnotation { - return UserLocationAnnotatonView(userLocationAnnotation: userLocationAnnotation) + return LocationAnnotatonView(userLocationAnnotation: userLocationAnnotation) + } else if let pinLocationAnnotation = annotation as? LocationAnnotation { + return LocationAnnotatonView(pinLocationAnnotation: pinLocationAnnotation) } else if annotation is MGLUserLocation && locationSharingMapView.mapCenterCoordinate == nil, let currentUserAvatarData = locationSharingMapView.userAvatarData { // Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location - return UserLocationAnnotatonView(avatarData: currentUserAvatarData) + return LocationAnnotatonView(avatarData: currentUserAvatarData) } return nil diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift index e88de275a..58de23f6a 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift @@ -19,7 +19,7 @@ import SwiftUI import Mapbox @available(iOS 14, *) -class UserLocationAnnotatonView: MGLUserLocationAnnotationView { +class LocationAnnotatonView: MGLUserLocationAnnotationView { // MARK: Private @@ -38,14 +38,15 @@ class UserLocationAnnotatonView: MGLUserLocationAnnotationView { // TODO: Use a reuseIdentifier super.init(annotation: userLocationAnnotation, reuseIdentifier: nil) - switch userLocationAnnotation.coordinateType { - case .user: - self.addUserMarkerView(with: userLocationAnnotation.avatarData) - case .pin, .generic: - self.addPinMarkerView() - @unknown default: - return - } + self.addUserMarkerView(with: userLocationAnnotation.avatarData) + + } + + init(pinLocationAnnotation: LocationAnnotation) { + // TODO: Use a reuseIdentifier + super.init(annotation: pinLocationAnnotation, reuseIdentifier: nil) + + self.addPinMarkerView() } required init?(coder: NSCoder) { From bbc4106be4bc2af6ff0214c8e42e5172aac18c7b Mon Sep 17 00:00:00 2001 From: MaximeE Date: Mon, 4 Apr 2022 17:01:47 +0200 Subject: [PATCH 118/135] 5858: Add specific type for pin location --- .../Modules/Room/LocationSharing/LocationAnnotation.swift | 2 ++ .../Modules/Room/LocationSharing/LocationSharingViewModel.swift | 2 +- .../Room/LocationSharing/View/LocationSharingMapView.swift | 2 +- .../Room/LocationSharing/View/UserLocationAnnotatonView.swift | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift index ad276883d..545348f0e 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift @@ -31,6 +31,8 @@ class LocationAnnotation: NSObject, MGLAnnotation { } } +class PinLocationAnnotation: LocationAnnotation {} + class UserLocationAnnotation: LocationAnnotation { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index 1fb1eb5fd..6295aab9c 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -49,7 +49,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie case .user: sharedLocationAnnotation = UserLocationAnnotation(avatarData: avatarData, coordinate: sharedCoordinate) case .pin: - sharedLocationAnnotation = LocationAnnotation(coordinate: sharedCoordinate) + sharedLocationAnnotation = PinLocationAnnotation(coordinate: sharedCoordinate) } annotations.append(sharedLocationAnnotation) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift index 10545affb..5828e5d0c 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -118,7 +118,7 @@ extension LocationSharingMapView { if let userLocationAnnotation = annotation as? UserLocationAnnotation { return LocationAnnotatonView(userLocationAnnotation: userLocationAnnotation) - } else if let pinLocationAnnotation = annotation as? LocationAnnotation { + } else if let pinLocationAnnotation = annotation as? PinLocationAnnotation { return LocationAnnotatonView(pinLocationAnnotation: pinLocationAnnotation) } else if annotation is MGLUserLocation && locationSharingMapView.mapCenterCoordinate == nil, let currentUserAvatarData = locationSharingMapView.userAvatarData { // Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift index 58de23f6a..92395b483 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift @@ -42,7 +42,7 @@ class LocationAnnotatonView: MGLUserLocationAnnotationView { } - init(pinLocationAnnotation: LocationAnnotation) { + init(pinLocationAnnotation: PinLocationAnnotation) { // TODO: Use a reuseIdentifier super.init(annotation: pinLocationAnnotation, reuseIdentifier: nil) From 75a5be19e9939980fffc48f62170a753d2224e32 Mon Sep 17 00:00:00 2001 From: Gil Eluard Date: Mon, 4 Apr 2022 20:07:06 +0200 Subject: [PATCH 119/135] Add support for Apple context menus in matrix items list screens (#5963) --- Riot/Generated/Storyboards.swift | 10 +- .../Common/Recents/RecentsViewController.h | 8 + .../Common/Recents/RecentsViewController.m | 70 +++++- .../PublicRoomActionProvider.swift | 51 ++++ .../ActionProviders/RoomActionProvider.swift | 147 +++++++++++ .../RoomActionProviderProtocol.swift | 24 ++ .../SpaceChildActionProvider.swift | 51 ++++ .../PublicRoomContextMenuProvider.swift | 83 +++++++ .../RecentCellContextMenuProvider.swift | 72 ++++++ ...oomContextPreviewViewController.storyboard | 229 ++++++++++++++++++ .../RoomContextPreviewViewController.swift} | 115 +++++++-- .../Services/RoomContextActionService.swift | 178 ++++++++++++++ .../RoomContextActionServiceProtocol.swift | 32 +++ .../UnownedRoomContextActionService.swift | 90 +++++++ .../PublicRoomContextPreviewViewModel.swift | 59 +++++ .../RoomContextPreviewViewModel.swift | 91 +++++++ .../RoomContextPreviewViewModels.swift | 52 ++++ .../SpaceChildContextPreviewViewModel.swift | 58 +++++ .../Rooms/DirectoryViewController.m | 156 +++++++++--- Riot/Modules/Home/HomeViewController.m | 183 +++----------- Riot/Modules/Room/RoomViewController.h | 2 + Riot/Modules/Room/RoomViewController.m | 5 + .../SpaceExploreRoomViewController.swift | 3 +- .../SpaceRoomPreviewViewController.storyboard | 146 ----------- Riot/SupportingFiles/Riot-Bridging-Header.h | 1 + .../MXRoomNotificationSettingsService.swift | 36 +-- changelog.d/5953.feature | 1 + 27 files changed, 1577 insertions(+), 376 deletions(-) create mode 100644 Riot/Modules/ContextMenu/ActionProviders/PublicRoomActionProvider.swift create mode 100644 Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift create mode 100644 Riot/Modules/ContextMenu/ActionProviders/RoomActionProviderProtocol.swift create mode 100644 Riot/Modules/ContextMenu/ActionProviders/SpaceChildActionProvider.swift create mode 100644 Riot/Modules/ContextMenu/ContextMenuProviders/PublicRoomContextMenuProvider.swift create mode 100644 Riot/Modules/ContextMenu/ContextMenuProviders/RecentCellContextMenuProvider.swift create mode 100644 Riot/Modules/ContextMenu/RoomContextPreviewViewController.storyboard rename Riot/Modules/{Spaces/SpaceRoomList/ExploreRoom/SpaceRoomPreviewViewController.swift => ContextMenu/RoomContextPreviewViewController.swift} (51%) create mode 100644 Riot/Modules/ContextMenu/Services/RoomContextActionService.swift create mode 100644 Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift create mode 100644 Riot/Modules/ContextMenu/Services/UnownedRoomContextActionService.swift create mode 100644 Riot/Modules/ContextMenu/ViewModels/PublicRoomContextPreviewViewModel.swift create mode 100644 Riot/Modules/ContextMenu/ViewModels/RoomContextPreviewViewModel.swift create mode 100644 Riot/Modules/ContextMenu/ViewModels/RoomContextPreviewViewModels.swift create mode 100644 Riot/Modules/ContextMenu/ViewModels/SpaceChildContextPreviewViewModel.swift delete mode 100644 Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceRoomPreviewViewController.storyboard create mode 100644 changelog.d/5953.feature diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift index ac4781afb..0ccec6bad 100644 --- a/Riot/Generated/Storyboards.swift +++ b/Riot/Generated/Storyboards.swift @@ -162,6 +162,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: ReactionHistoryViewController.self) } + internal enum RoomContextPreviewViewController: StoryboardType { + internal static let storyboardName = "RoomContextPreviewViewController" + + internal static let initialScene = InitialSceneType(storyboard: RoomContextPreviewViewController.self) + } internal enum RoomContextualMenuViewController: StoryboardType { internal static let storyboardName = "RoomContextualMenuViewController" @@ -279,11 +284,6 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: SpaceMenuViewController.self) } - internal enum SpaceRoomPreviewViewController: StoryboardType { - internal static let storyboardName = "SpaceRoomPreviewViewController" - - internal static let initialScene = InitialSceneType(storyboard: SpaceRoomPreviewViewController.self) - } internal enum TemplateScreenViewController: StoryboardType { internal static let storyboardName = "TemplateScreenViewController" diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h index a2fefb47d..09a9ee572 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.h +++ b/Riot/Modules/Common/Recents/RecentsViewController.h @@ -20,6 +20,7 @@ @class RootTabEmptyView; @class AnalyticsScreenTracker; @class UserIndicatorStore; +@class RecentCellContextMenuProvider; /** Notification to be posted when recents data is ready. Notification object will be the RecentsViewController instance. @@ -103,6 +104,8 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification; */ @property (nonatomic, strong) UserIndicatorStore *userIndicatorStore; +@property (nonatomic, readonly) RecentCellContextMenuProvider *contextMenuProvider; + /** Return the sticky header for the specified section of the table view @@ -198,6 +201,11 @@ Enable/disable the notifications for the selected room. */ - (void)openPublicRoom:(MXPublicRoom *)publicRoom; +/** + Show a room using its roomID + */ +- (void)showRoomWithRoomId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSession; + #pragma mark - Scrolling /** diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index ea12f0516..772b23260 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -36,7 +36,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewControllerDataReadyNotification"; -@interface RecentsViewController () +@interface RecentsViewController () { // Tell whether a recents refresh is pending (suspended during editing mode). BOOL isRefreshPending; @@ -138,6 +138,9 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro displayedSectionHeaders = [NSMutableArray array]; + _contextMenuProvider = [RecentCellContextMenuProvider new]; + self.contextMenuProvider.serviceDelegate = self; + // Set itself as delegate by default. self.delegate = self; } @@ -2487,4 +2490,69 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } } +#pragma mark - Context Menu + +- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) +{ + id cellData = [self.dataSource cellDataAtIndexPath:indexPath]; + UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + + if (!cellData || !cell) + { + return nil; + } + + return [self.contextMenuProvider contextMenuConfigurationWith:cellData from:cell session:self.dataSource.mxSession]; +} + +- (void)tableView:(UITableView *)tableView willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id)animator API_AVAILABLE(ios(13.0)) +{ + NSString *roomId = [self.contextMenuProvider roomIdFrom:configuration.identifier]; + + if (!roomId) + { + return; + } + + [animator addCompletion:^{ + [self showRoomWithRoomId:roomId inMatrixSession:self.mainSession]; + }]; +} + +#pragma mark - RoomContextActionServiceDelegate + +- (void)roomContextActionServiceDidJoinRoom:(id)service +{ + [self showRoomWithRoomId:service.roomId inMatrixSession:service.session]; +} + +- (void)roomContextActionServiceDidLeaveRoom:(id)service +{ + [self.userIndicatorStore presentSuccessWithLabel:VectorL10n.roomParticipantsLeaveSuccess]; +} + +- (void)roomContextActionService:(id)service presentAlert:(UIAlertController *)alertController +{ + [self presentViewController:alertController animated:YES completion:nil]; +} + +- (void)roomContextActionService:(id)service updateActivityIndicator:(BOOL)isActive +{ + if (isActive) + { + [self startActivityIndicator]; + } + else if ([self canStopActivityIndicator]) + { + [self stopActivityIndicator]; + } +} + +- (void)roomContextActionService:(id)service showRoomNotificationSettingsForRoomWithId:(NSString *)roomId +{ + editedRoomId = roomId; + [self changeEditedRoomNotificationSettings]; + editedRoomId = nil; +} + @end diff --git a/Riot/Modules/ContextMenu/ActionProviders/PublicRoomActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/PublicRoomActionProvider.swift new file mode 100644 index 000000000..c2d340797 --- /dev/null +++ b/Riot/Modules/ContextMenu/ActionProviders/PublicRoomActionProvider.swift @@ -0,0 +1,51 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `PublicRoomActionProvider` provides the menu for `MXPUblicRoom` instances +@available(iOS 13.0, *) +class PublicRoomActionProvider: RoomActionProviderProtocol { + + // MARK: - Properties + + private let publicRoom: MXPublicRoom + private let service: UnownedRoomContextActionService + + // MARK: - Setup + + init(publicRoom: MXPublicRoom, service: UnownedRoomContextActionService) { + self.publicRoom = publicRoom + self.service = service + } + + // MARK: - RoomActionProviderProtocol + + var menu: UIMenu { + return UIMenu(children: [ + self.joinAction + ]) + } + + // MARK: - Private + + private var joinAction: UIAction { + return UIAction( + title: VectorL10n.join) { [weak self] action in + self?.service.joinRoom() + } + } +} diff --git a/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift new file mode 100644 index 000000000..12346a193 --- /dev/null +++ b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProvider.swift @@ -0,0 +1,147 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// `RoomActionProvider` provides the menu for `MXRoom` instances +@available(iOS 13.0, *) +class RoomActionProvider: RoomActionProviderProtocol { + + // MARK: - Properties + + private let service: RoomContextActionService + + // MARK: - Setup + + init(service: RoomContextActionService) { + self.service = service + } + + // MARK: - RoomActionProviderProtocol + + var menu: UIMenu { + if service.isRoomJoined { + return UIMenu(children: [ + self.directChatAction, + self.notificationsAction, + self.favouriteAction, + self.lowPriorityAction, + self.leaveAction + ]) + } else { + if service.roomMembership == .invite { + return UIMenu(children: [ + self.acceptInviteAction, + self.declineInviteAction + ]) + } else { + return UIMenu(children: [ + self.joinAction + ]) + } + } + } + + // MARK: - Private + + private var directChatAction: UIAction { + return UIAction( + title: service.isRoomDirect ? VectorL10n.homeContextMenuMakeRoom : VectorL10n.homeContextMenuMakeDm, + image: UIImage(systemName: service.isRoomDirect ? "person.crop.circle.badge.xmark" : "person.circle")) { [weak self] action in + guard let self = self else { return } + self.service.isRoomDirect = !self.service.isRoomDirect + } + } + + private var notificationsAction: UIAction { + let notificationsImage: UIImage? + let notificationsTitle: String + if BuildSettings.showNotificationsV2 { + notificationsTitle = VectorL10n.homeContextMenuNotifications + notificationsImage = UIImage(systemName: "bell") + } else { + notificationsTitle = service.isRoomMuted ? VectorL10n.homeContextMenuUnmute : VectorL10n.homeContextMenuMute + notificationsImage = UIImage(systemName: service.isRoomMuted ? "bell.slash": "bell") + } + + return UIAction( + title: notificationsTitle, + image: notificationsImage) { [weak self] action in + guard let self = self else { return } + self.service.isRoomMuted = !self.service.isRoomMuted + } + } + + private var favouriteAction: UIAction { + return UIAction( + title: self.service.isRoomFavourite ? VectorL10n.homeContextMenuUnfavourite : VectorL10n.homeContextMenuFavourite, + image: UIImage(systemName: self.service.isRoomFavourite ? "star.slash" : "star")) { [weak self] action in + guard let self = self else { return } + self.service.isRoomFavourite = !self.service.isRoomFavourite + } + } + + private var lowPriorityAction: UIAction { + return UIAction( + title: self.service.isRoomLowPriority ? VectorL10n.homeContextMenuNormalPriority : VectorL10n.homeContextMenuLowPriority, + image: UIImage(systemName: self.service.isRoomLowPriority ? "arrow.up" : "arrow.down")) { [weak self] action in + guard let self = self else { return } + self.service.isRoomLowPriority = !self.service.isRoomLowPriority + } + } + + private var leaveAction: UIAction { + let image: UIImage? + if #available(iOS 14.0, *) { + image = UIImage(systemName: "rectangle.righthalf.inset.fill.arrow.right") + } else { + image = UIImage(systemName: "rectangle.xmark") + } + + let action = UIAction(title: VectorL10n.homeContextMenuLeave, image: image) { [weak self] action in + guard let self = self else { return } + self.service.leaveRoom(promptUser: true) + } + action.attributes = .destructive + return action + } + + private var acceptInviteAction: UIAction { + return UIAction( + title: VectorL10n.accept) { [weak self] action in + guard let self = self else { return } + self.service.joinRoom() + } + } + + private var declineInviteAction: UIAction { + let action = UIAction( + title: VectorL10n.decline) { [weak self] action in + guard let self = self else { return } + self.service.leaveRoom(promptUser: false) + } + action.attributes = .destructive + return action + } + + private var joinAction: UIAction { + return UIAction( + title: VectorL10n.join) { [weak self] action in + guard let self = self else { return } + self.service.joinRoom() + } + } +} diff --git a/Riot/Modules/ContextMenu/ActionProviders/RoomActionProviderProtocol.swift b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProviderProtocol.swift new file mode 100644 index 000000000..21d5f6ef3 --- /dev/null +++ b/Riot/Modules/ContextMenu/ActionProviders/RoomActionProviderProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Classes compliant with the protocol `RoomActionProviderProtocol` are meant to provide the menu within `UIContextMenuActionProvider` +@available(iOS 13.0, *) +protocol RoomActionProviderProtocol { + /// menu instance returned within the `UIContextMenuActionProvider` block + var menu: UIMenu { get } +} diff --git a/Riot/Modules/ContextMenu/ActionProviders/SpaceChildActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/SpaceChildActionProvider.swift new file mode 100644 index 000000000..97fd5efb4 --- /dev/null +++ b/Riot/Modules/ContextMenu/ActionProviders/SpaceChildActionProvider.swift @@ -0,0 +1,51 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `SpaceChildActionProvider` provides the menu for `MXSpaceChildInfo` instances +@available(iOS 13.0, *) +class SpaceChildActionProvider: RoomActionProviderProtocol { + + // MARK: - Properties + + private let spaceChildInfo: MXSpaceChildInfo + private let service: UnownedRoomContextActionService + + // MARK: - Setup + + init(spaceChildInfo: MXSpaceChildInfo, service: UnownedRoomContextActionService) { + self.spaceChildInfo = spaceChildInfo + self.service = service + } + + // MARK: - RoomActionProviderProtocol + + var menu: UIMenu { + return UIMenu(children: [ + self.joinAction + ]) + } + + // MARK: - Private + + private var joinAction: UIAction { + return UIAction( + title: VectorL10n.join) { [weak self] action in + self?.service.joinRoom() + } + } +} diff --git a/Riot/Modules/ContextMenu/ContextMenuProviders/PublicRoomContextMenuProvider.swift b/Riot/Modules/ContextMenu/ContextMenuProviders/PublicRoomContextMenuProvider.swift new file mode 100644 index 000000000..a9a800517 --- /dev/null +++ b/Riot/Modules/ContextMenu/ContextMenuProviders/PublicRoomContextMenuProvider.swift @@ -0,0 +1,83 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Helper class `PublicRoomContextMenuProvider` that provides an instace of `UIContextMenuConfiguration` from an instance of `MXPublicRoom` +@objcMembers +class PublicRoomContextMenuProvider: NSObject { + + weak var serviceDelegate: RoomContextActionServiceDelegate? + private var currentService: RoomContextActionServiceProtocol? + + @available(iOS 13.0, *) + func contextMenuConfiguration(with publicRoom: MXPublicRoom, from cell: UIView, session: MXSession) -> UIContextMenuConfiguration? { + if let room = session.room(withRoomId: publicRoom.roomId) { + let service = RoomContextActionService(room: room, delegate: serviceDelegate) + self.currentService = service + let actionProvider = RoomActionProvider(service: service) + return UIContextMenuConfiguration(identifier: publicRoom.jsonString() as? NSString) { + if room.summary?.isJoined == true { + let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main) + guard let roomViewController = storyboard.instantiateViewController(withIdentifier: "RoomViewControllerStoryboardId") as? RoomViewController else { + return nil + } + roomViewController.isContextPreview = true + + let roomDataSourceManager = MXKRoomDataSourceManager.sharedManager(forMatrixSession: session) + roomDataSourceManager?.roomDataSource(forRoom: room.roomId, create: true, onComplete: { roomDataSource in + roomViewController.displayRoom(roomDataSource) + }) + + return roomViewController + } else { + let viewModel = RoomContextPreviewViewModel(room: room) + return RoomContextPreviewViewController.instantiate(with: viewModel, mediaManager: session.mediaManager) + } + } actionProvider: { suggestedActions in + return actionProvider.menu + } + } else { + let service = UnownedRoomContextActionService(roomId: publicRoom.roomId, canonicalAlias: publicRoom.canonicalAlias, session: session, delegate: serviceDelegate) + self.currentService = service + let actionProvider = PublicRoomActionProvider(publicRoom: publicRoom, service: service) + return UIContextMenuConfiguration(identifier: publicRoom.jsonString() as? NSString) { + let viewModel = PublicRoomContextPreviewViewModel(publicRoom: publicRoom) + return RoomContextPreviewViewController.instantiate(with: viewModel, mediaManager: session.mediaManager) + } actionProvider: { suggestedActions in + return actionProvider.menu + } + } + } + + func publicRoom(from identifier: NSCopying) -> MXPublicRoom? { + guard let jsonString = identifier as? String, let data = jsonString.data(using: .utf8) else { + return nil + } + + do { + let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + + guard let publicRoom = MXPublicRoom(fromJSON: json) else { + return nil + } + + return publicRoom + } catch { + return nil + } + } +} diff --git a/Riot/Modules/ContextMenu/ContextMenuProviders/RecentCellContextMenuProvider.swift b/Riot/Modules/ContextMenu/ContextMenuProviders/RecentCellContextMenuProvider.swift new file mode 100644 index 000000000..066499ff3 --- /dev/null +++ b/Riot/Modules/ContextMenu/ContextMenuProviders/RecentCellContextMenuProvider.swift @@ -0,0 +1,72 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Helper class `RecentCellContextMenuProvider` that provides an instace of `UIContextMenuConfiguration` from an instance of `MXKRecentCellDataStoring` +@objcMembers +class RecentCellContextMenuProvider: NSObject { + + weak var serviceDelegate: RoomContextActionServiceDelegate? + private var currentService: RoomContextActionServiceProtocol? + + @available(iOS 13.0, *) + func contextMenuConfiguration(with cellData: MXKRecentCellDataStoring, from cell: UIView, session: MXSession) -> UIContextMenuConfiguration? { + if cellData.isSuggestedRoom, let childInfo = cellData.roomSummary.spaceChildInfo { + let service = UnownedRoomContextActionService(roomId: childInfo.childRoomId, canonicalAlias: childInfo.canonicalAlias, session: session, delegate: serviceDelegate) + self.currentService = service + let actionProvider = SpaceChildActionProvider(spaceChildInfo: childInfo, service: service) + return UIContextMenuConfiguration(identifier: "" as NSString) { + let viewModel = SpaceChildContextPreviewViewModel(childInfo: childInfo) + return RoomContextPreviewViewController.instantiate(with: viewModel, mediaManager: session.mediaManager) + } actionProvider: { suggestedActions in + return actionProvider.menu + } + } else if let room = session.room(withRoomId: cellData.roomIdentifier) { + let service = RoomContextActionService(room: room, delegate: serviceDelegate) + self.currentService = service + let actionProvider = RoomActionProvider(service: service) + return UIContextMenuConfiguration(identifier: cellData.roomIdentifier as NSString) { + if room.summary?.isJoined == true { + let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main) + guard let roomViewController = storyboard.instantiateViewController(withIdentifier: "RoomViewControllerStoryboardId") as? RoomViewController else { + return nil + } + roomViewController.isContextPreview = true + + let roomDataSourceManager = MXKRoomDataSourceManager.sharedManager(forMatrixSession: session) + roomDataSourceManager?.roomDataSource(forRoom: room.roomId, create: true, onComplete: { roomDataSource in + roomViewController.displayRoom(roomDataSource) + }) + + return roomViewController + } else { + let viewModel = RoomContextPreviewViewModel(room: room) + return RoomContextPreviewViewController.instantiate(with: viewModel, mediaManager: session.mediaManager) + } + } actionProvider: { suggestedActions in + return actionProvider.menu + } + } + + return nil + } + + func roomId(from identifier: NSCopying) -> String? { + let roomId = identifier as? String + return roomId?.isEmpty == true ? nil : roomId + } +} diff --git a/Riot/Modules/ContextMenu/RoomContextPreviewViewController.storyboard b/Riot/Modules/ContextMenu/RoomContextPreviewViewController.storyboard new file mode 100644 index 000000000..d9c7dc7df --- /dev/null +++ b/Riot/Modules/ContextMenu/RoomContextPreviewViewController.storyboard @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceRoomPreviewViewController.swift b/Riot/Modules/ContextMenu/RoomContextPreviewViewController.swift similarity index 51% rename from Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceRoomPreviewViewController.swift rename to Riot/Modules/ContextMenu/RoomContextPreviewViewController.swift index 57b7af624..7a9f49b57 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceRoomPreviewViewController.swift +++ b/Riot/Modules/ContextMenu/RoomContextPreviewViewController.swift @@ -17,8 +17,10 @@ */ import UIKit +import MatrixSDK -final class SpaceRoomPreviewViewController: UIViewController { +/// `RoomContextPreviewViewController` is used to dsplay room preview data within a `UIContextMenuContentPreviewProvider` +final class RoomContextPreviewViewController: UIViewController { // MARK: - Constants @@ -39,19 +41,25 @@ final class SpaceRoomPreviewViewController: UIViewController { @IBOutlet private weak var topicLabelBottomMargin: NSLayoutConstraint! @IBOutlet private weak var spaceTagView: UIView! @IBOutlet private weak var spaceTagLabel: UILabel! + @IBOutlet private weak var stackView: UIStackView! + @IBOutlet private weak var inviteHeaderView: UIView! + @IBOutlet private weak var inviterAvatarView: UserAvatarView! + @IBOutlet private weak var inviteTitleLabel: UILabel! + @IBOutlet private weak var inviteDetailLabel: UILabel! + @IBOutlet private weak var inviteSeparatorView: UIView! // MARK: Private private var theme: Theme! - private var roomInfo: MXSpaceChildInfo! - private var avatarViewData: AvatarViewDataProtocol! + private var viewModel: RoomContextPreviewViewModelProtocol! + private var mediaManager: MXMediaManager? // MARK: - Setup - class func instantiate(with roomInfo: MXSpaceChildInfo, avatarViewData: AvatarViewDataProtocol!) -> SpaceRoomPreviewViewController { - let viewController = StoryboardScene.SpaceRoomPreviewViewController.initialScene.instantiate() - viewController.roomInfo = roomInfo - viewController.avatarViewData = avatarViewData + class func instantiate(with viewModel: RoomContextPreviewViewModelProtocol, mediaManager: MXMediaManager?) -> RoomContextPreviewViewController { + let viewController = StoryboardScene.RoomContextPreviewViewController.initialScene.instantiate() + viewController.viewModel = viewModel + viewController.mediaManager = mediaManager viewController.theme = ThemeService.shared().theme return viewController } @@ -60,11 +68,14 @@ final class SpaceRoomPreviewViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - + // Do any additional setup after loading the view. + viewModel.viewDelegate = self + setupView() self.registerThemeServiceDidChangeThemeNotification() self.update(theme: self.theme) + self.viewModel.process(viewAction: .loadData) } override var preferredContentSize: CGSize { @@ -105,6 +116,15 @@ final class SpaceRoomPreviewViewController: UIViewController { self.spaceTagView.backgroundColor = theme.colors.quinaryContent self.spaceTagLabel.font = theme.fonts.caption1 self.spaceTagLabel.textColor = theme.colors.tertiaryContent + + self.inviteTitleLabel.textColor = theme.colors.tertiaryContent + self.inviteTitleLabel.font = theme.fonts.calloutSB + + self.inviteDetailLabel.textColor = theme.colors.tertiaryContent + self.inviteDetailLabel.font = theme.fonts.caption1 + + self.inviteSeparatorView.backgroundColor = theme.colors.quinaryContent + self.inviterAvatarView.alpha = 0.7 } private func registerThemeServiceDidChangeThemeNotification() { @@ -115,42 +135,85 @@ final class SpaceRoomPreviewViewController: UIViewController { self.update(theme: ThemeService.shared().theme) } - private func setupView() { - self.titleLabel.text = roomInfo.displayName - - self.spaceTagView.layer.masksToBounds = true - self.spaceTagView.layer.cornerRadius = 2 - self.spaceTagView.isHidden = roomInfo.roomType != .space - self.spaceTagLabel.text = VectorL10n.spaceTag + private func renderLoaded(with parameters: RoomContextPreviewLoadedParameters) { + self.titleLabel.text = parameters.displayName - self.avatarView.isHidden = roomInfo.roomType == .space - self.spaceAvatarView.isHidden = roomInfo.roomType != .space + self.spaceTagView.isHidden = parameters.roomType != .space + self.avatarView.isHidden = parameters.roomType == .space + self.spaceAvatarView.isHidden = parameters.roomType != .space + + let avatarViewData = AvatarViewData(matrixItemId: parameters.roomId, + displayName: parameters.displayName, + avatarUrl: parameters.avatarUrl, + mediaManager: mediaManager, + fallbackImage: .matrixItem(parameters.roomId, parameters.displayName)) + if !self.avatarView.isHidden { self.avatarView.fill(with: avatarViewData) } if !self.spaceAvatarView.isHidden { self.spaceAvatarView.fill(with: avatarViewData) } - self.membersLabel.text = roomInfo.activeMemberCount == 1 ? VectorL10n.roomTitleOneMember : VectorL10n.roomTitleMembers("\(roomInfo.activeMemberCount)") - if roomInfo.childrenIds.count == 1 { - self.roomsLabel.text = VectorL10n.spacesExploreRoomsOneRoom - } else { - self.roomsLabel.text = VectorL10n.spacesExploreRoomsRoomNumber("\(roomInfo.childrenIds.count)") + + if parameters.membership != .invite { + self.stackView.removeArrangedSubview(self.inviteHeaderView) + self.inviteHeaderView.isHidden = true } - self.topicLabel.text = roomInfo.topic + + self.membersLabel.text = parameters.membersCount == 1 ? VectorL10n.roomTitleOneMember : VectorL10n.roomTitleMembers("\(parameters.membersCount)") + + if let inviterId = parameters.inviterId { + if let inviter = parameters.inviter { + let avatarData = AvatarViewData(matrixItemId: inviterId, + displayName: inviter.displayname, + avatarUrl: inviter.avatarUrl, + mediaManager: mediaManager, + fallbackImage: .matrixItem(inviterId, inviter.displayname)) + self.inviterAvatarView.fill(with: avatarData) + if let inviterName = inviter.displayname { + self.inviteTitleLabel.text = VectorL10n.noticeRoomInviteYou(inviterName) + self.inviteDetailLabel.text = inviterId + } else { + self.inviteTitleLabel.text = VectorL10n.noticeRoomInviteYou(inviterId) + } + } else { + self.inviteTitleLabel.text = VectorL10n.noticeRoomInviteYou(inviterId) + } + } + + self.topicLabel.text = parameters.topic topicLabelBottomMargin.constant = self.topicLabel.text.isEmptyOrNil ? 0 : 16 - self.roomsIconView.isHidden = roomInfo.roomType != .space - self.roomsLabel.isHidden = roomInfo.roomType != .space + self.roomsIconView.isHidden = parameters.roomType != .space + self.roomsLabel.isHidden = parameters.roomType != .space + + self.view.layoutIfNeeded() + } + + private func setupView() { + self.spaceTagView.layer.masksToBounds = true + self.spaceTagView.layer.cornerRadius = 2 + self.spaceTagLabel.text = VectorL10n.spaceTag } private func intrisicHeight(with width: CGFloat) -> CGFloat { if self.topicLabel.text.isEmptyOrNil { return self.topicLabel.frame.minY } - + let topicHeight = self.topicLabel.sizeThatFits(CGSize(width: width - self.topicLabel.frame.minX * 2, height: 0)).height return self.topicLabel.frame.minY + topicHeight + 16 } } + +// MARK: - RoomContextPreviewViewModelViewDelegate + +extension RoomContextPreviewViewController: RoomContextPreviewViewModelViewDelegate { + func roomContextPreviewViewModel(_ viewModel: RoomContextPreviewViewModelProtocol, didUpdateViewState viewSate: RoomContextPreviewViewState) { + switch viewSate { + case .loaded(let parameters): + self.renderLoaded(with: parameters) + } + } +} diff --git a/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift new file mode 100644 index 000000000..add4db41e --- /dev/null +++ b/Riot/Modules/ContextMenu/Services/RoomContextActionService.swift @@ -0,0 +1,178 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `RoomContextActionService` implements all the possible actions for an instance of `MXRoom` +class RoomContextActionService: NSObject, RoomContextActionServiceProtocol { + + // MARK: - RoomContextActionServiceProtocol + + private(set) var session: MXSession + var roomId: String { + return room.roomId + } + internal weak var delegate: RoomContextActionServiceDelegate? + + // MARK: - Properties + + private let room: MXRoom + private let unownedRoomService: UnownedRoomContextActionService + + // MARK: - Setup + + init(room: MXRoom, delegate: RoomContextActionServiceDelegate?) { + self.room = room + self.delegate = delegate + self.isRoomJoined = room.summary?.isJoined ?? false + self.roomMembership = room.summary?.membership ?? .unknown + self.session = room.mxSession + self.unownedRoomService = UnownedRoomContextActionService(roomId: room.roomId, canonicalAlias: room.summary?.aliases?.first, session: self.session, delegate: delegate) + } + + // MARK: - Public + + let isRoomJoined: Bool + let roomMembership: MXMembership + + var isRoomDirect: Bool { + get { + return room.isDirect + } + set { + delegate?.roomContextActionService(self, updateActivityIndicator: true) + room.setIsDirect(newValue, withUserId: nil) { [weak self] in + guard let self = self else { return } + self.delegate?.roomContextActionService(self, updateActivityIndicator: false) + } failure: { [weak self] error in + guard let self = self else { return } + self.delegate?.roomContextActionService(self, updateActivityIndicator: false) + + // Notify the end user + if let userId = self.session.myUserId { + NotificationCenter.default.post(name: NSNotification.Name.mxkError, object: error, userInfo: [kMXKErrorUserIdKey: userId]) + } else { + NotificationCenter.default.post(name: NSNotification.Name.mxkError, object: error) + } + } + } + } + + var isRoomMuted: Bool { + get { + return room.isMuted || room.isMentionsOnly + } + set { + if BuildSettings.showNotificationsV2 { + self.delegate?.roomContextActionService(self, showRoomNotificationSettingsForRoomWithId: room.roomId) + } else { + self.muteRoomNotifications(newValue) + } + } + } + + var isRoomFavourite: Bool { + get { + let currentTag = room.accountData.tags?.values.first + return currentTag?.name == kMXRoomTagFavourite + } + set { + self.updateRoom(tag: newValue ? kMXRoomTagFavourite : nil) + } + } + + var isRoomLowPriority: Bool { + get { + let currentTag = room.accountData.tags?.values.first + return currentTag?.name == kMXRoomTagLowPriority + } + set { + self.updateRoom(tag: newValue ? kMXRoomTagLowPriority : nil) + } + } + + private func muteRoomNotifications(_ isMuted: Bool) { + self.delegate?.roomContextActionService(self, updateActivityIndicator: true) + if isMuted { + room.mentionsOnly { [weak self] in + guard let self = self else { return } + self.delegate?.roomContextActionService(self, updateActivityIndicator: false) + } + } else { + room.allMessages { [weak self] in + guard let self = self else { return } + self.delegate?.roomContextActionService(self, updateActivityIndicator: false) + } + } + } + + private func updateRoom(tag: String?) { + self.delegate?.roomContextActionService(self, updateActivityIndicator: true) + room.setRoomTag(tag) { + self.delegate?.roomContextActionService(self, updateActivityIndicator: false) + } + } + + func leaveRoom(promptUser: Bool) { + guard promptUser else { + self.leaveRoom() + return + } + + let title = room.isDirect ? VectorL10n.roomParticipantsLeavePromptTitleForDm : VectorL10n.roomParticipantsLeavePromptTitle + let message = room.isDirect ? VectorL10n.roomParticipantsLeavePromptMsgForDm : VectorL10n.roomParticipantsLeavePromptMsg + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: VectorL10n.cancel, style: .cancel, handler: nil)) + alertController.addAction(UIAlertAction(title: VectorL10n.leave, style: .default, handler: { action in + self.leaveRoom() + })) + self.delegate?.roomContextActionService(self, presentAlert: alertController) + } + + func joinRoom() { + unownedRoomService.joinRoom() + } + + private func leaveRoom() { + self.delegate?.roomContextActionService(self, updateActivityIndicator: true) + // cancel pending uploads/downloads + // they are useless by now + MXMediaManager.cancelDownloads(inCacheFolder: self.room.roomId) + + // TODO: GFO cancel pending uploads related to this room + + MXLog.debug("[RoomContextActionService] leaving room \(self.room.roomId ?? "nil")") + + self.room.leave { [weak self] response in + guard let self = self else { return } + + switch response { + case .success: + self.delegate?.roomContextActionService(self, updateActivityIndicator: false) + self.delegate?.roomContextActionServiceDidLeaveRoom(self) + case .failure(let error): + self.delegate?.roomContextActionService(self, updateActivityIndicator: false) + // Notify the end user + if let userId = self.session.myUserId { + NotificationCenter.default.post(name: NSNotification.Name.mxkError, object: error, userInfo: [kMXKErrorUserIdKey: userId]) + } else { + NotificationCenter.default.post(name: NSNotification.Name.mxkError, object: error) + } + } + } + } +} diff --git a/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift b/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift new file mode 100644 index 000000000..d44213bd4 --- /dev/null +++ b/Riot/Modules/ContextMenu/Services/RoomContextActionServiceProtocol.swift @@ -0,0 +1,32 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc protocol RoomContextActionServiceDelegate { + func roomContextActionService(_ service: RoomContextActionServiceProtocol, updateActivityIndicator isActive: Bool) + func roomContextActionService(_ service: RoomContextActionServiceProtocol, presentAlert alertController: UIAlertController) + func roomContextActionService(_ service: RoomContextActionServiceProtocol, showRoomNotificationSettingsForRoomWithId roomId: String) + func roomContextActionServiceDidJoinRoom(_ service: RoomContextActionServiceProtocol) + func roomContextActionServiceDidLeaveRoom(_ service: RoomContextActionServiceProtocol) +} + +/// `RoomContextActionServiceProtocol` classes are meant to be called by a `RoomActionProviderProtocol` instance so it provides the implementation of the menu actions. +@objc protocol RoomContextActionServiceProtocol { + var delegate: RoomContextActionServiceDelegate? { get set } + var roomId: String { get } + var session: MXSession { get } +} diff --git a/Riot/Modules/ContextMenu/Services/UnownedRoomContextActionService.swift b/Riot/Modules/ContextMenu/Services/UnownedRoomContextActionService.swift new file mode 100644 index 000000000..a23cc706c --- /dev/null +++ b/Riot/Modules/ContextMenu/Services/UnownedRoomContextActionService.swift @@ -0,0 +1,90 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `RoomContextActionService` implements all the possible actions for a room not owned by the user (e.g. `MXPublicRoom`, `MXSpaceChildInfo`) +class UnownedRoomContextActionService: NSObject, RoomContextActionServiceProtocol { + + // MARK: - RoomContextActionServiceProtocol + + internal let roomId: String + internal let session: MXSession + internal weak var delegate: RoomContextActionServiceDelegate? + + // MARK: - Properties + + private let canonicalAlias: String? + + // MARK: - Setup + + init(roomId: String, canonicalAlias: String?, session: MXSession, delegate: RoomContextActionServiceDelegate?) { + self.roomId = roomId + self.canonicalAlias = canonicalAlias + self.session = session + self.delegate = delegate + } + + // MARK: - Public + + func joinRoom() { + self.delegate?.roomContextActionService(self, updateActivityIndicator: true) + if let canonicalAlias = canonicalAlias { + self.session.matrixRestClient.resolveRoomAlias(canonicalAlias) { [weak self] (response) in + guard let self = self else { return } + switch response { + case .success(let resolution): + self.joinRoom(withId: resolution.roomId, via: resolution.servers) + case .failure(let error): + MXLog.warning("[UnownedRoomContextActionService] joinRoom: failed to resolve room alias due to error \(error).") + self.joinRoom(withId: self.roomId, via: nil) + } + } + } else { + MXLog.warning("[UnownedRoomContextActionService] joinRoom: no canonical alias provided.") + joinRoom(withId: self.roomId, via: nil) + } + } + + // MARK: - Private + + private func joinRoom(withId roomId: String, via viaServers: [String]?) { + self.session.joinRoom(roomId, viaServers: viaServers, withSignUrl: nil) { [weak self] response in + guard let self = self else { return } + switch response { + case .success: + self.delegate?.roomContextActionService(self, updateActivityIndicator: false) + self.delegate?.roomContextActionServiceDidJoinRoom(self) + case .failure(let error): + self.delegate?.roomContextActionService(self, updateActivityIndicator: false) + self.delegate?.roomContextActionService(self, presentAlert: self.roomJoinFailedAlert(with: error)) + } + } + } + + private func roomJoinFailedAlert(with error: Error) -> UIAlertController { + var message = (error as NSError).userInfo[NSLocalizedDescriptionKey] as? String + if message == "No known servers" { + // minging kludge until https://matrix.org/jira/browse/SYN-678 is fixed + // 'Error when trying to join an empty room should be more explicit' + message = VectorL10n.roomErrorJoinFailedEmptyRoom + } + + let alertController = UIAlertController(title: VectorL10n.roomErrorJoinFailedTitle, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil)) + return alertController + } +} diff --git a/Riot/Modules/ContextMenu/ViewModels/PublicRoomContextPreviewViewModel.swift b/Riot/Modules/ContextMenu/ViewModels/PublicRoomContextPreviewViewModel.swift new file mode 100644 index 000000000..72c0f2ba8 --- /dev/null +++ b/Riot/Modules/ContextMenu/ViewModels/PublicRoomContextPreviewViewModel.swift @@ -0,0 +1,59 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `PublicRoomContextPreviewViewModel` provides the data to the `RoomContextPreviewViewController` from an instance of `MXPublicRoom` +class PublicRoomContextPreviewViewModel: RoomContextPreviewViewModelProtocol { + + // MARK: - Properties + + private let publicRoom: MXPublicRoom + weak var viewDelegate: RoomContextPreviewViewModelViewDelegate? + + // MARK: - Setup + + init(publicRoom: MXPublicRoom) { + self.publicRoom = publicRoom + } + + // MARK: - RoomContextPreviewViewModelProtocol + + func process(viewAction: RoomContextPreviewViewAction) { + switch viewAction { + case .loadData: + self.loadData() + } + } + + // MARK: - Private + + private func loadData() { + let mapper = MXRoomTypeMapper(defaultRoomType: .room) + let parameters = RoomContextPreviewLoadedParameters( + roomId: publicRoom.roomId, + roomType: mapper.roomType(from: publicRoom.roomTypeString), + displayName: publicRoom.name, + topic: publicRoom.topic, + avatarUrl: publicRoom.avatarUrl, + joinRule: .none, + membership: .unknown, + inviterId: nil, + inviter: nil, + membersCount: publicRoom.numJoinedMembers) + self.viewDelegate?.roomContextPreviewViewModel(self, didUpdateViewState: .loaded(parameters)) + } +} diff --git a/Riot/Modules/ContextMenu/ViewModels/RoomContextPreviewViewModel.swift b/Riot/Modules/ContextMenu/ViewModels/RoomContextPreviewViewModel.swift new file mode 100644 index 000000000..8227fb33d --- /dev/null +++ b/Riot/Modules/ContextMenu/ViewModels/RoomContextPreviewViewModel.swift @@ -0,0 +1,91 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `RoomContextPreviewViewModel` provides the data to the `RoomContextPreviewViewController` from an instance of `MXRoom` +class RoomContextPreviewViewModel: RoomContextPreviewViewModelProtocol { + + // MARK: - Properties + + private let room: MXRoom + weak var viewDelegate: RoomContextPreviewViewModelViewDelegate? + + // MARK: - Setup + + init(room: MXRoom) { + self.room = room + } + + // MARK: - RoomContextPreviewViewModelProtocol + + func process(viewAction: RoomContextPreviewViewAction) { + switch viewAction { + case .loadData: + self.loadData() + } + } + + // MARK: - Private + + private func loadData() { + let parameters = RoomContextPreviewLoadedParameters( + roomId: self.room.roomId, + roomType: self.room.summary?.roomType ?? .none, + displayName: self.room.displayName, + topic: self.room.summary?.topic, + avatarUrl: self.room.summary?.avatar, + joinRule: .public, + membership: self.room.summary?.membership ?? .unknown, + inviterId: nil, + inviter: nil, + membersCount: 0) + self.viewDelegate?.roomContextPreviewViewModel(self, didUpdateViewState: .loaded(parameters)) + + room.state { roomState in + let membersCount = roomState?.members.joinedMembers.count ?? 0 + + var inviteEvent: MXEvent? + roomState?.stateEvents.forEach({ event in + guard let membership = event.wireContent["membership"] as? String, membership == "invite", event.stateKey == self.room.mxSession.myUserId else { + return + } + + inviteEvent = event + }) + + let inviter: MXUser? + if let inviterId = inviteEvent?.sender { + inviter = self.room.mxSession.user(withUserId: inviterId) + } else { + inviter = nil + } + + let parameters = RoomContextPreviewLoadedParameters( + roomId: self.room.roomId, + roomType: self.room.summary?.roomType ?? .none, + displayName: self.room.displayName, + topic: roomState?.topic, + avatarUrl: roomState?.avatar, + joinRule: roomState?.joinRule, + membership: self.room.summary?.membership ?? .unknown, + inviterId: inviteEvent?.sender, + inviter: inviter, + membersCount: membersCount) + self.viewDelegate?.roomContextPreviewViewModel(self, didUpdateViewState: .loaded(parameters)) + } + } +} diff --git a/Riot/Modules/ContextMenu/ViewModels/RoomContextPreviewViewModels.swift b/Riot/Modules/ContextMenu/ViewModels/RoomContextPreviewViewModels.swift new file mode 100644 index 000000000..16867bd40 --- /dev/null +++ b/Riot/Modules/ContextMenu/ViewModels/RoomContextPreviewViewModels.swift @@ -0,0 +1,52 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// All the data potentially loaded by the `RoomContextPreviewViewModelProtocol` to the `RoomContextPreviewViewController` +struct RoomContextPreviewLoadedParameters { + let roomId: String + let roomType: MXRoomType + let displayName: String? + let topic: String? + let avatarUrl: String? + let joinRule: MXRoomJoinRule? + let membership: MXMembership + let inviterId: String? + let inviter: MXUser? + let membersCount: Int +} + +/// `RoomContextPreviewViewController` view state +enum RoomContextPreviewViewState { + case loaded(_ paremeters: RoomContextPreviewLoadedParameters) +} + +/// `RoomContextPreviewViewController` view action +enum RoomContextPreviewViewAction { + case loadData +} + +/// View delegate for `RoomContextPreviewViewModelProtocol` +protocol RoomContextPreviewViewModelViewDelegate: AnyObject { + func roomContextPreviewViewModel(_ viewModel: RoomContextPreviewViewModelProtocol, didUpdateViewState viewSate: RoomContextPreviewViewState) +} + +/// Classes compliant with `RoomContextPreviewViewModelProtocol` are meant to provide the data to the `RoomContextPreviewViewController` +protocol RoomContextPreviewViewModelProtocol { + var viewDelegate: RoomContextPreviewViewModelViewDelegate? { get set } + func process(viewAction: RoomContextPreviewViewAction) +} diff --git a/Riot/Modules/ContextMenu/ViewModels/SpaceChildContextPreviewViewModel.swift b/Riot/Modules/ContextMenu/ViewModels/SpaceChildContextPreviewViewModel.swift new file mode 100644 index 000000000..aaee23760 --- /dev/null +++ b/Riot/Modules/ContextMenu/ViewModels/SpaceChildContextPreviewViewModel.swift @@ -0,0 +1,58 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `SpaceChildContextPreviewViewModel` provides the data to the `RoomContextPreviewViewController` from an instance of `MXSpaceChildInfo` +class SpaceChildContextPreviewViewModel: RoomContextPreviewViewModelProtocol { + + // MARK: - Properties + + private let childInfo: MXSpaceChildInfo + weak var viewDelegate: RoomContextPreviewViewModelViewDelegate? + + // MARK: - Setup + + init(childInfo: MXSpaceChildInfo) { + self.childInfo = childInfo + } + + // MARK: - RoomContextPreviewViewModelProtocol + + func process(viewAction: RoomContextPreviewViewAction) { + switch viewAction { + case .loadData: + self.loadData() + } + } + + // MARK: - Private + + private func loadData() { + let parameters = RoomContextPreviewLoadedParameters( + roomId: childInfo.childRoomId, + roomType: childInfo.roomType, + displayName: childInfo.displayName, + topic: childInfo.topic, + avatarUrl: childInfo.avatarUrl, + joinRule: .none, + membership: .unknown, + inviterId: nil, + inviter: nil, + membersCount: childInfo.activeMemberCount) + self.viewDelegate?.roomContextPreviewViewModel(self, didUpdateViewState: .loaded(parameters)) + } +} diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index 876d80cab..020dc624f 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -21,7 +21,7 @@ #import "GeneratedInterface-Swift.h" -@interface DirectoryViewController () +@interface DirectoryViewController () { PublicRoomsDirectoryDataSource *dataSource; @@ -37,6 +37,10 @@ @property (nonatomic) AnalyticsScreenTracker *screenTracker; +@property (nonatomic, strong) RoomNotificationSettingsCoordinatorBridgePresenter *roomNotificationSettingsCoordinatorBridgePresenter; + +@property (nonatomic, strong) PublicRoomContextMenuProvider *contextMenuProvider; + @end @implementation DirectoryViewController @@ -45,6 +49,9 @@ { [super finalizeInit]; + self.contextMenuProvider = [PublicRoomContextMenuProvider new]; + self.contextMenuProvider.serviceDelegate = self; + // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; @@ -191,36 +198,7 @@ { MXPublicRoom *publicRoom = [dataSource roomAtIndexPath:indexPath]; - // Check whether the user has already joined the selected public room - if ([dataSource.mxSession isJoinedOnRoom:publicRoom.roomId]) - { - // Open the public room. - [self showRoomWithId:publicRoom.roomId inMatrixSession:dataSource.mxSession]; - } - else - { - // Preview the public room - if (publicRoom.worldReadable) - { - RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:dataSource.mxSession]; - - [self startActivityIndicator]; - - // Try to get more information about the room before opening its preview - [roomPreviewData peekInRoom:^(BOOL succeeded) { - - [self stopActivityIndicator]; - - [self showRoomPreviewWithData:roomPreviewData]; - }]; - } - else - { - RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:dataSource.mxSession]; - [self showRoomPreviewWithData:roomPreviewData]; - } - - } + [self showRoomWithPublicRoom:publicRoom]; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView @@ -360,4 +338,120 @@ } } +- (void)showRoomWithPublicRoom:(MXPublicRoom*)publicRoom +{ + // Check whether the user has already joined the selected public room + if ([dataSource.mxSession isJoinedOnRoom:publicRoom.roomId]) + { + // Open the public room. + [self showRoomWithId:publicRoom.roomId inMatrixSession:dataSource.mxSession]; + } + else + { + // Preview the public room + if (publicRoom.worldReadable) + { + RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:dataSource.mxSession]; + + [self startActivityIndicator]; + + // Try to get more information about the room before opening its preview + [roomPreviewData peekInRoom:^(BOOL succeeded) { + + [self stopActivityIndicator]; + + [self showRoomPreviewWithData:roomPreviewData]; + }]; + } + else + { + RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:dataSource.mxSession]; + [self showRoomPreviewWithData:roomPreviewData]; + } + + } +} + +- (void)changeRoomNotificationSettingsForRoomWithId:(NSString*)roomId +{ + MXRoom *room = [dataSource.mxSession roomWithRoomId:roomId]; + if (room) + { + // navigate + self.roomNotificationSettingsCoordinatorBridgePresenter = [[RoomNotificationSettingsCoordinatorBridgePresenter alloc] initWithRoom:room]; + self.roomNotificationSettingsCoordinatorBridgePresenter.delegate = self; + [self.roomNotificationSettingsCoordinatorBridgePresenter presentFrom:self animated:YES]; + } +} + +#pragma mark - Context Menu + +- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) +{ + MXPublicRoom *publicRoom = [dataSource roomAtIndexPath:indexPath]; + UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + + if (!publicRoom || !cell) + { + return nil; + } + + return [self.contextMenuProvider contextMenuConfigurationWith:publicRoom from:cell session:dataSource.mxSession]; +} + +- (void)tableView:(UITableView *)tableView willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id)animator API_AVAILABLE(ios(13.0)) +{ + MXPublicRoom *publicRoom = [self.contextMenuProvider publicRoomFrom:configuration.identifier]; + if (!publicRoom) + { + return; + } + + [animator addCompletion:^{ + [self showRoomWithPublicRoom:publicRoom]; + }]; +} + +#pragma mark - RoomContextActionServiceDelegate + +- (void)roomContextActionServiceDidJoinRoom:(id)service +{ + // Nothing to do here +} + +- (void)roomContextActionServiceDidLeaveRoom:(id)service +{ + // Nothing to do here +} + +- (void)roomContextActionService:(id)service presentAlert:(UIAlertController *)alertController +{ + [self presentViewController:alertController animated:YES completion:nil]; +} + +- (void)roomContextActionService:(id)service updateActivityIndicator:(BOOL)isActive +{ + if (isActive) + { + [self startActivityIndicator]; + } + else + { + [self stopActivityIndicator]; + } +} + +- (void)roomContextActionService:(id)service showRoomNotificationSettingsForRoomWithId:(NSString *)roomId +{ + [self changeRoomNotificationSettingsForRoomWithId:roomId]; +} + +#pragma mark - RoomNotificationSettingsCoordinatorBridgePresenterDelegate + +-(void)roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete:(RoomNotificationSettingsCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + self.roomNotificationSettingsCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index bf4c0d3af..419cc50e1 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -622,111 +622,6 @@ [self.recentsSearchBar resignFirstResponder]; } -- (UIContextMenuConfiguration *)collectionView:(UICollectionView *)collectionView contextMenuConfigurationForItemAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) -{ - UIView *cell = [collectionView cellForItemAtIndexPath:indexPath]; - MXRoom *room = [self.dataSource getRoomAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:collectionView.tag]]; - NSString *roomId = room.roomId; - - MXWeakify(self); - MXWeakify(room); - - return [UIContextMenuConfiguration configurationWithIdentifier:roomId previewProvider:^UIViewController * _Nullable { - // Add a preview using the cell's data to prevent the avatar and displayname from changing with a room list update. - return [[ContextMenuSnapshotPreviewViewController alloc] initWithView:cell]; - - } actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) { - MXStrongifyAndReturnValueIfNil(room, nil); - - BOOL isDirect = room.isDirect; - UIAction *directChatAction = [UIAction actionWithTitle:isDirect ? VectorL10n.homeContextMenuMakeRoom : VectorL10n.homeContextMenuMakeDm - image:[UIImage systemImageNamed:isDirect ? @"person.crop.circle.badge.xmark" : @"person.circle"] - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - MXStrongifyAndReturnIfNil(self); - [self updateRoomWithId:roomId asDirect:!isDirect]; - }]; - - BOOL isMuted = room.isMute || room.isMentionsOnly; - UIImage *notificationsImage; - NSString *notificationsTitle; - if ([BuildSettings showNotificationsV2]) - { - notificationsTitle = VectorL10n.homeContextMenuNotifications; - notificationsImage = [UIImage systemImageNamed:@"bell"]; - } - else - { - notificationsTitle = isMuted ? VectorL10n.homeContextMenuUnmute : VectorL10n.homeContextMenuMute; - notificationsImage = [UIImage systemImageNamed:isMuted ? @"bell.slash": @"bell"]; - } - - UIAction *notificationsAction = [UIAction actionWithTitle:notificationsTitle - image:notificationsImage - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - MXStrongifyAndReturnIfNil(self); - [self updateRoomWithId:roomId asMuted:!isMuted]; - }]; - - - // Get the room tag (use only the first one). - MXRoomTag* currentTag = nil; - if (room.accountData.tags) - { - NSArray* tags = room.accountData.tags.allValues; - if (tags.count) - { - currentTag = tags[0]; - } - } - - BOOL isFavourite = (currentTag && [kMXRoomTagFavourite isEqualToString:currentTag.name]); - UIAction *favouriteAction = [UIAction actionWithTitle:isFavourite ? VectorL10n.homeContextMenuUnfavourite : VectorL10n.homeContextMenuFavourite - image:[UIImage systemImageNamed:isFavourite ? @"star.slash" : @"star"] - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - MXStrongifyAndReturnIfNil(self); - [self updateRoomWithId:roomId asFavourite:!isFavourite]; - }]; - - BOOL isLowPriority = (currentTag && [kMXRoomTagLowPriority isEqualToString:currentTag.name]); - UIAction *lowPriorityAction = [UIAction actionWithTitle:isLowPriority ? VectorL10n.homeContextMenuNormalPriority : VectorL10n.homeContextMenuLowPriority - image:[UIImage systemImageNamed:isLowPriority ? @"arrow.up" : @"arrow.down"] - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - MXStrongifyAndReturnIfNil(self); - [self updateRoomWithId:roomId asLowPriority:!isLowPriority]; - }]; - - UIImage *leaveImage; - if (@available(iOS 14.0, *)) - { - leaveImage = [UIImage systemImageNamed:@"rectangle.righthalf.inset.fill.arrow.right"]; - } - else - { - leaveImage = [UIImage systemImageNamed:@"rectangle.xmark"]; - } - UIAction *leaveAction = [UIAction actionWithTitle:VectorL10n.homeContextMenuLeave - image:leaveImage - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - MXStrongifyAndReturnIfNil(self); - [self leaveRoomWithId:roomId]; - }]; - leaveAction.attributes = UIMenuElementAttributesDestructive; - - return [UIMenu menuWithTitle:@"" children:@[ - directChatAction, - notificationsAction, - favouriteAction, - lowPriorityAction, - leaveAction - ]]; - }]; -} - #pragma mark - UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath @@ -863,50 +758,6 @@ [self leaveEditedRoom]; } -// MARK: - Context Menu Actions - -- (void)updateRoomWithId:(NSString *)roomId asDirect:(BOOL)direct -{ - editedRoomId = roomId; - [self makeDirectEditedRoom:direct]; - editedRoomId = nil; -} - -- (void)updateRoomWithId:(NSString *)roomId asMuted:(BOOL)muted -{ - editedRoomId = roomId; - if ([BuildSettings showNotificationsV2]) - { - [self changeEditedRoomNotificationSettings]; - } - else - { - [self muteEditedRoomNotifications:muted]; - } - editedRoomId = nil; -} - -- (void)updateRoomWithId:(NSString *)roomId asFavourite:(BOOL)favourite -{ - editedRoomId = roomId; - [self updateEditedRoomTag:favourite ? kMXRoomTagFavourite : nil]; - editedRoomId = nil; -} - -- (void)updateRoomWithId:(NSString *)roomId asLowPriority:(BOOL)lowPriority -{ - editedRoomId = roomId; - [self updateEditedRoomTag:lowPriority ? kMXRoomTagLowPriority : nil]; - editedRoomId = nil; -} - -- (void)leaveRoomWithId:(NSString *)roomId -{ - editedRoomId = roomId; - [self leaveEditedRoom]; - editedRoomId = nil; -} - #pragma mark - SecureBackupSetupCoordinatorBridgePresenterDelegate - (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidComplete:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter @@ -1034,4 +885,38 @@ }]; } +#pragma mark - Context Menu + +- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) +{ + return nil; +} + +- (UIContextMenuConfiguration *)collectionView:(UICollectionView *)collectionView contextMenuConfigurationForItemAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) +{ + id cellData = [recentsDataSource cellDataAtIndexPath:[NSIndexPath indexPathForRow:indexPath.item inSection:collectionView.tag]]; + UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; + + if (!cellData || !cell) + { + return nil; + } + + return [self.contextMenuProvider contextMenuConfigurationWith:cellData from:cell session:self.dataSource.mxSession]; +} + +- (void)collectionView:(UICollectionView *)collectionView willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id)animator API_AVAILABLE(ios(13.0)) +{ + NSString *roomId = [self.contextMenuProvider roomIdFrom:configuration.identifier]; + + if (!roomId) + { + return; + } + + [animator addCompletion:^{ + [self showRoomWithRoomId:roomId inMatrixSession:self.mainSession]; + }]; +} + @end diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index c3d8dd3cd..e04c183ad 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -90,6 +90,8 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; */ @property (nonatomic, nullable) NSString *parentSpaceId; +@property (nonatomic, getter=isContextPreview) BOOL contextPeview; + /** Display the preview of a room that is unknown for the user. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 136169871..eb3da6e67 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1593,6 +1593,11 @@ static CGSize kThreadListBarButtonItemImageSize; - (BOOL)isRoomPreview { + if (self.isContextPreview) + { + return YES; + } + // Check first whether some preview data are defined. if (roomPreviewData) { diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift index b19977e00..1fe801a1d 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift @@ -287,7 +287,8 @@ extension SpaceExploreRoomViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let viewData = self.itemDataList[indexPath.row] return UIContextMenuConfiguration(identifier: nil) { - return SpaceRoomPreviewViewController.instantiate(with: viewData.childInfo, avatarViewData: viewData.avatarViewData) + let viewModel = SpaceChildContextPreviewViewModel(childInfo: viewData.childInfo) + return RoomContextPreviewViewController.instantiate(with: viewModel, mediaManager: self.viewModel.space?.room?.mxSession.mediaManager) } actionProvider: { suggestedActions in return self.viewModel.contextMenu(for: self.itemDataList[indexPath.row]) } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceRoomPreviewViewController.storyboard b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceRoomPreviewViewController.storyboard deleted file mode 100644 index d1e93d6f6..000000000 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceRoomPreviewViewController.storyboard +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index e629216ae..1de9b0dc7 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -51,6 +51,7 @@ #import "PlainRoomTimelineCellProvider.h" #import "BubbleRoomTimelineCellProvider.h" #import "RoomSelectedStickerBubbleCell.h" +#import "MXRoom+Riot.h" // MatrixKit common imports, shared with all targets #import "MatrixKit-Bridging-Header.h" diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift index 8e9687c6a..03b868a83 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift @@ -251,6 +251,25 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy } } +extension MXRoom { + public var isMuted: Bool { + // Check whether an override rule has been defined with the roomm id as rule id. + // This kind of rule is created to mute the room + guard let rule = self.overridePushRule, + rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify), + rule.conditionIsEnabled(kind: .eventMatch, for: roomId) else { + return false + } + return rule.enabled + } + + public var isMentionsOnly: Bool { + // Check push rules at room level + guard let rule = roomPushRule else { return false } + return rule.enabled && rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) + } +} + // We could move these to their own file and make available in global namespace or move to sdk but they are only used here at the moment fileprivate extension MXRoom { @@ -288,23 +307,6 @@ fileprivate extension MXRoom { return .all } - var isMuted: Bool { - // Check whether an override rule has been defined with the roomm id as rule id. - // This kind of rule is created to mute the room - guard let rule = self.overridePushRule, - rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify), - rule.conditionIsEnabled(kind: .eventMatch, for: roomId) else { - return false - } - return rule.enabled - } - - var isMentionsOnly: Bool { - // Check push rules at room level - guard let rule = roomPushRule else { return false } - return rule.enabled && rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) - } - } fileprivate extension MXPushRule { diff --git a/changelog.d/5953.feature b/changelog.d/5953.feature new file mode 100644 index 000000000..54f920031 --- /dev/null +++ b/changelog.d/5953.feature @@ -0,0 +1 @@ +Addded support for Apple context menus in matrix items list screens From 571caa1ae9be00fa8de4f98e5c21932cab849b28 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 5 Apr 2022 01:12:00 +0300 Subject: [PATCH 120/135] Use final help link --- Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift b/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift index 3a177fb63..bf69f1173 100644 --- a/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift +++ b/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift @@ -21,7 +21,7 @@ class ThreadsBetaViewController: UIViewController { // MARK: Constants private enum Constants { - static let learnMoreLink = "https://element.io" + static let learnMoreLink = "https://element.io/help#threads" } // MARK: Outlets From 5e928b81be3358d6f976104eb6844bb1de13f36f Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 5 Apr 2022 01:59:11 +0300 Subject: [PATCH 121/135] Introduce ThreadsBetaCoordinator & open thread view directly after enabling threads --- Riot/Modules/Room/RoomViewController.m | 68 +++++++------- .../Threads/Beta/ThreadsBetaCoordinator.swift | 64 +++++++++++++ ...hreadsBetaCoordinatorBridgePresenter.swift | 89 +++++++++++++++++++ .../Beta/ThreadsBetaCoordinatorProtocol.swift | 29 ++++++ 4 files changed, 216 insertions(+), 34 deletions(-) create mode 100644 Riot/Modules/Threads/Beta/ThreadsBetaCoordinator.swift create mode 100644 Riot/Modules/Threads/Beta/ThreadsBetaCoordinatorBridgePresenter.swift create mode 100644 Riot/Modules/Threads/Beta/ThreadsBetaCoordinatorProtocol.swift diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index eb6dba222..9d930b143 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -97,7 +97,7 @@ static CGSize kThreadListBarButtonItemImageSize; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate> { // The preview header @@ -210,6 +210,7 @@ static CGSize kThreadListBarButtonItemImageSize; @property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController; @property (nonatomic, strong) RoomParticipantsInviteCoordinatorBridgePresenter *participantsInvitePresenter; @property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsBridgePresenter; +@property (nonatomic, strong) ThreadsBetaCoordinatorBridgePresenter *threadsBetaBridgePresenter; @property (nonatomic, strong) SlidingModalPresenter *threadsNoticeModalPresenter; @property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded; @property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden; @@ -6768,7 +6769,7 @@ static CGSize kThreadListBarButtonItemImageSize; } else { - [self showThreadsBeta]; + [self showThreadsBetaForEvent:event]; } }; @@ -6820,43 +6821,18 @@ static CGSize kThreadListBarButtonItemImageSize; completion:nil]; } -- (void)showThreadsBeta +- (void)showThreadsBetaForEvent:(MXEvent *)event { - if (!self.threadsNoticeModalPresenter) + if (self.threadsBetaBridgePresenter) { - self.threadsNoticeModalPresenter = [SlidingModalPresenter new]; + [self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:nil]; + self.threadsBetaBridgePresenter = nil; } - [self.threadsNoticeModalPresenter dismissWithAnimated:NO completion:nil]; + self.threadsBetaBridgePresenter = [[ThreadsBetaCoordinatorBridgePresenter alloc] initWithThreadId:event.eventId]; + self.threadsBetaBridgePresenter.delegate = self; - ThreadsBetaViewController *threadsBetaVC = [ThreadsBetaViewController instantiate]; - - MXWeakify(self); - - threadsBetaVC.didTapEnableButton = ^{ - MXStrongifyAndReturnIfNil(self); - - [self.threadsNoticeModalPresenter dismissWithAnimated:YES completion:^{ - RiotSettings.shared.enableThreads = YES; - MXSDKOptions.sharedInstance.enableThreads = YES; - [self cancelEventSelection]; - [self.roomDataSource reload]; - }]; - }; - - threadsBetaVC.didTapCancelButton = ^{ - MXStrongifyAndReturnIfNil(self); - - [self.threadsNoticeModalPresenter dismissWithAnimated:YES completion:^{ - [self cancelEventSelection]; - }]; - }; - - [self.threadsNoticeModalPresenter present:threadsBetaVC - from:self.presentedViewController?:self - animated:YES - options:SlidingModalPresenter.SpanningOption - completion:nil]; + [self.threadsBetaBridgePresenter presentFrom:self.presentedViewController?:self animated:YES]; } - (void)openThreadWithId:(NSString *)threadId @@ -7457,6 +7433,30 @@ static CGSize kThreadListBarButtonItemImageSize; self.threadsBridgePresenter = nil; } +#pragma mark - ThreadsBetaCoordinatorBridgePresenterDelegate + +- (void)threadsBetaCoordinatorBridgePresenterDelegateDidTapEnable:(ThreadsBetaCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + MXWeakify(self); + [self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:^{ + MXStrongifyAndReturnIfNil(self); + RiotSettings.shared.enableThreads = YES; + MXSDKOptions.sharedInstance.enableThreads = YES; + [self cancelEventSelection]; + [self.roomDataSource reload]; + [self openThreadWithId:coordinatorBridgePresenter.threadId]; + }]; +} + +- (void)threadsBetaCoordinatorBridgePresenterDelegateDidTapCancel:(ThreadsBetaCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + MXWeakify(self); + [self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:^{ + MXStrongifyAndReturnIfNil(self); + [self cancelEventSelection]; + }]; +} + #pragma mark - MXThreadingServiceDelegate - (void)threadingServiceDidUpdateThreads:(MXThreadingService *)service diff --git a/Riot/Modules/Threads/Beta/ThreadsBetaCoordinator.swift b/Riot/Modules/Threads/Beta/ThreadsBetaCoordinator.swift new file mode 100644 index 000000000..3a3c213d1 --- /dev/null +++ b/Riot/Modules/Threads/Beta/ThreadsBetaCoordinator.swift @@ -0,0 +1,64 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Threads ThreadsBeta +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +@objcMembers +final class ThreadsBetaCoordinator: NSObject, ThreadsBetaCoordinatorProtocol { + + // MARK: - Properties + + // MARK: Private + + private let threadId: String + private lazy var viewController: ThreadsBetaViewController = { + let result = ThreadsBetaViewController.instantiate() + result.didTapEnableButton = { [weak self] in + guard let self = self else { return } + self.delegate?.threadsBetaCoordinatorDidTapEnable(self) + } + result.didTapCancelButton = { [weak self] in + guard let self = self else { return } + self.delegate?.threadsBetaCoordinatorDidTapCancel(self) + } + return result + }() + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: ThreadsBetaCoordinatorDelegate? + + // MARK: - Setup + + init(threadId: String) { + self.threadId = threadId + } + + // MARK: - Public + + func start() { + // no-op. this is a static screen + } + + func toPresentable() -> UIViewController { + return viewController + } +} diff --git a/Riot/Modules/Threads/Beta/ThreadsBetaCoordinatorBridgePresenter.swift b/Riot/Modules/Threads/Beta/ThreadsBetaCoordinatorBridgePresenter.swift new file mode 100644 index 000000000..769f7eff6 --- /dev/null +++ b/Riot/Modules/Threads/Beta/ThreadsBetaCoordinatorBridgePresenter.swift @@ -0,0 +1,89 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Threads ThreadsBeta +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import MatrixSDK + +@objc protocol ThreadsBetaCoordinatorBridgePresenterDelegate { + func threadsBetaCoordinatorBridgePresenterDelegateDidTapEnable(_ coordinatorBridgePresenter: ThreadsBetaCoordinatorBridgePresenter) + func threadsBetaCoordinatorBridgePresenterDelegateDidTapCancel(_ coordinatorBridgePresenter: ThreadsBetaCoordinatorBridgePresenter) +} + +/// ThreadsBetaCoordinatorBridgePresenter enables to start ThreadsBetaCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only** (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +@objcMembers +final class ThreadsBetaCoordinatorBridgePresenter: NSObject { + + // MARK: - Constants + + // MARK: - Properties + + // MARK: Private + + public let threadId: String + private let slidingModalPresenter = SlidingModalPresenter() + private var coordinator: ThreadsBetaCoordinator? + + // MARK: Public + + weak var delegate: ThreadsBetaCoordinatorBridgePresenterDelegate? + + // MARK: - Setup + + init(threadId: String) { + self.threadId = threadId + super.init() + } + + // MARK: - Public + + func present(from viewController: UIViewController, animated: Bool) { + + let threadsBetaCoordinator = ThreadsBetaCoordinator(threadId: threadId) + threadsBetaCoordinator.delegate = self + guard let presentable = threadsBetaCoordinator.toPresentable() as? SlidingModalPresentable.ViewControllerType else { + MXLog.error("[ThreadsBetaCoordinatorBridgePresenter] Presentable is not 'SlidingModalPresentable'") + return + } + slidingModalPresenter.present(presentable, + from: viewController, + animated: animated, + options: .spanning, + completion: nil) + threadsBetaCoordinator.start() + + self.coordinator = threadsBetaCoordinator + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + slidingModalPresenter.dismiss(animated: animated, completion: completion) + } +} + +// MARK: - ThreadsBetaCoordinatorDelegate +extension ThreadsBetaCoordinatorBridgePresenter: ThreadsBetaCoordinatorDelegate { + + func threadsBetaCoordinatorDidTapEnable(_ coordinator: ThreadsBetaCoordinatorProtocol) { + self.delegate?.threadsBetaCoordinatorBridgePresenterDelegateDidTapEnable(self) + } + + func threadsBetaCoordinatorDidTapCancel(_ coordinator: ThreadsBetaCoordinatorProtocol) { + self.delegate?.threadsBetaCoordinatorBridgePresenterDelegateDidTapCancel(self) + } +} diff --git a/Riot/Modules/Threads/Beta/ThreadsBetaCoordinatorProtocol.swift b/Riot/Modules/Threads/Beta/ThreadsBetaCoordinatorProtocol.swift new file mode 100644 index 000000000..0382c9c76 --- /dev/null +++ b/Riot/Modules/Threads/Beta/ThreadsBetaCoordinatorProtocol.swift @@ -0,0 +1,29 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Threads ThreadsBeta +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol ThreadsBetaCoordinatorDelegate: AnyObject { + func threadsBetaCoordinatorDidTapEnable(_ coordinator: ThreadsBetaCoordinatorProtocol) + func threadsBetaCoordinatorDidTapCancel(_ coordinator: ThreadsBetaCoordinatorProtocol) +} + +/// `ThreadsBetaCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow. +protocol ThreadsBetaCoordinatorProtocol: Coordinator, Presentable { + var delegate: ThreadsBetaCoordinatorDelegate? { get } +} From 0f5f59b3adea59624fa65a9c6ffa6c73ffdcfdd5 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 5 Apr 2022 02:04:05 +0300 Subject: [PATCH 122/135] Set most of the parameters via storyboard --- .../Threads/Beta/ThreadsBetaViewController.storyboard | 2 +- Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Riot/Modules/Threads/Beta/ThreadsBetaViewController.storyboard b/Riot/Modules/Threads/Beta/ThreadsBetaViewController.storyboard index 3bf11bb2d..7a21c2b41 100644 --- a/Riot/Modules/Threads/Beta/ThreadsBetaViewController.storyboard +++ b/Riot/Modules/Threads/Beta/ThreadsBetaViewController.storyboard @@ -47,7 +47,7 @@ - + Keep discussions organised with threads. diff --git a/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift b/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift index bf69f1173..ee90da94f 100644 --- a/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift +++ b/Riot/Modules/Threads/Beta/ThreadsBetaViewController.swift @@ -30,14 +30,7 @@ class ThreadsBetaViewController: UIViewController { @IBOutlet private weak var separatorLineView: UIView! @IBOutlet private weak var informationTextView: UITextView! { didSet { - informationTextView.contentInset = .zero - informationTextView.textContainerInset = .zero informationTextView.textContainer.lineFragmentPadding = 0 - informationTextView.scrollsToTop = false - informationTextView.showsVerticalScrollIndicator = false - informationTextView.showsHorizontalScrollIndicator = false - informationTextView.isEditable = false - informationTextView.isScrollEnabled = false } } @IBOutlet private weak var enableButton: UIButton! From a8d63914093bd4e8f2ac8330a0e7b974f9401dda Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 5 Apr 2022 02:34:36 +0300 Subject: [PATCH 123/135] Do not scale room name in thread header --- .../Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib index 08667bfdc..c4af2bd92 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib @@ -1,9 +1,9 @@ - + - + @@ -58,7 +58,7 @@ - From 4562a56e45cff53380ef9de708860c1843d79b6b Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 5 Apr 2022 02:39:42 +0300 Subject: [PATCH 128/135] Add changelog --- changelog.d/5878.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5878.bugfix diff --git a/changelog.d/5878.bugfix b/changelog.d/5878.bugfix new file mode 100644 index 000000000..cbd1ee842 --- /dev/null +++ b/changelog.d/5878.bugfix @@ -0,0 +1 @@ +Threads: Tweaks for design review. From 29adae0f0dcb8259cd7b0f4d62beaa0beafcfb54 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 5 Apr 2022 11:35:57 +0300 Subject: [PATCH 129/135] Move threads enabling logic to the coordinator --- Riot/Modules/Room/RoomViewController.m | 2 -- Riot/Modules/Threads/Beta/ThreadsBetaCoordinator.swift | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 9d930b143..e4967648f 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -7440,8 +7440,6 @@ static CGSize kThreadListBarButtonItemImageSize; MXWeakify(self); [self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:^{ MXStrongifyAndReturnIfNil(self); - RiotSettings.shared.enableThreads = YES; - MXSDKOptions.sharedInstance.enableThreads = YES; [self cancelEventSelection]; [self.roomDataSource reload]; [self openThreadWithId:coordinatorBridgePresenter.threadId]; diff --git a/Riot/Modules/Threads/Beta/ThreadsBetaCoordinator.swift b/Riot/Modules/Threads/Beta/ThreadsBetaCoordinator.swift index 3a3c213d1..82369c268 100644 --- a/Riot/Modules/Threads/Beta/ThreadsBetaCoordinator.swift +++ b/Riot/Modules/Threads/Beta/ThreadsBetaCoordinator.swift @@ -30,6 +30,8 @@ final class ThreadsBetaCoordinator: NSObject, ThreadsBetaCoordinatorProtocol { let result = ThreadsBetaViewController.instantiate() result.didTapEnableButton = { [weak self] in guard let self = self else { return } + RiotSettings.shared.enableThreads = true + MXSDKOptions.sharedInstance().enableThreads = true self.delegate?.threadsBetaCoordinatorDidTapEnable(self) } result.didTapCancelButton = { [weak self] in From 81c5cf1c0393a7380bb0dd6ae543cdbcda6f36b3 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 5 Apr 2022 13:09:37 +0300 Subject: [PATCH 130/135] Move extension into Element --- Riot/Categories/NSAttributedString.swift | 27 +++++++++++++++++++ .../NSAttributedString+MatrixKit.swift | 7 ----- .../ThreadList/ThreadListViewModel.swift | 2 +- 3 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 Riot/Categories/NSAttributedString.swift diff --git a/Riot/Categories/NSAttributedString.swift b/Riot/Categories/NSAttributedString.swift new file mode 100644 index 000000000..65c2db8a7 --- /dev/null +++ b/Riot/Categories/NSAttributedString.swift @@ -0,0 +1,27 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +public extension NSAttributedString { + + /// Returns a new attributed string by removing all links from the receiver. + @objc var vc_byRemovingLinks: NSAttributedString { + let result = NSMutableAttributedString(attributedString: self) + result.removeAttribute(.link, range: NSRange(location: 0, length: length)) + return result + } +} diff --git a/Riot/Modules/MatrixKit/Categories/NSAttributedString+MatrixKit.swift b/Riot/Modules/MatrixKit/Categories/NSAttributedString+MatrixKit.swift index afee51868..c72e5c76c 100644 --- a/Riot/Modules/MatrixKit/Categories/NSAttributedString+MatrixKit.swift +++ b/Riot/Modules/MatrixKit/Categories/NSAttributedString+MatrixKit.swift @@ -29,11 +29,4 @@ public extension NSAttributedString { return unquotedSubstrings.joined(separator: " ") as NSString } - - /// Returns a new attributed string by removing all links from the receiver. - @objc var byRemovingLinks: NSAttributedString { - let result = NSMutableAttributedString(attributedString: self) - result.removeAttribute(.link, range: NSRange(location: 0, length: length)) - return result - } } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index 1f2dd01f5..7e197226a 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -209,7 +209,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { let formatterError = UnsafeMutablePointer.allocate(capacity: 1) return eventFormatter.attributedString(from: message.replyStrippedVersion, with: roomState, - error: formatterError).byRemovingLinks + error: formatterError).vc_byRemovingLinks } private func lastMessageTextAndTime(forThread thread: MXThreadProtocol) -> (NSAttributedString?, String?) { From fff61b12c1e43ae9e7b33d3f1fbac317b0cfb8fc Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 5 Apr 2022 14:04:48 +0300 Subject: [PATCH 131/135] Reload thread data source without notifying the screen --- Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h | 9 ++++++++- Riot/Modules/Room/DataSources/RoomDataSource.m | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 5d001ef08..146f5b23b 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -353,10 +353,17 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; - (void)limitMemoryUsage:(NSInteger)maxBubbleNb; /** - Force data reload. + Force data reload. Calls `reloadNotifying` with `YES`. */ - (void)reload; +/** + Force data reload. + + @param notify Flag to notify the delegate about the changes. + */ +- (void)reloadNotifying:(BOOL)notify; + /** Called when room property changed. Designed to be used by subclasses. */ diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 5e91b0d51..13f8fffc1 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -973,6 +973,13 @@ const CGFloat kTypingCellHeight = 24; // no need to reload when paginating back return; } + + BOOL notify = YES; + if (self.threadId) + { + // no need to notify the thread screen, it'll cause a flickering + notify = NO; + } NSUInteger count = 0; @synchronized (bubbles) { @@ -980,7 +987,7 @@ const CGFloat kTypingCellHeight = 24; } if (count > 0) { - [self reload]; + [self reloadNotifying:notify]; } } From fa11910940c105f44c903d9fca0acb214bae4646 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 5 Apr 2022 14:14:46 +0300 Subject: [PATCH 132/135] Add changelog --- changelog.d/5838.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5838.bugfix diff --git a/changelog.d/5838.bugfix b/changelog.d/5838.bugfix new file mode 100644 index 000000000..a35821948 --- /dev/null +++ b/changelog.d/5838.bugfix @@ -0,0 +1 @@ +RoomDataSource: Reload thread data source without notifying the screen for the first reply. From 904b2d656463cf411b009a4661827ff08a71cf22 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 5 Apr 2022 13:23:52 +0100 Subject: [PATCH 133/135] Disable RTL languages for now. --- Riot/target.yml | 2 ++ changelog.d/5935.i18n | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Riot/target.yml b/Riot/target.yml index 76014eb44..d0239e9ae 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -67,6 +67,8 @@ targets: - path: . excludes: - "Modules/Room/EmojiPicker/Data/EmojiMart/EmojiJSONStore.swift" + - "Assets/ar.lproj/**" # RTL is broken so languages are disabled for now + - "Assets/he.lproj/**" - path: ../RiotShareExtension/Shared - path: Modules/MatrixKit excludes: diff --git a/changelog.d/5935.i18n b/changelog.d/5935.i18n index 1d306244d..cbfd09559 100644 --- a/changelog.d/5935.i18n +++ b/changelog.d/5935.i18n @@ -1 +1 @@ -Translations: Enable all languages rather than waiting for an 80% translation. +Translations: Enable all languages rather than waiting for an 80% translation. RTL languages are still disabled due to layout and formatting bugs. From 233ce32d00729cd50bd93b30940bb9d695d45c1b Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 5 Apr 2022 14:58:15 +0100 Subject: [PATCH 134/135] changelog.d: Upgrade MatrixSDK version ([v0.23.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.2)). --- Podfile | 2 +- changelog.d/x-nolink-0.change | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/x-nolink-0.change diff --git a/Podfile b/Podfile index ec5c0420d..ec921a026 100644 --- a/Podfile +++ b/Podfile @@ -13,7 +13,7 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixSDKVersion = '= 0.23.1' +$matrixSDKVersion = '= 0.23.2' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change new file mode 100644 index 000000000..6d6216f94 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixSDK version ([v0.23.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.2)). \ No newline at end of file From 8ebbf57f672cdb9be11825b7cf6652cc832c820e Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 5 Apr 2022 14:58:15 +0100 Subject: [PATCH 135/135] version++ --- CHANGES.md | 56 +++++++++++++++++++++++++++++++++++ changelog.d/1575.bugfix | 1 - changelog.d/4842.bugfix | 1 - changelog.d/4858.bugfix | 1 - changelog.d/5058.bugfix | 1 - changelog.d/5134.bugfix | 1 - changelog.d/5170.bugfix | 1 - changelog.d/5488.change | 1 - changelog.d/5604.change | 1 - changelog.d/5606.change | 1 - changelog.d/5651.wip | 1 - changelog.d/5720.change | 1 - changelog.d/5770.feature | 1 - changelog.d/5771.change | 2 -- changelog.d/5772.change | 1 - changelog.d/5797.bugfix | 1 - changelog.d/5810.change | 1 - changelog.d/5837.change | 1 - changelog.d/5838.bugfix | 1 - changelog.d/5858.change | 1 - changelog.d/5870.bugfix | 1 - changelog.d/5875.bugfix | 1 - changelog.d/5878.bugfix | 1 - changelog.d/5883.bugfix | 2 -- changelog.d/5898.change | 1 - changelog.d/5906.bugfix | 1 - changelog.d/5911.bugfix | 1 - changelog.d/5915.bugfix | 1 - changelog.d/5935.i18n | 1 - changelog.d/5938.bugfix | 1 - changelog.d/5943.bugfix | 1 - changelog.d/5948.bugfix | 1 - changelog.d/5953.feature | 1 - changelog.d/pr-5910.api | 1 - changelog.d/pr-5926.bugfix | 1 - changelog.d/x-nolink-0.change | 1 - 36 files changed, 56 insertions(+), 37 deletions(-) delete mode 100644 changelog.d/1575.bugfix delete mode 100644 changelog.d/4842.bugfix delete mode 100644 changelog.d/4858.bugfix delete mode 100644 changelog.d/5058.bugfix delete mode 100644 changelog.d/5134.bugfix delete mode 100644 changelog.d/5170.bugfix delete mode 100644 changelog.d/5488.change delete mode 100644 changelog.d/5604.change delete mode 100644 changelog.d/5606.change delete mode 100644 changelog.d/5651.wip delete mode 100644 changelog.d/5720.change delete mode 100644 changelog.d/5770.feature delete mode 100644 changelog.d/5771.change delete mode 100644 changelog.d/5772.change delete mode 100644 changelog.d/5797.bugfix delete mode 100644 changelog.d/5810.change delete mode 100644 changelog.d/5837.change delete mode 100644 changelog.d/5838.bugfix delete mode 100644 changelog.d/5858.change delete mode 100644 changelog.d/5870.bugfix delete mode 100644 changelog.d/5875.bugfix delete mode 100644 changelog.d/5878.bugfix delete mode 100644 changelog.d/5883.bugfix delete mode 100644 changelog.d/5898.change delete mode 100644 changelog.d/5906.bugfix delete mode 100644 changelog.d/5911.bugfix delete mode 100644 changelog.d/5915.bugfix delete mode 100644 changelog.d/5935.i18n delete mode 100644 changelog.d/5938.bugfix delete mode 100644 changelog.d/5943.bugfix delete mode 100644 changelog.d/5948.bugfix delete mode 100644 changelog.d/5953.feature delete mode 100644 changelog.d/pr-5910.api delete mode 100644 changelog.d/pr-5926.bugfix delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index f3e7f3890..68cc98ab3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,59 @@ +## Changes in 1.8.11 (2022-04-05) + +✨ Features + +- RoomViewController: Display threads notice if not displayed before. ([#5770](https://github.com/vector-im/element-ios/issues/5770)) +- Addded support for Apple context menus in matrix items list screens ([#5953](https://github.com/vector-im/element-ios/issues/5953)) + +🙌 Improvements + +- Upgrade MatrixSDK version ([v0.23.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.2)). +- Threads: Strip `ìn reply to` from thread summaries and latest messages. ([#5488](https://github.com/vector-im/element-ios/issues/5488)) +- Room: New loading indicators when joining room ([#5604](https://github.com/vector-im/element-ios/issues/5604)) +- Room: New loading indicators when creating a room ([#5606](https://github.com/vector-im/element-ios/issues/5606)) +- Location Sharing: Update UI on location sharing view ([#5720](https://github.com/vector-im/element-ios/issues/5720)) +- Update suggested room preview to behave the same way in all cases ([#5771](https://github.com/vector-im/element-ios/issues/5771)) +- RoomViewController: Enable thread menu option and display opt-in screen if threads disabled. ([#5772](https://github.com/vector-im/element-ios/issues/5772)) +- Add "Invite people" to the space menu in the left panel and update menu order ([#5810](https://github.com/vector-im/element-ios/issues/5810)) +- Allow empty Jitsi default URL in BuildSettings ([#5837](https://github.com/vector-im/element-ios/issues/5837)) +- Location sharing: Add the ability for the user to share static location of a pin anywhere on the map ([#5858](https://github.com/vector-im/element-ios/issues/5858)) +- Restrict UI components on authentication screen to readable width ([#5898](https://github.com/vector-im/element-ios/issues/5898)) + +🐛 Bugfixes + +- Fixed the regular expression used for link detection in attributed strings. ([#5926](https://github.com/vector-im/element-ios/pull/5926)) +- Jitsi: fix app not leaving call when widget is removed ([#1575](https://github.com/vector-im/element-ios/issues/1575)) +- Space preview shows wrong number of members ([#4842](https://github.com/vector-im/element-ios/issues/4842)) +- Room: Enable joining a room via identifier from another home server ([#4858](https://github.com/vector-im/element-ios/issues/4858)) +- MXKRoomDataSource: Fix retain cycle ([#5058](https://github.com/vector-im/element-ios/issues/5058)) +- Sync Spaces order with web ([#5134](https://github.com/vector-im/element-ios/issues/5134)) +- Fix “It is not possible to join an empty room” on some suggested rooms. ([#5170](https://github.com/vector-im/element-ios/issues/5170)) +- Fixed "Add Space" error message ([#5797](https://github.com/vector-im/element-ios/issues/5797)) +- RoomDataSource: Reload thread data source without notifying the screen for the first reply. ([#5838](https://github.com/vector-im/element-ios/issues/5838)) +- VoiceMessagePlainCell: Fix cell height by adding missing thread summary displayable conformance. ([#5870](https://github.com/vector-im/element-ios/issues/5870)) +- Authentication: Ensure the login button is always visible ([#5875](https://github.com/vector-im/element-ios/issues/5875)) +- Threads: Tweaks for design review. ([#5878](https://github.com/vector-im/element-ios/issues/5878)) +- Search: prevent crash when searching for rooms ([#5883](https://github.com/vector-im/element-ios/issues/5883)) +- Room: Fix typing performance by avoiding expensive UI operations ([#5906](https://github.com/vector-im/element-ios/issues/5906)) +- The "Swipe to see all rooms" hint is sometimes presented at the wrong time ([#5911](https://github.com/vector-im/element-ios/issues/5911)) +- Push notifications: show space preview if user taps invite notification ([#5915](https://github.com/vector-im/element-ios/issues/5915)) +- Fix session handling of the call presenter. ([#5938](https://github.com/vector-im/element-ios/issues/5938)) +- m.room.join_rules not properly set for private access ([#5943](https://github.com/vector-im/element-ios/issues/5943)) +- Fix for app occasionally getting stuck during launch after Login/Register. ([#5948](https://github.com/vector-im/element-ios/issues/5948)) + +⚠️ API Changes + +- Remove unused Bindings in RoundedBorderTextField/Editor ([#5910](https://github.com/vector-im/element-ios/pull/5910)) + +🗣 Translations + +- Translations: Enable all languages rather than waiting for an 80% translation. RTL languages are still disabled due to layout and formatting bugs. ([#5935](https://github.com/vector-im/element-ios/issues/5935)) + +🚧 In development 🚧 + +- Onboarding: Add celebration screen after display name and avatar screens. ([#5651](https://github.com/vector-im/element-ios/issues/5651)) + + ## Changes in 1.8.10 (2022-03-31) 🐛 Bugfixes diff --git a/changelog.d/1575.bugfix b/changelog.d/1575.bugfix deleted file mode 100644 index 609b0150d..000000000 --- a/changelog.d/1575.bugfix +++ /dev/null @@ -1 +0,0 @@ -Jitsi: fix app not leaving call when widget is removed diff --git a/changelog.d/4842.bugfix b/changelog.d/4842.bugfix deleted file mode 100644 index 4e7ef2c03..000000000 --- a/changelog.d/4842.bugfix +++ /dev/null @@ -1 +0,0 @@ -Space preview shows wrong number of members diff --git a/changelog.d/4858.bugfix b/changelog.d/4858.bugfix deleted file mode 100644 index 2a7d342e8..000000000 --- a/changelog.d/4858.bugfix +++ /dev/null @@ -1 +0,0 @@ -Room: Enable joining a room via identifier from another home server diff --git a/changelog.d/5058.bugfix b/changelog.d/5058.bugfix deleted file mode 100644 index 8254bddd2..000000000 --- a/changelog.d/5058.bugfix +++ /dev/null @@ -1 +0,0 @@ -MXKRoomDataSource: Fix retain cycle diff --git a/changelog.d/5134.bugfix b/changelog.d/5134.bugfix deleted file mode 100644 index 74df83c35..000000000 --- a/changelog.d/5134.bugfix +++ /dev/null @@ -1 +0,0 @@ -Sync Spaces order with web \ No newline at end of file diff --git a/changelog.d/5170.bugfix b/changelog.d/5170.bugfix deleted file mode 100644 index 1ef0fceb3..000000000 --- a/changelog.d/5170.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix “It is not possible to join an empty room” on some suggested rooms. diff --git a/changelog.d/5488.change b/changelog.d/5488.change deleted file mode 100644 index e1b4dc675..000000000 --- a/changelog.d/5488.change +++ /dev/null @@ -1 +0,0 @@ -Threads: Strip `ìn reply to` from thread summaries and latest messages. diff --git a/changelog.d/5604.change b/changelog.d/5604.change deleted file mode 100644 index 23bb078ab..000000000 --- a/changelog.d/5604.change +++ /dev/null @@ -1 +0,0 @@ -Room: New loading indicators when joining room diff --git a/changelog.d/5606.change b/changelog.d/5606.change deleted file mode 100644 index 15c4fcd84..000000000 --- a/changelog.d/5606.change +++ /dev/null @@ -1 +0,0 @@ -Room: New loading indicators when creating a room diff --git a/changelog.d/5651.wip b/changelog.d/5651.wip deleted file mode 100644 index d5f2f46bb..000000000 --- a/changelog.d/5651.wip +++ /dev/null @@ -1 +0,0 @@ -Onboarding: Add celebration screen after display name and avatar screens. \ No newline at end of file diff --git a/changelog.d/5720.change b/changelog.d/5720.change deleted file mode 100644 index 0b0eeee50..000000000 --- a/changelog.d/5720.change +++ /dev/null @@ -1 +0,0 @@ -Location Sharing: Update UI on location sharing view diff --git a/changelog.d/5770.feature b/changelog.d/5770.feature deleted file mode 100644 index c46dd12ea..000000000 --- a/changelog.d/5770.feature +++ /dev/null @@ -1 +0,0 @@ -RoomViewController: Display threads notice if not displayed before. diff --git a/changelog.d/5771.change b/changelog.d/5771.change deleted file mode 100644 index cef75bd46..000000000 --- a/changelog.d/5771.change +++ /dev/null @@ -1,2 +0,0 @@ -Update suggested room preview to behave the same way in all cases - diff --git a/changelog.d/5772.change b/changelog.d/5772.change deleted file mode 100644 index 46210ef01..000000000 --- a/changelog.d/5772.change +++ /dev/null @@ -1 +0,0 @@ -RoomViewController: Enable thread menu option and display opt-in screen if threads disabled. diff --git a/changelog.d/5797.bugfix b/changelog.d/5797.bugfix deleted file mode 100644 index df2986a5d..000000000 --- a/changelog.d/5797.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed "Add Space" error message diff --git a/changelog.d/5810.change b/changelog.d/5810.change deleted file mode 100644 index 63585a270..000000000 --- a/changelog.d/5810.change +++ /dev/null @@ -1 +0,0 @@ -Add "Invite people" to the space menu in the left panel and update menu order \ No newline at end of file diff --git a/changelog.d/5837.change b/changelog.d/5837.change deleted file mode 100644 index 59904919e..000000000 --- a/changelog.d/5837.change +++ /dev/null @@ -1 +0,0 @@ -Allow empty Jitsi default URL in BuildSettings diff --git a/changelog.d/5838.bugfix b/changelog.d/5838.bugfix deleted file mode 100644 index a35821948..000000000 --- a/changelog.d/5838.bugfix +++ /dev/null @@ -1 +0,0 @@ -RoomDataSource: Reload thread data source without notifying the screen for the first reply. diff --git a/changelog.d/5858.change b/changelog.d/5858.change deleted file mode 100644 index 3d45b56d9..000000000 --- a/changelog.d/5858.change +++ /dev/null @@ -1 +0,0 @@ -Location sharing: Add the ability for the user to share static location of a pin anywhere on the map diff --git a/changelog.d/5870.bugfix b/changelog.d/5870.bugfix deleted file mode 100644 index 7708f0b2d..000000000 --- a/changelog.d/5870.bugfix +++ /dev/null @@ -1 +0,0 @@ -VoiceMessagePlainCell: Fix cell height by adding missing thread summary displayable conformance. diff --git a/changelog.d/5875.bugfix b/changelog.d/5875.bugfix deleted file mode 100644 index cbd4c0699..000000000 --- a/changelog.d/5875.bugfix +++ /dev/null @@ -1 +0,0 @@ -Authentication: Ensure the login button is always visible diff --git a/changelog.d/5878.bugfix b/changelog.d/5878.bugfix deleted file mode 100644 index cbd1ee842..000000000 --- a/changelog.d/5878.bugfix +++ /dev/null @@ -1 +0,0 @@ -Threads: Tweaks for design review. diff --git a/changelog.d/5883.bugfix b/changelog.d/5883.bugfix deleted file mode 100644 index 27400193c..000000000 --- a/changelog.d/5883.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Search: prevent crash when searching for rooms - diff --git a/changelog.d/5898.change b/changelog.d/5898.change deleted file mode 100644 index 9d83c6009..000000000 --- a/changelog.d/5898.change +++ /dev/null @@ -1 +0,0 @@ -Restrict UI components on authentication screen to readable width diff --git a/changelog.d/5906.bugfix b/changelog.d/5906.bugfix deleted file mode 100644 index 6015bc061..000000000 --- a/changelog.d/5906.bugfix +++ /dev/null @@ -1 +0,0 @@ -Room: Fix typing performance by avoiding expensive UI operations diff --git a/changelog.d/5911.bugfix b/changelog.d/5911.bugfix deleted file mode 100644 index edbb6c62b..000000000 --- a/changelog.d/5911.bugfix +++ /dev/null @@ -1 +0,0 @@ -The "Swipe to see all rooms" hint is sometimes presented at the wrong time diff --git a/changelog.d/5915.bugfix b/changelog.d/5915.bugfix deleted file mode 100644 index 88733d6ff..000000000 --- a/changelog.d/5915.bugfix +++ /dev/null @@ -1 +0,0 @@ -Push notifications: show space preview if user taps invite notification diff --git a/changelog.d/5935.i18n b/changelog.d/5935.i18n deleted file mode 100644 index cbfd09559..000000000 --- a/changelog.d/5935.i18n +++ /dev/null @@ -1 +0,0 @@ -Translations: Enable all languages rather than waiting for an 80% translation. RTL languages are still disabled due to layout and formatting bugs. diff --git a/changelog.d/5938.bugfix b/changelog.d/5938.bugfix deleted file mode 100644 index 929aff34c..000000000 --- a/changelog.d/5938.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix session handling of the call presenter. diff --git a/changelog.d/5943.bugfix b/changelog.d/5943.bugfix deleted file mode 100644 index ceae52674..000000000 --- a/changelog.d/5943.bugfix +++ /dev/null @@ -1 +0,0 @@ -m.room.join_rules not properly set for private access \ No newline at end of file diff --git a/changelog.d/5948.bugfix b/changelog.d/5948.bugfix deleted file mode 100644 index 09e891482..000000000 --- a/changelog.d/5948.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix for app occasionally getting stuck during launch after Login/Register. diff --git a/changelog.d/5953.feature b/changelog.d/5953.feature deleted file mode 100644 index 54f920031..000000000 --- a/changelog.d/5953.feature +++ /dev/null @@ -1 +0,0 @@ -Addded support for Apple context menus in matrix items list screens diff --git a/changelog.d/pr-5910.api b/changelog.d/pr-5910.api deleted file mode 100644 index ff9523166..000000000 --- a/changelog.d/pr-5910.api +++ /dev/null @@ -1 +0,0 @@ -Remove unused Bindings in RoundedBorderTextField/Editor \ No newline at end of file diff --git a/changelog.d/pr-5926.bugfix b/changelog.d/pr-5926.bugfix deleted file mode 100644 index f80a17cc2..000000000 --- a/changelog.d/pr-5926.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed the regular expression used for link detection in attributed strings. \ No newline at end of file diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index 6d6216f94..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixSDK version ([v0.23.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.2)). \ No newline at end of file