From 3db15471ee75455c8d1d64b93452dea065e725b2 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 8 Mar 2023 15:16:19 +0100 Subject: [PATCH 001/149] Enable user mentions in Rich Text Editor --- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Pills/PillAttachmentViewProvider.swift | 11 ++- Riot/Modules/Pills/PillViewFlusher.swift | 39 ++++++++ Riot/Modules/Pills/PillsFormatter.swift | 69 +++++++++++++- Riot/Modules/Room/RoomViewController.m | 5 + Riot/Modules/Room/RoomViewController.swift | 92 +++++++++++++------ .../Views/InputToolbar/RoomInputToolbarView.h | 5 + .../WysiwygInputToolbarView.swift | 37 +++++++- .../Room/Composer/Model/ComposerModels.swift | 11 +++ .../Modules/Room/Composer/View/Composer.swift | 7 ++ .../ViewModel/ComposerViewModel.swift | 2 + .../UserSuggestionCoordinator.swift | 5 + .../UserSuggestionCoordinatorBridge.swift | 4 + .../Service/UserSuggestionService.swift | 11 +++ .../UserSuggestionServiceProtocol.swift | 2 + project.yml | 2 +- 16 files changed, 267 insertions(+), 39 deletions(-) create mode 100644 Riot/Modules/Pills/PillViewFlusher.swift diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 44f6bd53c..751383169 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "addf90f3e2a6ab46bd2b2febe117d9cddb646e7d", - "version" : "1.1.1" + "revision" : "efa0b75e383a8f8a8269b871cbdee3d9a3a99060", + "version" : "1.2.0" } }, { diff --git a/Riot/Modules/Pills/PillAttachmentViewProvider.swift b/Riot/Modules/Pills/PillAttachmentViewProvider.swift index ba03ef61a..07806eddc 100644 --- a/Riot/Modules/Pills/PillAttachmentViewProvider.swift +++ b/Riot/Modules/Pills/PillAttachmentViewProvider.swift @@ -25,13 +25,18 @@ import UIKit avatarLeading: 2.0, avatarSideLength: 16.0, itemSpacing: 4) - private weak var messageTextView: MXKMessageTextView? + private weak var pillViewFlusher: PillViewFlusher? // MARK: - Override override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) { super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location) - self.messageTextView = parentView?.superview as? MXKMessageTextView + // Try to register a flusher for the pills. + if let pillViewFlusher = parentView?.superview as? PillViewFlusher { + self.pillViewFlusher = pillViewFlusher + } else { + MXLog.debug("[PillAttachmentViewProvider]: no handler found, pills will not be flushed properly") + } } override func loadView() { @@ -55,6 +60,6 @@ import UIKit mediaManager: mainSession?.mediaManager, andPillData: pillData) view = pillView - messageTextView?.registerPillView(pillView) + pillViewFlusher?.registerPillView(pillView) } } diff --git a/Riot/Modules/Pills/PillViewFlusher.swift b/Riot/Modules/Pills/PillViewFlusher.swift new file mode 100644 index 000000000..44a4d7cbf --- /dev/null +++ b/Riot/Modules/Pills/PillViewFlusher.swift @@ -0,0 +1,39 @@ +// +// Copyright 2023 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 +import WysiwygComposer + +/// Defines behaviour for an object that is able to manage views created +/// by a `NSTextAttachmentViewProvider`. This can be implemented +/// by an `UITextView` that would keep track of views in order to +/// (internally) clear them when required (e.g. when setting a new attributed text). +/// +/// Note: It is necessary to clear views manually due to a bug in iOS. See `MXKMessageTextView`. +@available(iOS 15.0, *) +protocol PillViewFlusher: AnyObject { + /// Register a pill view that has been added through `NSTextAttachmentViewProvider`. + /// Should be called within the `loadView` function in order to clear the pills properly on text updates. + /// + /// - Parameter pillView: View to register. + func registerPillView(_ pillView: UIView) +} + +@available(iOS 15.0, *) +extension MXKMessageTextView: PillViewFlusher { } + +@available(iOS 15.0, *) +extension WysiwygTextView: PillViewFlusher { } diff --git a/Riot/Modules/Pills/PillsFormatter.swift b/Riot/Modules/Pills/PillsFormatter.swift index a9df99fd4..334a39e73 100644 --- a/Riot/Modules/Pills/PillsFormatter.swift +++ b/Riot/Modules/Pills/PillsFormatter.swift @@ -74,6 +74,48 @@ class PillsFormatter: NSObject { return newAttr } + /// Insert text attachments for pills inside given attributed string containing markdown. + /// + /// - Parameters: + /// - markdownString: An attributed string with markdown formatting + /// - roomState: The current room state + /// - Returns: A new attributed string with pills. + static func insertPills(in markdownString: NSAttributedString, roomState: MXRoomState) -> NSAttributedString { + // Create a regexp that detects markdown links. + let pattern = "\\[([^\\]]+)\\]\\(([^\\)\"\\s]+)(?:\\s+\"(.*)\")?\\)" + guard let regExp = try? NSRegularExpression(pattern: pattern) else { return markdownString } + + let matches = regExp.matches(in: markdownString.string, + range: .init(location: 0, length: markdownString.length)) + + // If we have some matches, replace permalinks by a pill version. + let mutable = NSMutableAttributedString(attributedString: markdownString) + for match in matches.reversed() { + // Range at 2 is the URL, no need to care about the other parts because + // we are retrieving the most recent display name from the room state. + let urlRange = match.range(at: 2) + var url = markdownString.attributedSubstring(from: urlRange).string + + // Note: a valid markdown link can be written with + // enclosing <..>, remove them for userId detection. + if url.first == "<" && url.last == ">" { + url = String(url[url.index(after: url.startIndex)...url.index(url.endIndex, offsetBy: -2)]) + } + + // If we find a user matching the link, replace the + // entire range of the match with a mention pill. + if let userId = userIdFromPermalink(url), + let roomMember = roomMember(withUserId: userId, + roomState: roomState, + andLatestRoomState: nil) { + let attachmentString = mentionPill(withRoomMember: roomMember, isHighlighted: false, font: UIFont.systemFont(ofSize: 14)) + mutable.replaceCharacters(in: match.range, with: attachmentString) + } + } + + return mutable + } + /// Creates a string with all pills of given attributed string replaced by display names. /// /// - Parameters: @@ -160,7 +202,6 @@ class PillsFormatter: NSObject { } } } - } // MARK: - Private Methods @@ -175,4 +216,30 @@ extension PillsFormatter { } return string } + + /// Extract user id from given permalink + /// - Parameter permalink: the permalink + /// - Returns: userId, if any + static func userIdFromPermalink(_ permalink: String) -> String? { + let baseUrl: String + if let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl { + baseUrl = String(format: "%@/#/user/", clientBaseUrl) + } else { + baseUrl = String(format: "%@/#/", kMXMatrixDotToUrl) + } + return permalink.starts(with: baseUrl) ? String(permalink.dropFirst(baseUrl.count)) : nil + } + + /// Retrieve the latest available `MXRoomMember` from given data. + /// + /// - Parameters: + /// - userId: the id of the user + /// - roomState: room state for message + /// - latestRoomState: latest room state of the room containing this message + /// - Returns: the room member, if available + static func roomMember(withUserId userId: String, + roomState: MXRoomState, + andLatestRoomState latestRoomState: MXRoomState?) -> MXRoomMember? { + return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId) + } } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 2e641916a..17ab27fc4 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5149,6 +5149,11 @@ static CGSize kThreadListBarButtonItemImageSize; [self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage]; } +- (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern +{ + [self.userSuggestionCoordinator processSuggestionPattern:suggestionPattern]; +} + - (void)roomInputToolbarViewDidOpenActionMenu:(RoomInputToolbarView*)toolbarView { // Consider opening the action menu as beginning to type and share encryption keys if requested. diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index a177281f3..1da92a731 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -20,40 +20,42 @@ import WysiwygComposer extension RoomViewController { // MARK: - Override open override func mention(_ roomMember: MXRoomMember) { - guard let inputToolbar = inputToolbar else { - return - } - - let newAttributedString = NSMutableAttributedString(attributedString: inputToolbar.attributedTextMessage) - - if inputToolbar.attributedTextMessage.length > 0 { - if #available(iOS 15.0, *) { - newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember, - isHighlighted: false, - font: inputToolbar.textDefaultFont)) - } else { - newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId) - } - newAttributedString.appendString(" ") - } else if roomMember.userId == self.mainSession.myUser.userId { - newAttributedString.appendString("/me ") + if let wysiwygInputToolbar, wysiwygInputToolbar.textFormattingEnabled { + wysiwygInputToolbar.mention(roomMember) + wysiwygInputToolbar.becomeFirstResponder() } else { - if #available(iOS 15.0, *) { - newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember, - isHighlighted: false, - font: inputToolbar.textDefaultFont)) - } else { - newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId) - } - newAttributedString.appendString(": ") - } + guard let attributedText = inputToolbarView.attributedTextMessage else { return } + let newAttributedString = NSMutableAttributedString(attributedString: attributedText) - inputToolbar.attributedTextMessage = newAttributedString - inputToolbar.becomeFirstResponder() + if attributedText.length > 0 { + if #available(iOS 15.0, *) { + newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember, + isHighlighted: false, + font: UIFont.systemFont(ofSize: 14))) + } else { + newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId) + } + newAttributedString.appendString(" ") + } else if roomMember.userId == self.mainSession.myUser.userId { + newAttributedString.appendString("/me ") + } else { + if #available(iOS 15.0, *) { + newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember, + isHighlighted: false, + font: UIFont.systemFont(ofSize: 14))) + } else { + newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId) + } + newAttributedString.appendString(": ") + } + + inputToolbarView.attributedTextMessage = newAttributedString + inputToolbarView.becomeFirstResponder() + } } - /// Send the formatted text message and its raw counterpat to the room + /// Send the formatted text message and its raw counterpart to the room /// /// - Parameter rawTextMsg: the raw text message /// - Parameter htmlMsg: the html text message @@ -153,7 +155,22 @@ extension RoomViewController { @objc func togglePlainTextMode() { RiotSettings.shared.enableWysiwygTextFormatting.toggle() - wysiwygInputToolbar?.textFormattingEnabled.toggle() + + guard let wysiwygInputToolbar else { return } + + // Switching from plain -> RTE, replace Pills by valid markdown links for parsing. + if !wysiwygInputToolbar.textFormattingEnabled, #available(iOS 15.0, *), + let attributedText = wysiwygInputToolbar.attributedTextMessage { + wysiwygInputToolbar.attributedTextMessage = NSAttributedString(string: PillsFormatter.stringByReplacingPills(in: attributedText, mode: .markdown)) + } + + wysiwygInputToolbar.textFormattingEnabled.toggle() + + // Switching from RTE -> plain, replace markdown links with Pills. + if !wysiwygInputToolbar.textFormattingEnabled, #available(iOS 15.0, *), + let attributedText = wysiwygInputToolbar.attributedTextMessage { + wysiwygInputToolbar.attributedTextMessage = PillsFormatter.insertPills(in: attributedText, roomState: self.roomDataSource.roomState) + } } @objc func didChangeMaximisedState(_ isMaximised: Bool) { @@ -251,6 +268,21 @@ extension RoomViewController { composerLinkActionBridgePresenter = presenter presenter.present(from: self, animated: true) } + + @objc func didRequestAttachmentStringForLink(_ link: String, andDisplayName: String) -> NSAttributedString? { + guard #available(iOS 15.0, *), + let userId = PillsFormatter.userIdFromPermalink(link), + let roomState = self.roomDataSource.roomState, + let member = PillsFormatter.roomMember(withUserId: userId, + roomState: roomState, + andLatestRoomState: nil) else { + return nil + } + + return PillsFormatter.mentionPill(withRoomMember: member, + isHighlighted: false, + font: UIFont.systemFont(ofSize: 14)) + } @objc func showWaitingOtherParticipantHeader() { let controller = VectorHostingController(rootView: RoomWaitingForMembers()) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index af84b462d..ee1a032e0 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -21,6 +21,7 @@ @class RoomActionsBar; @class RoomInputToolbarView; @class LinkActionWrapper; +@class SuggestionPatternWrapper; /** Destination of the message in the composer @@ -80,6 +81,10 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode) - (void)didSendLinkAction: (LinkActionWrapper *)linkAction; +- (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern; + +- (nullable NSAttributedString *)didRequestAttachmentStringForLink: (NSString *)link andDisplayName: (NSString *)displayName; + @end /** diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index db6cc8193..aefef2459 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -43,8 +43,9 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var heightConstraint: NSLayoutConstraint! private var voiceMessageBottomConstraint: NSLayoutConstraint? private var hostingViewController: VectorHostingController! - private var wysiwygViewModel = WysiwygComposerViewModel( - parserStyle: WysiwygInputToolbarView.parserStyle + private lazy var wysiwygViewModel = WysiwygComposerViewModel( + parserStyle: WysiwygInputToolbarView.parserStyle, + permalinkReplacer: self ) /// Compute current HTML parser style for composer. private static var parserStyle: HTMLParserStyle { @@ -85,6 +86,19 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp override var isFocused: Bool { viewModel.isFocused } + + override var attributedTextMessage: NSAttributedString? { + // Note: this is only interactive in plain text mode. If RTE is enabled, + // APIs from the composer view model should be used. + get { + guard !self.textFormattingEnabled else { return nil } + return self.wysiwygViewModel.textView.attributedText + } + set { + guard !self.textFormattingEnabled else { return } + self.wysiwygViewModel.textView.attributedText = newValue + } + } var isMaximised: Bool { wysiwygViewModel.maximised @@ -217,6 +231,11 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp override func dismissKeyboard() { self.viewModel.dismissKeyboard() } + + @discardableResult + override func becomeFirstResponder() -> Bool { + self.wysiwygViewModel.textView.becomeFirstResponder() + } override func dismissValidationView(_ validationView: MXKImageView!) { super.dismissValidationView(validationView) @@ -239,6 +258,12 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } wysiwygViewModel.applyLinkOperation(linkOperation) } + + func mention(_ member: MXRoomMember) { + self.wysiwygViewModel.setMention(link: MXTools.permalinkToUser(withUserId: member.userId), + name: member.displayname, + key: .at) + } // MARK: - Private @@ -291,6 +316,8 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp setVoiceMessageToolbarIsHidden(!isEmpty) case let .linkTapped(linkAction): toolbarViewDelegate?.didSendLinkAction(LinkActionWrapper(linkAction)) + case let .suggestion(pattern): + toolbarViewDelegate?.didDetectTextPattern(SuggestionPatternWrapper(pattern)) } } @@ -412,6 +439,12 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } } +extension WysiwygInputToolbarView: PermalinkReplacer { + func replacementForLink(_ link: String, text: String) -> NSAttributedString? { + return toolbarViewDelegate?.didRequestAttachmentString(forLink: link, andDisplayName: text) + } +} + // MARK: - LegacySendModeAdapter fileprivate extension ComposerSendMode { diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index 98d7febf6..c96453667 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -229,12 +229,14 @@ enum ComposerViewAction: Equatable { case contentDidChange(isEmpty: Bool) case linkTapped(linkAction: LinkAction) case storeSelection(selection: NSRange) + case suggestion(pattern: SuggestionPattern?) } enum ComposerViewModelResult: Equatable { case cancel case contentDidChange(isEmpty: Bool) case linkTapped(LinkAction: LinkAction) + case suggestion(pattern: SuggestionPattern?) } final class LinkActionWrapper: NSObject { @@ -245,3 +247,12 @@ final class LinkActionWrapper: NSObject { super.init() } } + +final class SuggestionPatternWrapper: NSObject { + let suggestionPattern: SuggestionPattern? + + init(_ suggestionPattern: SuggestionPattern?) { + self.suggestionPattern = suggestionPattern + super.init() + } +} diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index 1413912c2..fb6ed8851 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -248,6 +248,9 @@ struct Composer: View { wysiwygViewModel.maximised = false } } + .onChange(of: wysiwygViewModel.suggestionPattern) { newValue in + sendMentionPattern(pattern: newValue) + } } private func storeCurrentSelection() { @@ -258,6 +261,10 @@ struct Composer: View { let linkAction = wysiwygViewModel.getLinkAction() viewModel.send(viewAction: .linkTapped(linkAction: linkAction)) } + + private func sendMentionPattern(pattern: SuggestionPattern?) { + viewModel.send(viewAction: .suggestion(pattern: pattern)) + } } private extension WysiwygComposerViewModel { diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift index a78018f60..6448b9de3 100644 --- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift @@ -90,6 +90,8 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol callback?(.linkTapped(LinkAction: linkAction)) case let .storeSelection(selection): selectionToRestore = selection + case let .suggestion(pattern: pattern): + callback?(.suggestion(pattern: pattern)) } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index c6d86a655..59b25ef86 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -18,6 +18,7 @@ import Combine import Foundation import SwiftUI import UIKit +import WysiwygComposer protocol UserSuggestionCoordinatorDelegate: AnyObject { func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) @@ -92,6 +93,10 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { userSuggestionService.processTextMessage(textMessage) } + func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) { + userSuggestionService.processSuggestionPattern(suggestionPattern) + } + // MARK: - Public func start() { } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift index c5b68eeee..4605547eb 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -44,6 +44,10 @@ final class UserSuggestionCoordinatorBridge: NSObject { func processTextMessage(_ textMessage: String) { userSuggestionCoordinator.processTextMessage(textMessage) } + + func processSuggestionPattern(_ suggestionPatternWrapper: SuggestionPatternWrapper) { + userSuggestionCoordinator.processSuggestionPattern(suggestionPatternWrapper.suggestionPattern) + } func toPresentable() -> UIViewController? { userSuggestionCoordinator.toPresentable() diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index bf8fa00a5..0f161ee38 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -16,6 +16,7 @@ import Combine import Foundation +import WysiwygComposer struct RoomMembersProviderMember { var userId: String @@ -85,6 +86,16 @@ class UserSuggestionService: UserSuggestionServiceProtocol { currentTextTriggerSubject.send(lastComponent) } + + func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) { + guard let suggestionPattern, suggestionPattern.key == .at else { + items.send([]) + currentTextTriggerSubject.send(nil) + return + } + + currentTextTriggerSubject.send("@" + suggestionPattern.text) + } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index 81edb0df9..43006dbed 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -16,6 +16,7 @@ import Combine import Foundation +import WysiwygComposer protocol UserSuggestionItemProtocol: Avatarable { var userId: String { get } @@ -29,6 +30,7 @@ protocol UserSuggestionServiceProtocol { var currentTextTrigger: String? { get } func processTextMessage(_ textMessage: String?) + func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) } // MARK: Avatarable diff --git a/project.yml b/project.yml index acc69ccdc..3df4c94ff 100644 --- a/project.yml +++ b/project.yml @@ -56,7 +56,7 @@ packages: branch: 0.0.1 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 1.1.1 + version: 1.2.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From 0379c977afbd5cab710599ceba4fa8fdb686b68c Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 8 Mar 2023 17:30:50 +0100 Subject: [PATCH 002/149] Use textDefaultFont in all variants of the `InputToolbarView` --- .../Views/RoomInputToolbar/MXKRoomInputToolbarView.h | 2 ++ .../Views/RoomInputToolbar/MXKRoomInputToolbarView.m | 4 ++++ Riot/Modules/Pills/PillsFormatter.swift | 5 +++-- Riot/Modules/Room/RoomViewController.swift | 10 ++++++---- .../WYSIWYGInputToolbar/WysiwygInputToolbarView.swift | 4 ++++ 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h index d7bf9d8fc..bc9b8e0b2 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h @@ -382,6 +382,8 @@ typedef enum : NSUInteger */ @property (nonatomic) NSAttributedString *attributedTextMessage; +@property (nonatomic, readonly, nonnull) UIFont *textDefaultFont; + - (void)dismissValidationView:(MXKImageView*)validationView; @end diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m index 9581df2a7..44199cc5b 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m @@ -358,6 +358,10 @@ self.textMessage = [NSString stringWithFormat:@"%@%@", self.textMessage, text]; } +- (UIFont *)textDefaultFont +{ + return [UIFont systemFontOfSize:15.f]; +} #pragma mark - MXKFileSizes diff --git a/Riot/Modules/Pills/PillsFormatter.swift b/Riot/Modules/Pills/PillsFormatter.swift index 334a39e73..4882ad51c 100644 --- a/Riot/Modules/Pills/PillsFormatter.swift +++ b/Riot/Modules/Pills/PillsFormatter.swift @@ -79,8 +79,9 @@ class PillsFormatter: NSObject { /// - Parameters: /// - markdownString: An attributed string with markdown formatting /// - roomState: The current room state + /// - font: The font to use for the pill text /// - Returns: A new attributed string with pills. - static func insertPills(in markdownString: NSAttributedString, roomState: MXRoomState) -> NSAttributedString { + static func insertPills(in markdownString: NSAttributedString, roomState: MXRoomState, font: UIFont) -> NSAttributedString { // Create a regexp that detects markdown links. let pattern = "\\[([^\\]]+)\\]\\(([^\\)\"\\s]+)(?:\\s+\"(.*)\")?\\)" guard let regExp = try? NSRegularExpression(pattern: pattern) else { return markdownString } @@ -108,7 +109,7 @@ class PillsFormatter: NSObject { let roomMember = roomMember(withUserId: userId, roomState: roomState, andLatestRoomState: nil) { - let attachmentString = mentionPill(withRoomMember: roomMember, isHighlighted: false, font: UIFont.systemFont(ofSize: 14)) + let attachmentString = mentionPill(withRoomMember: roomMember, isHighlighted: false, font: font) mutable.replaceCharacters(in: match.range, with: attachmentString) } } diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 1da92a731..b36447654 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -31,7 +31,7 @@ extension RoomViewController { if #available(iOS 15.0, *) { newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember, isHighlighted: false, - font: UIFont.systemFont(ofSize: 14))) + font: inputToolbarView.textDefaultFont)) } else { newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId) } @@ -42,7 +42,7 @@ extension RoomViewController { if #available(iOS 15.0, *) { newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember, isHighlighted: false, - font: UIFont.systemFont(ofSize: 14))) + font: inputToolbarView.textDefaultFont)) } else { newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId) } @@ -169,7 +169,9 @@ extension RoomViewController { // Switching from RTE -> plain, replace markdown links with Pills. if !wysiwygInputToolbar.textFormattingEnabled, #available(iOS 15.0, *), let attributedText = wysiwygInputToolbar.attributedTextMessage { - wysiwygInputToolbar.attributedTextMessage = PillsFormatter.insertPills(in: attributedText, roomState: self.roomDataSource.roomState) + wysiwygInputToolbar.attributedTextMessage = PillsFormatter.insertPills(in: attributedText, + roomState: self.roomDataSource.roomState, + font: self.inputToolbarView.textDefaultFont) } } @@ -281,7 +283,7 @@ extension RoomViewController { return PillsFormatter.mentionPill(withRoomMember: member, isHighlighted: false, - font: UIFont.systemFont(ofSize: 14)) + font: inputToolbarView.textDefaultFont) } @objc func showWaitingOtherParticipantHeader() { diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index aefef2459..1c5a4233a 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -99,6 +99,10 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp self.wysiwygViewModel.textView.attributedText = newValue } } + + override var textDefaultFont: UIFont { + return self.wysiwygViewModel.textView.font ?? UIFont.preferredFont(forTextStyle: .body) + } var isMaximised: Bool { wysiwygViewModel.maximised From d0bb3066caf43d50e3941242e2e0c365e9382266 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 9 Mar 2023 12:05:02 +0100 Subject: [PATCH 003/149] Bump to version 1.2.2 --- Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- project.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 751383169..daf871ac6 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "efa0b75e383a8f8a8269b871cbdee3d9a3a99060", - "version" : "1.2.0" + "revision" : "b81654b30f8b22b2d13f17e5e4c843e1fdc1db32", + "version" : "1.2.2" } }, { diff --git a/project.yml b/project.yml index 3df4c94ff..64ab238d2 100644 --- a/project.yml +++ b/project.yml @@ -56,7 +56,7 @@ packages: branch: 0.0.1 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 1.2.0 + version: 1.2.2 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From 6aab04a96c3aa117461c05fd6f8c65637cb3ce81 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 9 Mar 2023 12:11:58 +0100 Subject: [PATCH 004/149] Always use preferred font for body --- .../Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 1c5a4233a..a092a18ea 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -101,7 +101,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } override var textDefaultFont: UIFont { - return self.wysiwygViewModel.textView.font ?? UIFont.preferredFont(forTextStyle: .body) + return UIFont.preferredFont(forTextStyle: .body) } var isMaximised: Bool { From 43e46d1ff5f36ae356e93e568ec7dfaf3fb1d0f6 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 21 Mar 2023 10:26:37 +0100 Subject: [PATCH 005/149] Update composer library to 1.3.0 and apply changes --- .../xcshareddata/swiftpm/Package.resolved | 4 +- Riot/Modules/Room/RoomViewController.swift | 75 +++++++++++-------- .../Views/InputToolbar/RoomInputToolbarView.h | 4 +- .../WysiwygInputToolbarView.swift | 27 +++++-- project.yml | 2 +- 5 files changed, 66 insertions(+), 46 deletions(-) diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index daf871ac6..a087c8ac3 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "b81654b30f8b22b2d13f17e5e4c843e1fdc1db32", - "version" : "1.2.2" + "revision" : "aa98d9b6e4c3d2c4927190c09c5a7e56d08dbfb0", + "version" : "1.3.0" } }, { diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index b36447654..a51beba3b 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import HTMLParser import UIKit import WysiwygComposer @@ -38,6 +39,9 @@ extension RoomViewController { newAttributedString.appendString(" ") } else if roomMember.userId == self.mainSession.myUser.userId { newAttributedString.appendString("/me ") + newAttributedString.addAttribute(.font, + value: inputToolbarView.textDefaultFont, + range: .init(location: 0, length: newAttributedString.length)) } else { if #available(iOS 15.0, *) { newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember, @@ -155,24 +159,7 @@ extension RoomViewController { @objc func togglePlainTextMode() { RiotSettings.shared.enableWysiwygTextFormatting.toggle() - - guard let wysiwygInputToolbar else { return } - - // Switching from plain -> RTE, replace Pills by valid markdown links for parsing. - if !wysiwygInputToolbar.textFormattingEnabled, #available(iOS 15.0, *), - let attributedText = wysiwygInputToolbar.attributedTextMessage { - wysiwygInputToolbar.attributedTextMessage = NSAttributedString(string: PillsFormatter.stringByReplacingPills(in: attributedText, mode: .markdown)) - } - - wysiwygInputToolbar.textFormattingEnabled.toggle() - - // Switching from RTE -> plain, replace markdown links with Pills. - if !wysiwygInputToolbar.textFormattingEnabled, #available(iOS 15.0, *), - let attributedText = wysiwygInputToolbar.attributedTextMessage { - wysiwygInputToolbar.attributedTextMessage = PillsFormatter.insertPills(in: attributedText, - roomState: self.roomDataSource.roomState, - font: self.inputToolbarView.textDefaultFont) - } + wysiwygInputToolbar?.textFormattingEnabled.toggle() } @objc func didChangeMaximisedState(_ isMaximised: Bool) { @@ -270,21 +257,6 @@ extension RoomViewController { composerLinkActionBridgePresenter = presenter presenter.present(from: self, animated: true) } - - @objc func didRequestAttachmentStringForLink(_ link: String, andDisplayName: String) -> NSAttributedString? { - guard #available(iOS 15.0, *), - let userId = PillsFormatter.userIdFromPermalink(link), - let roomState = self.roomDataSource.roomState, - let member = PillsFormatter.roomMember(withUserId: userId, - roomState: roomState, - andLatestRoomState: nil) else { - return nil - } - - return PillsFormatter.mentionPill(withRoomMember: member, - isHighlighted: false, - font: inputToolbarView.textDefaultFont) - } @objc func showWaitingOtherParticipantHeader() { let controller = VectorHostingController(rootView: RoomWaitingForMembers()) @@ -395,6 +367,43 @@ extension RoomViewController: ComposerLinkActionBridgePresenterDelegate { } } +// MARK: - PermalinkReplacer +extension RoomViewController: PermalinkReplacer { + public func replacementForLink(_ url: String, text: String) -> NSAttributedString? { + guard #available(iOS 15.0, *), + let userId = PillsFormatter.userIdFromPermalink(url), + let roomState = roomDataSource.roomState, + let member = PillsFormatter.roomMember(withUserId: userId, + roomState: roomState, + andLatestRoomState: nil) else { + return nil + } + + return PillsFormatter.mentionPill(withRoomMember: member, + isHighlighted: false, + font: inputToolbarView.textDefaultFont) + } + + public func postProcessMarkdown(in attributedString: NSAttributedString) -> NSAttributedString { + guard #available(iOS 15.0, *), + let roomState = roomDataSource.roomState else { + return attributedString + } + + return PillsFormatter.insertPills(in: attributedString, + roomState: roomState, + font: inputToolbarView.textDefaultFont) + } + + public func restoreMarkdown(in attributedString: NSAttributedString) -> String { + if #available(iOS 15.0, *) { + return PillsFormatter.stringByReplacingPills(in: attributedString, mode: .markdown) + } else { + return attributedString.string + } + } +} + // MARK: - VoiceBroadcast extension RoomViewController { @objc func stopUncompletedVoiceBroadcastIfNeeded() { diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index ee1a032e0..ebbb8305a 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -60,7 +60,7 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode) @param toolbarView the room input toolbar view */ -- (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView*)toolbarView; +- (void)roomInputToolbarViewDidChangeTextMessage:(MXKRoomInputToolbarView*)toolbarView; /** Inform the delegate that the action menu was opened. @@ -83,8 +83,6 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode) - (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern; -- (nullable NSAttributedString *)didRequestAttachmentStringForLink: (NSString *)link andDisplayName: (NSString *)displayName; - @end /** diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index a092a18ea..78989a23e 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -45,7 +45,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var hostingViewController: VectorHostingController! private lazy var wysiwygViewModel = WysiwygComposerViewModel( parserStyle: WysiwygInputToolbarView.parserStyle, - permalinkReplacer: self + permalinkReplacer: permalinkReplacer ) /// Compute current HTML parser style for composer. private static var parserStyle: HTMLParserStyle { @@ -73,6 +73,12 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } // MARK: Public + + override var delegate: MXKRoomInputToolbarViewDelegate! { + didSet { + wysiwygViewModel.permalinkReplacer = permalinkReplacer + } + } override var placeholder: String! { get { @@ -138,6 +144,10 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private weak var toolbarViewDelegate: RoomInputToolbarViewDelegate? { return (delegate as? RoomInputToolbarViewDelegate) ?? nil } + + private var permalinkReplacer: PermalinkReplacer? { + return (delegate as? PermalinkReplacer) + } override func awakeFromNib() { super.awakeFromNib() @@ -207,6 +217,15 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp if !value { self.voiceMessageBottomConstraint?.constant = 2 } + }, + + wysiwygViewModel.$plainTextContent + .dropFirst() + .removeDuplicates() + .sink { [weak self] value in + guard let self else { return } + self.textMessage = value.string + self.toolbarViewDelegate?.roomInputToolbarViewDidChangeTextMessage(self) } ] @@ -443,12 +462,6 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } } -extension WysiwygInputToolbarView: PermalinkReplacer { - func replacementForLink(_ link: String, text: String) -> NSAttributedString? { - return toolbarViewDelegate?.didRequestAttachmentString(forLink: link, andDisplayName: text) - } -} - // MARK: - LegacySendModeAdapter fileprivate extension ComposerSendMode { diff --git a/project.yml b/project.yml index 64ab238d2..ff745767a 100644 --- a/project.yml +++ b/project.yml @@ -56,7 +56,7 @@ packages: branch: 0.0.1 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 1.2.2 + version: 1.3.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From a3e4fac23f634b48ce75c070513404aa98670f39 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 21 Mar 2023 10:27:08 +0100 Subject: [PATCH 006/149] Fix broken constraint after using fullscreen mode --- Riot/Modules/Room/RoomViewController.xib | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index f33d661bd..b7a62a8bf 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -39,6 +39,7 @@ + From 52594087a28d267fbd7470fb598d0804f36a0ca2 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 22 Mar 2023 15:49:42 +0100 Subject: [PATCH 007/149] Display user suggestion list in fullscreen mode with shared context from `UserSuggestionCoordinator` --- Riot/Modules/Room/RoomViewController.m | 5 ++ .../Views/InputToolbar/RoomInputToolbarView.h | 3 + .../WysiwygInputToolbarView.swift | 40 ++++++---- .../Composer/MockComposerScreenState.swift | 23 +++++- .../Room/Composer/Model/ComposerModels.swift | 9 +++ .../Modules/Room/Composer/View/Composer.swift | 75 ++++++++++++++----- .../UserSuggestionCoordinator.swift | 18 +++++ .../UserSuggestionCoordinatorBridge.swift | 4 + .../UserSuggestionViewModel.swift | 6 +- .../UserSuggestionViewModelProtocol.swift | 1 + 10 files changed, 148 insertions(+), 36 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 17ab27fc4..e13b5c038 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5154,6 +5154,11 @@ static CGSize kThreadListBarButtonItemImageSize; [self.userSuggestionCoordinator processSuggestionPattern:suggestionPattern]; } +- (UserSuggestionSharedContext *)userSuggestionContext +{ + return [self.userSuggestionCoordinator sharedContext]; +} + - (void)roomInputToolbarViewDidOpenActionMenu:(RoomInputToolbarView*)toolbarView { // Consider opening the action menu as beginning to type and share encryption keys if requested. diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index ebbb8305a..454134d28 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -22,6 +22,7 @@ @class RoomInputToolbarView; @class LinkActionWrapper; @class SuggestionPatternWrapper; +@class UserSuggestionSharedContext; /** Destination of the message in the composer @@ -83,6 +84,8 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode) - (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern; +- (UserSuggestionSharedContext *)userSuggestionContext; + @end /** diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 78989a23e..d50be4e3a 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -76,7 +76,8 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp override var delegate: MXKRoomInputToolbarViewDelegate! { didSet { - wysiwygViewModel.permalinkReplacer = permalinkReplacer + setComposer() + //wysiwygViewModel.permalinkReplacer = permalinkReplacer } } @@ -134,6 +135,10 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp var maxCompressedHeight: CGFloat { wysiwygViewModel.maxCompressedHeight } + + var userSuggestionSharedContext: UserSuggestionSharedContext { + return toolbarViewDelegate!.userSuggestionContext() + } // MARK: - Setup @@ -148,23 +153,24 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var permalinkReplacer: PermalinkReplacer? { return (delegate as? PermalinkReplacer) } - - override func awakeFromNib() { - super.awakeFromNib() + + func setComposer() { viewModel = ComposerViewModel( initialViewState: ComposerViewState(textFormattingEnabled: RiotSettings.shared.enableWysiwygTextFormatting, - isLandscapePhone: isLandscapePhone, bindings: ComposerBindings(focused: false))) - + isLandscapePhone: isLandscapePhone, + bindings: ComposerBindings(focused: false))) + viewModel.callback = { [weak self] result in self?.handleViewModelResult(result) } wysiwygViewModel.plainTextMode = !RiotSettings.shared.enableWysiwygTextFormatting - + inputAccessoryViewForKeyboard = UIView(frame: .zero) - + let composer = Composer( viewModel: viewModel.context, wysiwygViewModel: wysiwygViewModel, + userSuggestionSharedContext: userSuggestionSharedContext, resizeAnimationDuration: Double(kResizeComposerAnimationDuration), sendMessageAction: { [weak self] content in guard let self = self else { return } @@ -176,13 +182,13 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp guard let self = self else { return } textView.inputAccessoryView = self.inputAccessoryViewForKeyboard } - + hostingViewController = VectorHostingController(rootView: composer) hostingViewController.publishHeightChanges = true let height = hostingViewController.sizeThatFits(in: CGSize(width: self.frame.width, height: UIView.layoutFittingExpandedSize.height)).height let subView: UIView = hostingViewController.view self.addSubview(subView) - + self.translatesAutoresizingMaskIntoConstraints = false subView.translatesAutoresizingMaskIntoConstraints = false heightConstraint = subView.heightAnchor.constraint(equalToConstant: height) @@ -192,7 +198,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp subView.trailingAnchor.constraint(equalTo: self.trailingAnchor), subView.bottomAnchor.constraint(equalTo: self.bottomAnchor) ]) - + cancellables = [ hostingViewController.heightPublisher .removeDuplicates() @@ -206,7 +212,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp .sink { [weak hostingViewController] _ in hostingViewController?.view.setNeedsLayout() }, - + wysiwygViewModel.$maximised .dropFirst() .removeDuplicates() @@ -228,7 +234,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp self.toolbarViewDelegate?.roomInputToolbarViewDidChangeTextMessage(self) } ] - + update(theme: ThemeService.shared().theme) registerThemeServiceDidChangeThemeNotification() NotificationCenter.default.addObserver( @@ -246,6 +252,14 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate), name: UIDevice.orientationDidChangeNotification, object: nil) } + override func awakeFromNib() { + super.awakeFromNib() + + if delegate != nil { + setComposer() + } + } + override func customizeRendering() { super.customizeRendering() self.backgroundColor = .clear diff --git a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift index 35a628d02..b7d20d38a 100644 --- a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift @@ -29,12 +29,24 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let viewModel: ComposerViewModel + let userSuggestionViewModel = MockUserSuggestionViewModel(initialViewState: UserSuggestionViewState(items: [])) + let userSuggestionSharedContext = UserSuggestionSharedContext(context: userSuggestionViewModel.context, + mediaManager: MXMediaManager()) let bindings = ComposerBindings(focused: false) switch self { - case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings)) - case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit, textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings)) - case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply, textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings)) + case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, + isLandscapePhone: false, + bindings: bindings)) + case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit, + textFormattingEnabled: true, + isLandscapePhone: false, + bindings: bindings)) + case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", + sendMode: .reply, + textFormattingEnabled: true, + isLandscapePhone: false, + bindings: bindings)) } let wysiwygviewModel = WysiwygComposerViewModel(minHeight: 20, maxCompressedHeight: 360) @@ -57,6 +69,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { Spacer() Composer(viewModel: viewModel.context, wysiwygViewModel: wysiwygviewModel, + userSuggestionSharedContext: userSuggestionSharedContext, resizeAnimationDuration: 0.1, sendMessageAction: { _ in }, showSendMediaActions: { }) @@ -70,3 +83,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { ) } } + +private final class MockUserSuggestionViewModel: UserSuggestionViewModelType { + +} diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index c96453667..6f7bab165 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -256,3 +256,12 @@ final class SuggestionPatternWrapper: NSObject { super.init() } } + +final class UserSuggestionViewModelWrapper: NSObject { + let userSuggestionViewModel: UserSuggestionViewModel + + init(_ userSuggestionViewModel: UserSuggestionViewModel) { + self.userSuggestionViewModel = userSuggestionViewModel + super.init() + } +} diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index fb6ed8851..93793fb72 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -23,6 +23,7 @@ struct Composer: View { // MARK: Private @ObservedObject private var viewModel: ComposerViewModelType.Context @ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel + private let userSuggestionSharedContext: UserSuggestionSharedContext private let resizeAnimationDuration: Double private let sendMessageAction: (WysiwygComposerContent) -> Void @@ -31,15 +32,42 @@ struct Composer: View { @Environment(\.theme) private var theme: ThemeSwiftUI @State private var isActionButtonShowing = false - + private let horizontalPadding: CGFloat = 12 private let borderHeight: CGFloat = 40 - private var verticalPadding: CGFloat { + private let standardVerticalPadding: CGFloat = 8.0 + private let contextBannerHeight: CGFloat = 14.5 + + /// Spacing applied within the VStack holding the context banner and the composer text view. + private let verticalComponentSpacing: CGFloat = 12.0 + /// Padding for the main composer text view. Always applied on bottom. + /// Applied on top only if no context banner is present. + private var composerVerticalPadding: CGFloat { (borderHeight - wysiwygViewModel.minHeight) / 2 } - - private var topPadding: CGFloat { - viewModel.viewState.shouldDisplayContext ? 0 : verticalPadding + + /// Computes the top padding to apply on the composer text view depending on context. + private var composerTopPadding: CGFloat { + viewModel.viewState.shouldDisplayContext ? 0 : composerVerticalPadding + } + + /// Computes the additional height required to display the context banner. + /// Returns 0.0 if the banner is not displayed. + /// Note: height of the actual banner + its added standard top padding + VStack spacing + private var additionalHeightForContextBanner: CGFloat { + viewModel.viewState.shouldDisplayContext ? contextBannerHeight + standardVerticalPadding + verticalComponentSpacing : 0 + } + + /// Computes the total height of the composer (excluding the RTE formatting bar). + /// This height includes the text view, as well as the context banner + /// and user suggestion list when displayed. + private var composerHeight: CGFloat { + wysiwygViewModel.idealHeight + + composerTopPadding + + composerVerticalPadding + // Extra padding added on top of the VStack containing the composer + + standardVerticalPadding + + additionalHeightForContextBanner } private var cornerRadius: CGFloat { @@ -84,7 +112,7 @@ struct Composer: View { private var composerContainer: some View { let rect = RoundedRectangle(cornerRadius: cornerRadius) - return VStack(spacing: 12) { + return VStack(spacing: verticalComponentSpacing) { if viewModel.viewState.shouldDisplayContext { HStack { if let imageName = viewModel.viewState.contextImageName { @@ -106,7 +134,8 @@ struct Composer: View { } .accessibilityIdentifier("cancelButton") } - .padding(.top, 8) + .frame(height: contextBannerHeight) + .padding(.top, standardVerticalPadding) .padding(.horizontal, horizontalPadding) } HStack(alignment: shouldFixRoundCorner ? .top : .center, spacing: 0) { @@ -116,7 +145,6 @@ struct Composer: View { ) .tintColor(theme.colors.accent) .placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent) - .frame(height: wysiwygViewModel.idealHeight) .onAppear { if wysiwygViewModel.isContentEmpty { wysiwygViewModel.setup() @@ -137,13 +165,13 @@ struct Composer: View { } } .padding(.horizontal, horizontalPadding) - .padding(.top, topPadding) - .padding(.bottom, verticalPadding) + .padding(.top, composerTopPadding) + .padding(.bottom, composerVerticalPadding) } .clipShape(rect) .overlay(rect.stroke(borderColor, lineWidth: 1)) .animation(.easeInOut(duration: resizeAnimationDuration), value: wysiwygViewModel.idealHeight) - .padding(.top, 8) + .padding(.top, standardVerticalPadding) .onTapGesture { if viewModel.focused { viewModel.focused = true @@ -195,11 +223,13 @@ struct Composer: View { init( viewModel: ComposerViewModelType.Context, wysiwygViewModel: WysiwygComposerViewModel, + userSuggestionSharedContext: UserSuggestionSharedContext, resizeAnimationDuration: Double, sendMessageAction: @escaping (WysiwygComposerContent) -> Void, showSendMediaActions: @escaping () -> Void) { self.viewModel = viewModel self.wysiwygViewModel = wysiwygViewModel + self.userSuggestionSharedContext = userSuggestionSharedContext self.resizeAnimationDuration = resizeAnimationDuration self.sendMessageAction = sendMessageAction self.showSendMediaActions = showSendMediaActions @@ -213,17 +243,24 @@ struct Composer: View { .frame(width: 36, height: 5) .padding(.top, 10) } - HStack(alignment: .bottom, spacing: 0) { - if !viewModel.viewState.textFormattingEnabled { - sendMediaButton - .padding(.bottom, 1) + VStack { + HStack(alignment: .bottom, spacing: 0) { + if !viewModel.viewState.textFormattingEnabled { + sendMediaButton + .padding(.bottom, 1) + } + composerContainer + if !viewModel.viewState.textFormattingEnabled { + sendButton + .padding(.bottom, 1) + } } - composerContainer - if !viewModel.viewState.textFormattingEnabled { - sendButton - .padding(.bottom, 1) + if wysiwygViewModel.maximised { + UserSuggestionList(viewModel: userSuggestionSharedContext.context) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: userSuggestionSharedContext.mediaManager))) } } + .frame(height: composerHeight) if viewModel.viewState.textFormattingEnabled { HStack(alignment: .center, spacing: 0) { sendMediaButton diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index 59b25ef86..38776fbd3 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -30,6 +30,19 @@ struct UserSuggestionCoordinatorParameters { let room: MXRoom } +/// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple +/// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController` +/// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload data. +final class UserSuggestionSharedContext: NSObject { + let context: UserSuggestionViewModelType.Context + let mediaManager: MXMediaManager + + init(context: UserSuggestionViewModelType.Context, mediaManager: MXMediaManager) { + self.context = context + self.mediaManager = mediaManager + } +} + final class UserSuggestionCoordinator: Coordinator, Presentable { // MARK: - Properties @@ -105,6 +118,11 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { userSuggestionHostingController } + func sharedContext() -> UserSuggestionSharedContext { + UserSuggestionSharedContext(context: userSuggestionViewModel.sharedContext, + mediaManager: parameters.mediaManager) + } + // MARK: - Private private func calculateViewHeight() -> CGFloat { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift index 4605547eb..a7615e43f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -52,6 +52,10 @@ final class UserSuggestionCoordinatorBridge: NSObject { func toPresentable() -> UIViewController? { userSuggestionCoordinator.toPresentable() } + + func sharedContext() -> UserSuggestionSharedContext { + userSuggestionCoordinator.sharedContext() + } } extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift index 1e1f490fc..3999447b7 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift @@ -27,7 +27,11 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo private let userSuggestionService: UserSuggestionServiceProtocol // MARK: Public - + + var sharedContext: UserSuggestionViewModelType.Context { + return self.context + } + var completion: ((UserSuggestionViewModelResult) -> Void)? // MARK: - Setup diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift index 1d89ca9b4..40318c5df 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift @@ -17,5 +17,6 @@ import Foundation protocol UserSuggestionViewModelProtocol { + var sharedContext: UserSuggestionViewModelType.Context { get } var completion: ((UserSuggestionViewModelResult) -> Void)? { get set } } From 66b44bc459508cf760af10e9c4668fb0924e6978 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 11:47:15 +0100 Subject: [PATCH 008/149] Use `PillProvider` for RTE Pills creation --- Riot/Modules/Pills/PillProvider.swift | 10 +- Riot/Modules/Pills/PillsFormatter.swift | 111 +++++++++++---------- Riot/Modules/Room/RoomViewController.swift | 22 ++-- 3 files changed, 81 insertions(+), 62 deletions(-) diff --git a/Riot/Modules/Pills/PillProvider.swift b/Riot/Modules/Pills/PillProvider.swift index 60363bc47..ddff3084b 100644 --- a/Riot/Modules/Pills/PillProvider.swift +++ b/Riot/Modules/Pills/PillProvider.swift @@ -26,14 +26,14 @@ private enum PillAttachmentKind { struct PillProvider { private let session: MXSession private let eventFormatter: MXKEventFormatter - private let event: MXEvent + private let event: MXEvent? private let roomState: MXRoomState private let latestRoomState: MXRoomState? private let isEditMode: Bool init(withSession session: MXSession, eventFormatter: MXKEventFormatter, - event: MXEvent, + event: MXEvent?, roomState: MXRoomState, andLatestRoomState latestRoomState: MXRoomState?, isEditMode: Bool) { @@ -46,7 +46,7 @@ struct PillProvider { self.isEditMode = isEditMode } - func pillTextAttachmentString(forUrl url: URL, withLabel label: String, event: MXEvent) -> NSAttributedString? { + func pillTextAttachmentString(forUrl url: URL, withLabel label: String) -> NSAttributedString? { // Try to get a pill from this url guard let pillType = PillType.from(url: url) else { @@ -133,6 +133,10 @@ struct PillProvider { let avatarUrl = roomMember?.avatarUrl ?? user?.avatarUrl let displayName = roomMember?.displayname ?? user?.displayName ?? userId let isHighlighted = userId == session.myUserId + // No actual event means it is a composer Pill. No highlight + && event != nil + // No highlight on self-mentions. + && event?.sender == session.myUserId let avatar: PillTextAttachmentItem if roomMember == nil && user == nil { diff --git a/Riot/Modules/Pills/PillsFormatter.swift b/Riot/Modules/Pills/PillsFormatter.swift index 4882ad51c..675e824ba 100644 --- a/Riot/Modules/Pills/PillsFormatter.swift +++ b/Riot/Modules/Pills/PillsFormatter.swift @@ -65,7 +65,7 @@ class PillsFormatter: NSObject { // try to get a mention pill from the url let label = Range(range, in: newAttr.string).flatMap { String(newAttr.string[$0]) } - if let attachmentString: NSAttributedString = provider.pillTextAttachmentString(forUrl: url, withLabel: label ?? "", event: event) { + if let attachmentString: NSAttributedString = provider.pillTextAttachmentString(forUrl: url, withLabel: label ?? "") { // replace the url with the pill newAttr.replaceCharacters(in: range, with: attachmentString) } @@ -81,36 +81,28 @@ class PillsFormatter: NSObject { /// - roomState: The current room state /// - font: The font to use for the pill text /// - Returns: A new attributed string with pills. - static func insertPills(in markdownString: NSAttributedString, roomState: MXRoomState, font: UIFont) -> NSAttributedString { - // Create a regexp that detects markdown links. - let pattern = "\\[([^\\]]+)\\]\\(([^\\)\"\\s]+)(?:\\s+\"(.*)\")?\\)" - guard let regExp = try? NSRegularExpression(pattern: pattern) else { return markdownString } - - let matches = regExp.matches(in: markdownString.string, - range: .init(location: 0, length: markdownString.length)) + static func insertPills(in markdownString: NSAttributedString, + withSession session: MXSession, + eventFormatter: MXKEventFormatter, + roomState: MXRoomState, + font: UIFont) -> NSAttributedString { + let matches = markdownUrls(in: markdownString) // If we have some matches, replace permalinks by a pill version. + guard !matches.isEmpty else { return markdownString } + + let pillProvider = PillProvider(withSession: session, + eventFormatter: eventFormatter, + event: nil, + roomState: roomState, + andLatestRoomState: nil, + isEditMode: true) + let mutable = NSMutableAttributedString(attributedString: markdownString) - for match in matches.reversed() { - // Range at 2 is the URL, no need to care about the other parts because - // we are retrieving the most recent display name from the room state. - let urlRange = match.range(at: 2) - var url = markdownString.attributedSubstring(from: urlRange).string - // Note: a valid markdown link can be written with - // enclosing <..>, remove them for userId detection. - if url.first == "<" && url.last == ">" { - url = String(url[url.index(after: url.startIndex)...url.index(url.endIndex, offsetBy: -2)]) - } - - // If we find a user matching the link, replace the - // entire range of the match with a mention pill. - if let userId = userIdFromPermalink(url), - let roomMember = roomMember(withUserId: userId, - roomState: roomState, - andLatestRoomState: nil) { - let attachmentString = mentionPill(withRoomMember: roomMember, isHighlighted: false, font: font) - mutable.replaceCharacters(in: match.range, with: attachmentString) + matches.reversed().forEach { (url: URL, label: String, range: NSRange) in + if let attachmentString = pillProvider.pillTextAttachmentString(forUrl: url, withLabel: label) { + mutable.replaceCharacters(in: range, with: attachmentString) } } @@ -166,6 +158,20 @@ class PillsFormatter: NSObject { } return attributedStringWithAttachment(attachment, link: url, font: font) } + + static func mentionPill(withUrl url: URL, + andLabel label: String, + session: MXSession, + eventFormatter: MXKEventFormatter, + roomState: MXRoomState) -> NSAttributedString? { + let pillProvider = PillProvider(withSession: session, + eventFormatter: eventFormatter, + event: nil, + roomState: roomState, + andLatestRoomState: nil, + isEditMode: true) + return pillProvider.pillTextAttachmentString(forUrl: url, withLabel: label) + } /// Update alpha of all `PillTextAttachment` contained in given attributed string. /// @@ -217,30 +223,35 @@ extension PillsFormatter { } return string } +} - /// Extract user id from given permalink - /// - Parameter permalink: the permalink - /// - Returns: userId, if any - static func userIdFromPermalink(_ permalink: String) -> String? { - let baseUrl: String - if let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl { - baseUrl = String(format: "%@/#/user/", clientBaseUrl) - } else { - baseUrl = String(format: "%@/#/", kMXMatrixDotToUrl) +@available(iOS 15.0, *) +private extension PillsFormatter { + static func markdownUrls(in attributedString: NSAttributedString) -> [(url: URL, label: String, range: NSRange)] { + // Create a regexp that detects markdown links. + let pattern = "\\[([^\\]]+)\\]\\(([^\\)\"\\s]+)(?:\\s+\"(.*)\")?\\)" + guard let regExp = try? NSRegularExpression(pattern: pattern) else { return [] } + + let matches = regExp.matches(in: attributedString.string, + range: .init(location: 0, length: attributedString.length)) + + return matches.compactMap { match in + let labelRange = match.range(at: 1) + let urlRange = match.range(at: 2) + let label = attributedString.attributedSubstring(from: labelRange).string + var url = attributedString.attributedSubstring(from: urlRange).string + + // Note: a valid markdown link can be written with + // enclosing <..>, remove them for userId detection. + if url.first == "<" && url.last == ">" { + url = String(url[url.index(after: url.startIndex)...url.index(url.endIndex, offsetBy: -2)]) + } + + if let url = URL(string: url) { + return (url: url, label: label, range: match.range) + } else { + return nil + } } - return permalink.starts(with: baseUrl) ? String(permalink.dropFirst(baseUrl.count)) : nil - } - - /// Retrieve the latest available `MXRoomMember` from given data. - /// - /// - Parameters: - /// - userId: the id of the user - /// - roomState: room state for message - /// - latestRoomState: latest room state of the room containing this message - /// - Returns: the room member, if available - static func roomMember(withUserId userId: String, - roomState: MXRoomState, - andLatestRoomState latestRoomState: MXRoomState?) -> MXRoomMember? { - return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId) } } diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index a51beba3b..3d9ac2260 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -371,26 +371,30 @@ extension RoomViewController: ComposerLinkActionBridgePresenterDelegate { extension RoomViewController: PermalinkReplacer { public func replacementForLink(_ url: String, text: String) -> NSAttributedString? { guard #available(iOS 15.0, *), - let userId = PillsFormatter.userIdFromPermalink(url), - let roomState = roomDataSource.roomState, - let member = PillsFormatter.roomMember(withUserId: userId, - roomState: roomState, - andLatestRoomState: nil) else { + let url = URL(string: url), + let session = roomDataSource.mxSession, + let eventFormatter = roomDataSource.eventFormatter, + let roomState = roomDataSource.roomState else { return nil } - return PillsFormatter.mentionPill(withRoomMember: member, - isHighlighted: false, - font: inputToolbarView.textDefaultFont) + return PillsFormatter.mentionPill(withUrl: url, + andLabel: text, + session: session, + eventFormatter: eventFormatter, + roomState: roomState) } public func postProcessMarkdown(in attributedString: NSAttributedString) -> NSAttributedString { guard #available(iOS 15.0, *), + let session = roomDataSource.mxSession, + let eventFormatter = roomDataSource.eventFormatter, let roomState = roomDataSource.roomState else { return attributedString } - return PillsFormatter.insertPills(in: attributedString, + withSession: session, + eventFormatter: eventFormatter, roomState: roomState, font: inputToolbarView.textDefaultFont) } From 3538d4dd7f49db70996e0dc996952c3d7ee7d212 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 14:30:17 +0100 Subject: [PATCH 009/149] Avoid crashing if data source is not ready when translating Pills --- Riot/Modules/Room/RoomViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 3d9ac2260..00de9de95 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -387,6 +387,7 @@ extension RoomViewController: PermalinkReplacer { public func postProcessMarkdown(in attributedString: NSAttributedString) -> NSAttributedString { guard #available(iOS 15.0, *), + let roomDataSource, let session = roomDataSource.mxSession, let eventFormatter = roomDataSource.eventFormatter, let roomState = roomDataSource.roomState else { From 572777205a3e71f7f7bda22fc6ffb1ab5729c7a0 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 14:50:49 +0100 Subject: [PATCH 010/149] Clean `WysiwygInputToolbarView` code --- .../WysiwygInputToolbarView.swift | 123 +++++++++--------- 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index d50be4e3a..343f020f8 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -43,9 +43,8 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var heightConstraint: NSLayoutConstraint! private var voiceMessageBottomConstraint: NSLayoutConstraint? private var hostingViewController: VectorHostingController! - private lazy var wysiwygViewModel = WysiwygComposerViewModel( - parserStyle: WysiwygInputToolbarView.parserStyle, - permalinkReplacer: permalinkReplacer + private var wysiwygViewModel = WysiwygComposerViewModel( + parserStyle: WysiwygInputToolbarView.parserStyle ) /// Compute current HTML parser style for composer. private static var parserStyle: HTMLParserStyle { @@ -76,8 +75,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp override var delegate: MXKRoomInputToolbarViewDelegate! { didSet { - setComposer() - //wysiwygViewModel.permalinkReplacer = permalinkReplacer + setupComposerIfNeeded() } } @@ -135,10 +133,6 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp var maxCompressedHeight: CGFloat { wysiwygViewModel.maxCompressedHeight } - - var userSuggestionSharedContext: UserSuggestionSharedContext { - return toolbarViewDelegate!.userSuggestionContext() - } // MARK: - Setup @@ -153,8 +147,62 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var permalinkReplacer: PermalinkReplacer? { return (delegate as? PermalinkReplacer) } + + override func awakeFromNib() { + super.awakeFromNib() + + setupComposerIfNeeded() + } + + override func customizeRendering() { + super.customizeRendering() + self.backgroundColor = .clear + } + + override func dismissKeyboard() { + self.viewModel.dismissKeyboard() + } + + @discardableResult + override func becomeFirstResponder() -> Bool { + self.wysiwygViewModel.textView.becomeFirstResponder() + } + + override func dismissValidationView(_ validationView: MXKImageView!) { + super.dismissValidationView(validationView) + if isMaximised { + showKeyboard() + } + } + + func showKeyboard() { + self.viewModel.showKeyboard() + } + + func minimise() { + wysiwygViewModel.maximised = false + } + + func performLinkOperation(_ linkOperation: WysiwygLinkOperation) { + if let selectionToRestore = viewModel.selectionToRestore { + wysiwygViewModel.select(range: selectionToRestore) + } + wysiwygViewModel.applyLinkOperation(linkOperation) + } + + func mention(_ member: MXRoomMember) { + self.wysiwygViewModel.setMention(link: MXTools.permalinkToUser(withUserId: member.userId), + name: member.displayname, + key: .at) + } + + // MARK: - Private + + private func setupComposerIfNeeded() { + guard hostingViewController == nil, + let toolbarViewDelegate, + let permalinkReplacer else { return } - func setComposer() { viewModel = ComposerViewModel( initialViewState: ComposerViewState(textFormattingEnabled: RiotSettings.shared.enableWysiwygTextFormatting, isLandscapePhone: isLandscapePhone, @@ -164,13 +212,14 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp self?.handleViewModelResult(result) } wysiwygViewModel.plainTextMode = !RiotSettings.shared.enableWysiwygTextFormatting + wysiwygViewModel.permalinkReplacer = permalinkReplacer inputAccessoryViewForKeyboard = UIView(frame: .zero) let composer = Composer( viewModel: viewModel.context, wysiwygViewModel: wysiwygViewModel, - userSuggestionSharedContext: userSuggestionSharedContext, + userSuggestionSharedContext: toolbarViewDelegate.userSuggestionContext(), resizeAnimationDuration: Double(kResizeComposerAnimationDuration), sendMessageAction: { [weak self] content in guard let self = self else { return } @@ -252,58 +301,6 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate), name: UIDevice.orientationDidChangeNotification, object: nil) } - override func awakeFromNib() { - super.awakeFromNib() - - if delegate != nil { - setComposer() - } - } - - override func customizeRendering() { - super.customizeRendering() - self.backgroundColor = .clear - } - - override func dismissKeyboard() { - self.viewModel.dismissKeyboard() - } - - @discardableResult - override func becomeFirstResponder() -> Bool { - self.wysiwygViewModel.textView.becomeFirstResponder() - } - - override func dismissValidationView(_ validationView: MXKImageView!) { - super.dismissValidationView(validationView) - if isMaximised { - showKeyboard() - } - } - - func showKeyboard() { - self.viewModel.showKeyboard() - } - - func minimise() { - wysiwygViewModel.maximised = false - } - - func performLinkOperation(_ linkOperation: WysiwygLinkOperation) { - if let selectionToRestore = viewModel.selectionToRestore { - wysiwygViewModel.select(range: selectionToRestore) - } - wysiwygViewModel.applyLinkOperation(linkOperation) - } - - func mention(_ member: MXRoomMember) { - self.wysiwygViewModel.setMention(link: MXTools.permalinkToUser(withUserId: member.userId), - name: member.displayname, - key: .at) - } - - // MARK: - Private - @objc private func keyboardWillShow(_ notification: Notification) { if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { let keyboardRectangle = keyboardFrame.cgRectValue From f7dbc738af9885483ab8fe9e6f4a828fc4ac4b74 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 14:51:56 +0100 Subject: [PATCH 011/149] Allow displaying `UserSuggestionList` without shadow --- .../Modules/Room/Composer/View/Composer.swift | 2 +- .../View/UserSuggestionList.swift | 45 +++++++++++-------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index 93793fb72..824e04d73 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -256,7 +256,7 @@ struct Composer: View { } } if wysiwygViewModel.maximised { - UserSuggestionList(viewModel: userSuggestionSharedContext.context) + UserSuggestionList(viewModel: userSuggestionSharedContext.context, showBackgroundShadow: false) .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: userSuggestionSharedContext.mediaManager))) } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 859b0b414..9c32b892f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -35,6 +35,7 @@ struct UserSuggestionList: View { // MARK: Public @ObservedObject var viewModel: UserSuggestionViewModel.Context + var showBackgroundShadow: Bool = true var body: some View { if viewModel.viewState.items.isEmpty { @@ -46,25 +47,12 @@ struct UserSuggestionList: View { userId: "Prototype") .background(ViewFrameReader(frame: $prototypeListItemFrame)) .hidden() - BackgroundView { - List(viewModel.viewState.items) { item in - Button { - viewModel.send(viewAction: .selectedItem(item)) - } label: { - UserSuggestionListItem( - avatar: item.avatar, - displayName: item.displayName, - userId: item.id - ) - .padding(.bottom, Constants.listItemPadding) - .padding(.top, viewModel.viewState.items.first?.id == item.id ? Constants.listItemPadding + Constants.topPadding : Constants.listItemPadding) - } + if showBackgroundShadow { + BackgroundView { + list() } - .listStyle(PlainListStyle()) - .frame(height: min(Constants.maxHeight, - min(contentHeightForRowCount(Constants.maxVisibleRows), - contentHeightForRowCount(viewModel.viewState.items.count)))) - .id(UUID()) // Rebuild the whole list on item changes. Fixes performance issues. + } else { + list() } } } @@ -73,6 +61,27 @@ struct UserSuggestionList: View { private func contentHeightForRowCount(_ count: Int) -> CGFloat { (prototypeListItemFrame.height + (Constants.listItemPadding * 2) + Constants.lineSpacing) * CGFloat(count) + Constants.topPadding } + + private func list() -> some View { + List(viewModel.viewState.items) { item in + Button { + viewModel.send(viewAction: .selectedItem(item)) + } label: { + UserSuggestionListItem( + avatar: item.avatar, + displayName: item.displayName, + userId: item.id + ) + .padding(.bottom, Constants.listItemPadding) + .padding(.top, viewModel.viewState.items.first?.id == item.id ? Constants.listItemPadding + Constants.topPadding : Constants.listItemPadding) + } + } + .listStyle(PlainListStyle()) + .frame(height: min(Constants.maxHeight, + min(contentHeightForRowCount(Constants.maxVisibleRows), + contentHeightForRowCount(viewModel.viewState.items.count)))) + .id(UUID()) // Rebuild the whole list on item changes. Fixes performance issues. + } } private struct BackgroundView: View { From e6bad67a28c3c64faf49fff57485c0f623d4aa4b Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 15:02:19 +0100 Subject: [PATCH 012/149] Fix wrong condition for highlight test --- Riot/Modules/Pills/PillProvider.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Pills/PillProvider.swift b/Riot/Modules/Pills/PillProvider.swift index ddff3084b..1941f8af1 100644 --- a/Riot/Modules/Pills/PillProvider.swift +++ b/Riot/Modules/Pills/PillProvider.swift @@ -135,8 +135,8 @@ struct PillProvider { let isHighlighted = userId == session.myUserId // No actual event means it is a composer Pill. No highlight && event != nil - // No highlight on self-mentions. - && event?.sender == session.myUserId + // No highlight on self-mentions + && event?.sender != session.myUserId let avatar: PillTextAttachmentItem if roomMember == nil && user == nil { From 24a1ce547b857242dc272eb7bb4dded0655f7eee Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 15:45:31 +0100 Subject: [PATCH 013/149] Update environment object setup and view model context wrapping to restore SwiftUI UI tests --- Riot/Modules/Room/RoomViewController.m | 7 ++++++- .../Views/InputToolbar/RoomInputToolbarView.h | 6 ++++-- .../WysiwygInputToolbarView.swift | 12 +++++++----- .../Room/Composer/MockComposerScreenState.swift | 4 +--- .../Modules/Room/Composer/View/Composer.swift | 7 +++---- .../Coordinator/UserSuggestionCoordinator.swift | 15 +++++---------- .../UserSuggestionCoordinatorBridge.swift | 2 +- .../UserSuggestionViewModelProtocol.swift | 3 +++ 8 files changed, 30 insertions(+), 26 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index e13b5c038..6fa73faa5 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5154,11 +5154,16 @@ static CGSize kThreadListBarButtonItemImageSize; [self.userSuggestionCoordinator processSuggestionPattern:suggestionPattern]; } -- (UserSuggestionSharedContext *)userSuggestionContext +- (UserSuggestionViewModelContextWrapper *)userSuggestionContext { return [self.userSuggestionCoordinator sharedContext]; } +- (MXMediaManager *)mediaManager +{ + return self.roomDataSource.mxSession.mediaManager; +} + - (void)roomInputToolbarViewDidOpenActionMenu:(RoomInputToolbarView*)toolbarView { // Consider opening the action menu as beginning to type and share encryption keys if requested. diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index 454134d28..897922832 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -22,7 +22,7 @@ @class RoomInputToolbarView; @class LinkActionWrapper; @class SuggestionPatternWrapper; -@class UserSuggestionSharedContext; +@class UserSuggestionViewModelContextWrapper; /** Destination of the message in the composer @@ -84,7 +84,9 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode) - (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern; -- (UserSuggestionSharedContext *)userSuggestionContext; +- (UserSuggestionViewModelContextWrapper *)userSuggestionContext; + +- (MXMediaManager *)mediaManager; @end diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 343f020f8..e6b191e2b 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -219,7 +219,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp let composer = Composer( viewModel: viewModel.context, wysiwygViewModel: wysiwygViewModel, - userSuggestionSharedContext: toolbarViewDelegate.userSuggestionContext(), + userSuggestionSharedContext: toolbarViewDelegate.userSuggestionContext().context, resizeAnimationDuration: Double(kResizeComposerAnimationDuration), sendMessageAction: { [weak self] content in guard let self = self else { return } @@ -227,10 +227,12 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp }, showSendMediaActions: { [weak self] in guard let self = self else { return } self.showSendMediaActions() - }).introspectTextView { [weak self] textView in - guard let self = self else { return } - textView.inputAccessoryView = self.inputAccessoryViewForKeyboard - } + }) + .introspectTextView { [weak self] textView in + guard let self = self else { return } + textView.inputAccessoryView = self.inputAccessoryViewForKeyboard + } + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: toolbarViewDelegate.mediaManager()))) hostingViewController = VectorHostingController(rootView: composer) hostingViewController.publishHeightChanges = true diff --git a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift index b7d20d38a..8b5327b14 100644 --- a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift @@ -30,8 +30,6 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let viewModel: ComposerViewModel let userSuggestionViewModel = MockUserSuggestionViewModel(initialViewState: UserSuggestionViewState(items: [])) - let userSuggestionSharedContext = UserSuggestionSharedContext(context: userSuggestionViewModel.context, - mediaManager: MXMediaManager()) let bindings = ComposerBindings(focused: false) switch self { @@ -69,7 +67,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { Spacer() Composer(viewModel: viewModel.context, wysiwygViewModel: wysiwygviewModel, - userSuggestionSharedContext: userSuggestionSharedContext, + userSuggestionSharedContext: userSuggestionViewModel.context, resizeAnimationDuration: 0.1, sendMessageAction: { _ in }, showSendMediaActions: { }) diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index 824e04d73..e4317a275 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -23,7 +23,7 @@ struct Composer: View { // MARK: Private @ObservedObject private var viewModel: ComposerViewModelType.Context @ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel - private let userSuggestionSharedContext: UserSuggestionSharedContext + private let userSuggestionSharedContext: UserSuggestionViewModelType.Context private let resizeAnimationDuration: Double private let sendMessageAction: (WysiwygComposerContent) -> Void @@ -223,7 +223,7 @@ struct Composer: View { init( viewModel: ComposerViewModelType.Context, wysiwygViewModel: WysiwygComposerViewModel, - userSuggestionSharedContext: UserSuggestionSharedContext, + userSuggestionSharedContext: UserSuggestionViewModelType.Context, resizeAnimationDuration: Double, sendMessageAction: @escaping (WysiwygComposerContent) -> Void, showSendMediaActions: @escaping () -> Void) { @@ -256,8 +256,7 @@ struct Composer: View { } } if wysiwygViewModel.maximised { - UserSuggestionList(viewModel: userSuggestionSharedContext.context, showBackgroundShadow: false) - .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: userSuggestionSharedContext.mediaManager))) + UserSuggestionList(viewModel: userSuggestionSharedContext, showBackgroundShadow: false) } } .frame(height: composerHeight) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index 38776fbd3..de56c0736 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -30,16 +30,12 @@ struct UserSuggestionCoordinatorParameters { let room: MXRoom } -/// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple -/// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController` -/// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload data. -final class UserSuggestionSharedContext: NSObject { +/// Wrapper around `UserSuggestionViewModelType.Context` to pass it through obj-c. +final class UserSuggestionViewModelContextWrapper: NSObject { let context: UserSuggestionViewModelType.Context - let mediaManager: MXMediaManager - init(context: UserSuggestionViewModelType.Context, mediaManager: MXMediaManager) { + init(context: UserSuggestionViewModelType.Context) { self.context = context - self.mediaManager = mediaManager } } @@ -118,9 +114,8 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { userSuggestionHostingController } - func sharedContext() -> UserSuggestionSharedContext { - UserSuggestionSharedContext(context: userSuggestionViewModel.sharedContext, - mediaManager: parameters.mediaManager) + func sharedContext() -> UserSuggestionViewModelContextWrapper { + UserSuggestionViewModelContextWrapper(context: userSuggestionViewModel.sharedContext) } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift index a7615e43f..9dbebdbf3 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -53,7 +53,7 @@ final class UserSuggestionCoordinatorBridge: NSObject { userSuggestionCoordinator.toPresentable() } - func sharedContext() -> UserSuggestionSharedContext { + func sharedContext() -> UserSuggestionViewModelContextWrapper { userSuggestionCoordinator.sharedContext() } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift index 40318c5df..33aa5bb79 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift @@ -17,6 +17,9 @@ import Foundation protocol UserSuggestionViewModelProtocol { + /// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple + /// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController` + /// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload the data. var sharedContext: UserSuggestionViewModelType.Context { get } var completion: ((UserSuggestionViewModelResult) -> Void)? { get set } } From fb9717599d581ea2fb59c169098c83313b7b8921 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 17:12:54 +0100 Subject: [PATCH 014/149] Bump composer version to 1.4.0 --- Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- .../Composer/LinkAction/Model/ComposerLinkActionModel.swift | 2 ++ .../LinkAction/ViewModel/ComposerLinkActionViewModel.swift | 5 +++++ project.yml | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index a087c8ac3..75c0b6438 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "aa98d9b6e4c3d2c4927190c09c5a7e56d08dbfb0", - "version" : "1.3.0" + "revision" : "ca2f6508bcd8ec0ce239a48347ff155a3a7bef06", + "version" : "1.4.0" } }, { diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift index fdf92cab5..0c3ba03e2 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift @@ -41,6 +41,7 @@ extension ComposerLinkActionViewState { switch linkAction { case .createWithText, .create: return VectorL10n.wysiwygComposerLinkActionCreateTitle case .edit: return VectorL10n.wysiwygComposerLinkActionEditTitle + case .disabled: return "" } } @@ -64,6 +65,7 @@ extension ComposerLinkActionViewState { case .createWithText: return bindings.text.isEmpty case .create: return false case .edit: return !bindings.hasEditedUrl + case .disabled: return false } } } diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift index 9683ac621..367417282 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift @@ -46,6 +46,9 @@ final class ComposerLinkActionViewModel: ComposerLinkActionViewModelType, Compos initialViewState = .init(linkAction: .createWithText, bindings: simpleBindings) case .create: initialViewState = .init(linkAction: .create, bindings: simpleBindings) + case .disabled: + // Note: Unreachable + initialViewState = .init(linkAction: .disabled, bindings: simpleBindings) } super.init(initialViewState: initialViewState) @@ -74,6 +77,8 @@ final class ComposerLinkActionViewModel: ComposerLinkActionViewModelType, Compos .setLink(urlString: state.bindings.linkUrl) ) ) + case .disabled: + break } } } diff --git a/project.yml b/project.yml index ff745767a..bb958c6da 100644 --- a/project.yml +++ b/project.yml @@ -56,7 +56,7 @@ packages: branch: 0.0.1 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 1.3.0 + version: 1.4.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From 7e444e0d26f304cb3d9405b56a47325236d265dc Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 17:15:27 +0100 Subject: [PATCH 015/149] Add changelog --- changelog.d/7442.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7442.change diff --git a/changelog.d/7442.change b/changelog.d/7442.change new file mode 100644 index 000000000..aeb75b57d --- /dev/null +++ b/changelog.d/7442.change @@ -0,0 +1 @@ +Labs: Rich Text Editor: Integrate version 1.4.0 with mention Pills support. From 1c17657865d30a6fa0492d9aad5144307f96037f Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 4 Apr 2023 15:00:52 +0300 Subject: [PATCH 016/149] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 69908f4d4..dea3ab8b7 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.9 -CURRENT_PROJECT_VERSION = 1.10.9 +MARKETING_VERSION = 1.10.10 +CURRENT_PROJECT_VERSION = 1.10.10 From b043666cc7ef01f6be8f13274a75a13f880f4be6 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 5 Apr 2023 09:27:51 +0300 Subject: [PATCH 017/149] Pin Xcode version to 14.2. The app is currently failing ASC validation on using private symbols from Down. --- changelog.d/7476.build | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7476.build diff --git a/changelog.d/7476.build b/changelog.d/7476.build new file mode 100644 index 000000000..09ac016f1 --- /dev/null +++ b/changelog.d/7476.build @@ -0,0 +1 @@ +Pinned used Xcode version to 14.2 as newer version fail ASC validation \ No newline at end of file From 4ff26622361925ef6dacd3dea4513cfa374a4d33 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 7 Apr 2023 14:21:27 +0200 Subject: [PATCH 018/149] Fix: Continue to display pills for matrix.to permalinks if a custom permalinkBaseUrl is set. --- Riot/Modules/MatrixKit/Utils/MXKTools.m | 5 +++-- Riot/Modules/Pills/PillType.swift | 2 +- changelog.d/pr-7482.bugfix | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelog.d/pr-7482.bugfix diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index 3af8ef1fd..c993d0832 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -69,8 +69,9 @@ static NSRegularExpression* permalinkRegex; httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil]; htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil]; linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil]; - - NSString *permalinkPattern = [NSString stringWithFormat:@"%@%@", BuildSettings.clientPermalinkBaseUrl ?: kMXMatrixDotToUrl, kMXKToolsRegexStringForPermalink]; + + // if we have a custom clientPermalinkBaseUrl, we also need to support matrix.to permalinks + NSString *permalinkPattern = [NSString stringWithFormat:@"(?:%@|%@)%@", BuildSettings.clientPermalinkBaseUrl, kMXMatrixDotToUrl, kMXKToolsRegexStringForPermalink]; permalinkRegex = [NSRegularExpression regularExpressionWithPattern:permalinkPattern options:NSRegularExpressionCaseInsensitive error:nil]; }); } diff --git a/Riot/Modules/Pills/PillType.swift b/Riot/Modules/Pills/PillType.swift index 8b90de15b..53b42e0e2 100644 --- a/Riot/Modules/Pills/PillType.swift +++ b/Riot/Modules/Pills/PillType.swift @@ -27,7 +27,7 @@ enum PillType: Codable { extension PillType { private static var regexPermalinkTarget: NSRegularExpression? = { let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl ?? kMXMatrixDotToUrl - let pattern = #"\#(clientBaseUrl)/#/(?:(?:room|user)/)?((?:@|!|#)[^@!#/?\s]*)/?((?:\$)[^\$/?\s]*)?"# + let pattern = #"(?:\#(clientBaseUrl)|\#(kMXMatrixDotToUrl))/#/(?:(?:room|user)/)?((?:@|!|#)[^@!#/?\s]*)/?((?:\$)[^\$/?\s]*)?"# return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) }() diff --git a/changelog.d/pr-7482.bugfix b/changelog.d/pr-7482.bugfix new file mode 100644 index 000000000..842d621b2 --- /dev/null +++ b/changelog.d/pr-7482.bugfix @@ -0,0 +1 @@ +Continue to display pills for matrix.to permalinks if a custom permalinkBaseUrl is set. From 7673372eed85316b16b9ace9f1c276e1c82218d6 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 11 Apr 2023 14:28:48 +0200 Subject: [PATCH 019/149] Bump composer version to 2.0.0 and fix `PillAttachmentViewProvider` --- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../Pills/PillAttachmentViewProvider.swift | 24 +++++++++++-------- .../WysiwygInputToolbarView.swift | 2 +- project.yml | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 75c0b6438..9870eb6da 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "ca2f6508bcd8ec0ce239a48347ff155a3a7bef06", - "version" : "1.4.0" + "revision" : "758f226a92d6726ab626c1e78ecd183bdba77016", + "version" : "2.0.0" } }, { diff --git a/Riot/Modules/Pills/PillAttachmentViewProvider.swift b/Riot/Modules/Pills/PillAttachmentViewProvider.swift index e47331a36..5d57bb3b2 100644 --- a/Riot/Modules/Pills/PillAttachmentViewProvider.swift +++ b/Riot/Modules/Pills/PillAttachmentViewProvider.swift @@ -25,30 +25,29 @@ import UIKit avatarLeading: 2.0, avatarSideLength: 16.0, itemSpacing: 4) - private weak var pillViewFlusher: PillViewFlusher? + private weak var messageTextView: UITextView? + private var pillViewFlusher: PillViewFlusher? { + messageTextView as? PillViewFlusher + } // MARK: - Override override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) { super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location) - // Try to register a flusher for the pills. - if let pillViewFlusher = parentView?.superview as? PillViewFlusher { - self.pillViewFlusher = pillViewFlusher - } else { - MXLog.debug("[PillAttachmentViewProvider]: no handler found, pills will not be flushed properly") - } + // Keep a reference to the parent text view for size adjustments and pills flushing. + messageTextView = parentView?.superview as? UITextView } override func loadView() { super.loadView() guard let textAttachment = self.textAttachment as? PillTextAttachment else { - MXLog.debug("[PillAttachmentViewProvider]: attachment is missing or not of expected class") + MXLog.failure("[PillAttachmentViewProvider]: attachment is missing or not of expected class") return } guard var pillData = textAttachment.data else { - MXLog.debug("[PillAttachmentViewProvider]: attachment misses pill data") + MXLog.failure("[PillAttachmentViewProvider]: attachment misses pill data") return } @@ -64,6 +63,11 @@ import UIKit mediaManager: mainSession?.mediaManager, andPillData: pillData) view = pillView - pillViewFlusher?.registerPillView(pillView) + + if let pillViewFlusher { + pillViewFlusher.registerPillView(pillView) + } else { + MXLog.failure("[PillAttachmentViewProvider]: no handler found, pill will not be flushed properly") + } } } diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index e6b191e2b..cb9a162b3 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -193,7 +193,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp func mention(_ member: MXRoomMember) { self.wysiwygViewModel.setMention(link: MXTools.permalinkToUser(withUserId: member.userId), name: member.displayname, - key: .at) + mentionType: .user) } // MARK: - Private diff --git a/project.yml b/project.yml index bb958c6da..6a207706d 100644 --- a/project.yml +++ b/project.yml @@ -56,7 +56,7 @@ packages: branch: 0.0.1 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 1.4.0 + version: 2.0.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From 4519229349e273085becaf312bbebcf40da8ce8c Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 11 Apr 2023 14:54:55 +0200 Subject: [PATCH 020/149] Rename `textDefaultFont` to `defaultFont` and remove unnecessary definition in `RoomInputToolbarView.h` --- .../Views/RoomInputToolbar/MXKRoomInputToolbarView.h | 5 ++++- .../Views/RoomInputToolbar/MXKRoomInputToolbarView.m | 2 +- Riot/Modules/Room/RoomViewController.swift | 8 ++++---- .../Room/Views/InputToolbar/RoomInputToolbarView.h | 2 -- .../Room/Views/InputToolbar/RoomInputToolbarView.m | 4 ++-- .../WYSIWYGInputToolbar/WysiwygInputToolbarView.swift | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h index bc9b8e0b2..e366ae239 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h @@ -382,7 +382,10 @@ typedef enum : NSUInteger */ @property (nonatomic) NSAttributedString *attributedTextMessage; -@property (nonatomic, readonly, nonnull) UIFont *textDefaultFont; +/** + Default font for the message composer. + */ +@property (nonatomic, readonly, nonnull) UIFont *defaultFont; - (void)dismissValidationView:(MXKImageView*)validationView; diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m index 44199cc5b..d05cd9f53 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m @@ -358,7 +358,7 @@ self.textMessage = [NSString stringWithFormat:@"%@%@", self.textMessage, text]; } -- (UIFont *)textDefaultFont +- (UIFont *)defaultFont { return [UIFont systemFontOfSize:15.f]; } diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 00de9de95..3fec13de9 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -32,7 +32,7 @@ extension RoomViewController { if #available(iOS 15.0, *) { newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember, isHighlighted: false, - font: inputToolbarView.textDefaultFont)) + font: inputToolbarView.defaultFont)) } else { newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId) } @@ -40,13 +40,13 @@ extension RoomViewController { } else if roomMember.userId == self.mainSession.myUser.userId { newAttributedString.appendString("/me ") newAttributedString.addAttribute(.font, - value: inputToolbarView.textDefaultFont, + value: inputToolbarView.defaultFont, range: .init(location: 0, length: newAttributedString.length)) } else { if #available(iOS 15.0, *) { newAttributedString.append(PillsFormatter.mentionPill(withRoomMember: roomMember, isHighlighted: false, - font: inputToolbarView.textDefaultFont)) + font: inputToolbarView.defaultFont)) } else { newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId) } @@ -397,7 +397,7 @@ extension RoomViewController: PermalinkReplacer { withSession: session, eventFormatter: eventFormatter, roomState: roomState, - font: inputToolbarView.textDefaultFont) + font: inputToolbarView.defaultFont) } public func restoreMarkdown(in attributedString: NSAttributedString) -> String { diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index 897922832..df71790be 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -136,8 +136,6 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode) */ @property (nonatomic, weak, readonly) UIButton *attachMediaButton; -@property (nonatomic, readonly, nonnull) UIFont *textDefaultFont; - /** Adds a voice message toolbar view to be displayed inside this input toolbar */ diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 9abfde421..2cead382a 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -154,7 +154,7 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; { NSMutableAttributedString *mutableTextMessage = [[NSMutableAttributedString alloc] initWithAttributedString:attributedTextMessage]; [mutableTextMessage addAttributes:@{ NSForegroundColorAttributeName: ThemeService.shared.theme.textPrimaryColor, - NSFontAttributeName: self.textDefaultFont } + NSFontAttributeName: self.defaultFont } range:NSMakeRange(0, mutableTextMessage.length)]; attributedTextMessage = mutableTextMessage; } @@ -181,7 +181,7 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; return self.textView.text; } -- (UIFont *)textDefaultFont +- (UIFont *)defaultFont { if (self.textView.font) { diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index cb9a162b3..f3fc1111b 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -105,7 +105,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } } - override var textDefaultFont: UIFont { + override var defaultFont: UIFont { return UIFont.preferredFont(forTextStyle: .body) } From 77ab08a35757f01e75f992d351a364fd080a223d Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 11 Apr 2023 12:03:24 +0100 Subject: [PATCH 021/149] Enable Rust Crypto for all users --- Riot/Experiments/CryptoSDKFeature.swift | 2 +- RiotTests/SessionCreatorTests.swift | 3 ++- changelog.d/pr-7485.change | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pr-7485.change diff --git a/Riot/Experiments/CryptoSDKFeature.swift b/Riot/Experiments/CryptoSDKFeature.swift index 1bcff8a43..e52fc637b 100644 --- a/Riot/Experiments/CryptoSDKFeature.swift +++ b/Riot/Experiments/CryptoSDKFeature.swift @@ -52,7 +52,7 @@ import MatrixSDKCrypto init( remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared, - localTargetPercentage: Double = 0.5 + localTargetPercentage: Double = 1 ) { self.remoteFeature = remoteFeature self.localFeature = PhasedRolloutFeature( diff --git a/RiotTests/SessionCreatorTests.swift b/RiotTests/SessionCreatorTests.swift index dbc28c728..688313756 100644 --- a/RiotTests/SessionCreatorTests.swift +++ b/RiotTests/SessionCreatorTests.swift @@ -25,8 +25,9 @@ class SessionCreatorTests: XCTestCase { let mockIS = "mock_identity_server" let credentials = MXCredentials(homeServer: "mock_home_server", - userId: "mock_user_id", + userId: "@mock_user_id:localhost", accessToken: "mock_access_token") + credentials.deviceId = "mock_device_id" let client = MXRestClient(credentials: credentials) client.identityServer = mockIS let session = await sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false) diff --git a/changelog.d/pr-7485.change b/changelog.d/pr-7485.change new file mode 100644 index 000000000..582ebf504 --- /dev/null +++ b/changelog.d/pr-7485.change @@ -0,0 +1 @@ +Crypto: Enable Rust Crypto for all users From 770ee576baca400570b986f891cda9335eb0ae1c Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 12 Apr 2023 14:43:52 +0200 Subject: [PATCH 022/149] we can now support more than just mp4 as audio messages --- .../VoiceMessageAttachmentCacheManager.swift | 9 +++++---- .../VoiceMessages/VoiceMessageAudioConverter.swift | 12 ++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 8e31a229c..2cc989d99 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -206,11 +206,12 @@ class VoiceMessageAttachmentCacheManager { } private func convertFileAtPath(_ path: String?, numberOfSamples: Int, identifier: String, semaphore: DispatchSemaphore) { - guard let filePath = path else { + guard let path else { return } - - let fileExtension = filePath.hasSuffix(".mp4") ? "mp4" : "m4a" + + let filePath = URL(fileURLWithPath: path) + let fileExtension = filePath.hasSupportedAudioExtension ? filePath.pathExtension : "m4a" let newURL = temporaryFilesFolderURL.appendingPathComponent(identifier).appendingPathExtension(fileExtension) let conversionCompletion: (Result) -> Void = { result in @@ -252,7 +253,7 @@ class VoiceMessageAttachmentCacheManager { if FileManager.default.fileExists(atPath: newURL.path) { conversionCompletion(Result.success(())) } else { - VoiceMessageAudioConverter.convertToMPEG4AAC(sourceURL: URL(fileURLWithPath: filePath), destinationURL: newURL, completion: conversionCompletion) + VoiceMessageAudioConverter.convertToMPEG4AACIfNeeded(sourceURL: filePath, destinationURL: newURL, completion: conversionCompletion) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift index 996e33b4a..608f14234 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift @@ -39,10 +39,10 @@ struct VoiceMessageAudioConverter { } } - static func convertToMPEG4AAC(sourceURL: URL, destinationURL: URL, completion: @escaping (Result) -> Void) { + static func convertToMPEG4AACIfNeeded(sourceURL: URL, destinationURL: URL, completion: @escaping (Result) -> Void) { DispatchQueue.global(qos: .userInitiated).async { do { - if sourceURL.pathExtension == "mp4" { + if sourceURL.hasSupportedAudioExtension { try FileManager.default.copyItem(atPath: sourceURL.path, toPath: destinationURL.path) } else { try OGGConverter.convertOpusOGGToM4aFile(src: sourceURL, dest: destinationURL) @@ -86,3 +86,11 @@ struct VoiceMessageAudioConverter { } } } + +extension URL { + /// Returns true if the URL has a supported audio extension + var hasSupportedAudioExtension: Bool { + let supportedExtensions = ["mp3", "mp4", "m4a", "wav", "aac"] + return supportedExtensions.contains(pathExtension.lowercased()) + } +} From f0e42c9895bb2d5fee5adb89bc0ca642a1a492b8 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 12 Apr 2023 14:45:59 +0200 Subject: [PATCH 023/149] changelog --- changelog.d/7451.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7451.bugfix diff --git a/changelog.d/7451.bugfix b/changelog.d/7451.bugfix new file mode 100644 index 000000000..3a80a3b90 --- /dev/null +++ b/changelog.d/7451.bugfix @@ -0,0 +1 @@ +Fixed a bug that prevented audio messages that were not .mp4 to be played in the timeline \ No newline at end of file From 165a250c9d6930c34022d374d7566a3295dfc594 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 12 Apr 2023 14:55:59 +0200 Subject: [PATCH 024/149] Unit tests for `insertPills` and `markdownLinks` --- Riot/Modules/Pills/PillsFormatter.swift | 37 +++++----- RiotTests/PillsFormatterTests.swift | 90 ++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 20 deletions(-) diff --git a/Riot/Modules/Pills/PillsFormatter.swift b/Riot/Modules/Pills/PillsFormatter.swift index 675e824ba..1b6256835 100644 --- a/Riot/Modules/Pills/PillsFormatter.swift +++ b/Riot/Modules/Pills/PillsFormatter.swift @@ -86,7 +86,7 @@ class PillsFormatter: NSObject { eventFormatter: MXKEventFormatter, roomState: MXRoomState, font: UIFont) -> NSAttributedString { - let matches = markdownUrls(in: markdownString) + let matches = markdownLinks(in: markdownString) // If we have some matches, replace permalinks by a pill version. guard !matches.isEmpty else { return markdownString } @@ -100,9 +100,9 @@ class PillsFormatter: NSObject { let mutable = NSMutableAttributedString(attributedString: markdownString) - matches.reversed().forEach { (url: URL, label: String, range: NSRange) in - if let attachmentString = pillProvider.pillTextAttachmentString(forUrl: url, withLabel: label) { - mutable.replaceCharacters(in: range, with: attachmentString) + matches.reversed().forEach { + if let attachmentString = pillProvider.pillTextAttachmentString(forUrl: $0.url, withLabel: $0.label) { + mutable.replaceCharacters(in: $0.range, with: attachmentString) } } @@ -214,21 +214,15 @@ class PillsFormatter: NSObject { // MARK: - Private Methods @available (iOS 15.0, *) extension PillsFormatter { - - static func attributedStringWithAttachment(_ attachment: PillTextAttachment, link: URL?, font: UIFont) -> NSAttributedString { - let string = NSMutableAttributedString(attachment: attachment) - string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length)) - if let url = link { - string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length)) - } - return string + struct MarkdownLinkResult: Equatable { + let url: URL + let label: String + let range: NSRange } -} -@available(iOS 15.0, *) -private extension PillsFormatter { - static func markdownUrls(in attributedString: NSAttributedString) -> [(url: URL, label: String, range: NSRange)] { + static func markdownLinks(in attributedString: NSAttributedString) -> [MarkdownLinkResult] { // Create a regexp that detects markdown links. + // Pattern source: https://gist.github.com/hugocf/66d6cd241eff921e0e02 let pattern = "\\[([^\\]]+)\\]\\(([^\\)\"\\s]+)(?:\\s+\"(.*)\")?\\)" guard let regExp = try? NSRegularExpression(pattern: pattern) else { return [] } @@ -248,10 +242,19 @@ private extension PillsFormatter { } if let url = URL(string: url) { - return (url: url, label: label, range: match.range) + return MarkdownLinkResult(url: url, label: label, range: match.range) } else { return nil } } } + + static func attributedStringWithAttachment(_ attachment: PillTextAttachment, link: URL?, font: UIFont) -> NSAttributedString { + let string = NSMutableAttributedString(attachment: attachment) + string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length)) + if let url = link { + string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length)) + } + return string + } } diff --git a/RiotTests/PillsFormatterTests.swift b/RiotTests/PillsFormatterTests.swift index 573fd234c..a52011776 100644 --- a/RiotTests/PillsFormatterTests.swift +++ b/RiotTests/PillsFormatterTests.swift @@ -29,12 +29,14 @@ private enum Inputs { static let aliceMemberAway = FakeMXRoomMember(displayname: aliceAwayDisplayname, avatarUrl: aliceNewAvatarUrl, userId: "@alice:matrix.org") static let alicePermalink = "https://matrix.to/#/@alice:matrix.org" static let mentionToAlice = NSAttributedString(string: aliceDisplayname, attributes: [.link: URL(string: alicePermalink)!]) - static let markdownLinkToAlice = "[Alice](\(alicePermalink))" + static let markdownLinkToAlice = "[\(aliceDisplayname)](\(alicePermalink))" static let bobUserId = "@bob:matrix.org" static let bobDisplayname = "Bob" static let bobAvatarUrl = "mxc://matrix.org/VyNYBgahazAzUuOeZETtQ" static let bobMember = FakeMXRoomMember(displayname: bobDisplayname, avatarUrl: bobAvatarUrl, userId: bobUserId) + static let bobPermalink = "https://matrix.to/#/@bob:matrix.org" + static let markdownLinkToBob = "[\(bobDisplayname)](\(bobPermalink))" static let anotherUserId = "@another.user:matrix.org" static let anotherUserPermalink = "https://matrix.to/#/@another.user:matrix.org" @@ -310,7 +312,7 @@ class PillsFormatterTests: XCTestCase { case .room(let userId): XCTAssertEqual(userId, Inputs.roomId) switch pillTextAttachmentData.items.first { - case .asset(let assetName, let parameters): + case .asset(let assetName, _): XCTAssertEqual(assetName, "link_icon") default: XCTFail("First pill item should be the asset") @@ -436,7 +438,7 @@ class PillsFormatterTests: XCTestCase { XCTAssertEqual(roomId, Inputs.anotherRoomId) XCTAssertEqual(messageId, Inputs.messageEventId) switch pillTextAttachmentData.items.first { - case .asset(let name, let parameters): + case .asset(let name, _): XCTAssertEqual(name, "link_icon") default: XCTFail("First pill item should be the asset") @@ -445,6 +447,79 @@ class PillsFormatterTests: XCTestCase { XCTFail("Pill should be of type .message") } } + + func testInsertPillInMarkdownString() { + let message = "Hello \(Inputs.markdownLinkToBob)" + let messageWithPills = insertPillsInMarkdownString(message) + XCTAssertTrue(messageWithPills.attribute(.attachment, at: 6, effectiveRange: nil) is PillTextAttachment) + let pillTextAttachment = messageWithPills.attribute(.attachment, at: 6, effectiveRange: nil) as? PillTextAttachment + XCTAssertEqual(pillTextAttachment?.data?.displayText, Inputs.bobDisplayname) + } + + func testInsertMultiplePillsInMarkdownString() { + let message = "Hello \(Inputs.markdownLinkToBob) and \(Inputs.markdownLinkToAlice)" + let messageWithPills = insertPillsInMarkdownString(message) + let bobPillTextAttachment = messageWithPills.attribute(.attachment, at: 6, effectiveRange: nil) as? PillTextAttachment + XCTAssertEqual(bobPillTextAttachment?.data?.displayText, Inputs.bobDisplayname) + + let alicePillTextAttachment = messageWithPills.attribute(.attachment, at: 12, effectiveRange: nil) as? PillTextAttachment + XCTAssertEqual(alicePillTextAttachment?.data?.displayText, Inputs.aliceDisplayname) + // No self highlight + XCTAssert(alicePillTextAttachment?.data?.isHighlighted == false) + } + + func testMarkdownLinkToUnknownUserIsNotPillified() { + let message = "Hello [Unknown user](https://matrix.to/#/@unknown:matrix.org)" + let messageWithPills = insertPillsInMarkdownString(message) + XCTAssertFalse(messageWithPills.attribute(.attachment, at: 6, effectiveRange: nil) is PillTextAttachment) + } + + func testMarkdownSingleLinkDetection() { + let message = NSAttributedString(string: "Hello \(Inputs.markdownLinkToAlice)") + let expected = [ + PillsFormatter.MarkdownLinkResult(url: URL(string: Inputs.alicePermalink)!, + label: Inputs.aliceDisplayname, + range: NSRange(location: 6, length: Inputs.markdownLinkToAlice.count)) + ] + + XCTAssertEqual( + PillsFormatter.markdownLinks(in: message), + expected + ) + } + + func testMarkdownMultipleLinksDetection() { + let message = NSAttributedString(string: "Hello \(Inputs.markdownLinkToAlice) and \(Inputs.markdownLinkToBob)") + let expected = [ + PillsFormatter.MarkdownLinkResult(url: URL(string: Inputs.alicePermalink)!, + label: Inputs.aliceDisplayname, + range: NSRange(location: 6, length: Inputs.markdownLinkToAlice.count)), + PillsFormatter.MarkdownLinkResult(url: URL(string: Inputs.bobPermalink)!, + label: Inputs.bobDisplayname, + range: NSRange(location: 6 + Inputs.markdownLinkToAlice.count + 5, + length: Inputs.markdownLinkToBob.count)) + ] + + XCTAssertEqual( + PillsFormatter.markdownLinks(in: message), + expected + ) + } + + func testBrokenMarkdownLinkIsNotDetected() { + let brokenMarkdownMessages = [ + NSAttributedString(string: "Hello [Alice](https://matrix.to/#/@alice:matrix.org"), + NSAttributedString(string: "Hello [Alice]https://matrix.to/#/@alice:matrix.org)"), + NSAttributedString(string: "Hello [Alice(https://matrix.to/#/@alice:matrix.org)"), + NSAttributedString(string: "Hello Alice](https://matrix.to/#/@alice:matrix.org)"), + NSAttributedString(string: "Hello [Alice]](https://matrix.to/#/@alice:matrix.org)"), + NSAttributedString(string: "Hello (https://matrix.to/#/@alice:matrix.org)"), + ] + + for message in brokenMarkdownMessages { + XCTAssertTrue(PillsFormatter.markdownLinks(in: message).isEmpty) + } + } } @available(iOS 15.0, *) @@ -604,6 +679,15 @@ private extension PillsFormatterTests { return messageWithPills } + private func insertPillsInMarkdownString(_ markdownString: String) -> NSAttributedString { + let message = NSAttributedString(string: markdownString) + let session = FakeMXSession(myUserId: Inputs.aliceUserId) + return PillsFormatter.insertPills(in: message, + withSession: session, + eventFormatter: EventFormatter(matrixSession: session), + roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()), + font: UIFont.systemFont(ofSize: 15.0)) + } } // MARK: - Mock objects From 453d69704bda310cfecc40aec39c636e75902081 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 12 Apr 2023 14:56:33 +0200 Subject: [PATCH 025/149] Update changelog --- changelog.d/7442.change | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/7442.change b/changelog.d/7442.change index aeb75b57d..f8ae96d5b 100644 --- a/changelog.d/7442.change +++ b/changelog.d/7442.change @@ -1 +1 @@ -Labs: Rich Text Editor: Integrate version 1.4.0 with mention Pills support. +Labs: Rich Text Editor: Integrate version 2.0.0 with mention Pills support. From 7e87e0a2e56bcf3e58659bc0b697b47fa30be30f Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 12 Apr 2023 16:53:25 +0100 Subject: [PATCH 026/149] changelog.d: Upgrade MatrixSDK version ([v0.26.7](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.7)). --- Config/AppVersion.xcconfig | 4 ++-- Podfile | 2 +- changelog.d/x-nolink-0.change | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/x-nolink-0.change diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 69908f4d4..dea3ab8b7 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.9 -CURRENT_PROJECT_VERSION = 1.10.9 +MARKETING_VERSION = 1.10.10 +CURRENT_PROJECT_VERSION = 1.10.10 diff --git a/Podfile b/Podfile index ec141c86d..f716233ae 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,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.26.6' +$matrixSDKVersion = '= 0.26.7' # $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..fbe0ca3c4 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixSDK version ([v0.26.7](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.7)). \ No newline at end of file From 05614210f0c611e10658316f63cdcad0cc99b357 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 12 Apr 2023 16:53:26 +0100 Subject: [PATCH 027/149] version++ --- CHANGES.md | 8 ++++++++ changelog.d/pr-7485.change | 1 - changelog.d/x-nolink-0.change | 1 - 3 files changed, 8 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/pr-7485.change delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index 033842289..af7fdc039 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +## Changes in 1.10.10 (2023-04-12) + +🙌 Improvements + +- Crypto: Enable Rust Crypto for all users ([#7485](https://github.com/vector-im/element-ios/pull/7485)) +- Upgrade MatrixSDK version ([v0.26.7](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.7)). + + ## Changes in 1.10.9 (2023-04-04) 🙌 Improvements diff --git a/changelog.d/pr-7485.change b/changelog.d/pr-7485.change deleted file mode 100644 index 582ebf504..000000000 --- a/changelog.d/pr-7485.change +++ /dev/null @@ -1 +0,0 @@ -Crypto: Enable Rust Crypto for all users diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index fbe0ca3c4..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixSDK version ([v0.26.7](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.7)). \ No newline at end of file From a7484a9c98725e331bbc817882f12931da1d184a Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 12 Apr 2023 17:38:38 +0100 Subject: [PATCH 028/149] finish version++ --- Podfile.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index fcb75095b..c4d534f96 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -39,20 +39,20 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.26.6): - - MatrixSDK/Core (= 0.26.6) - - MatrixSDK/Core (0.26.6): + - MatrixSDK (0.26.7): + - MatrixSDK/Core (= 0.26.7) + - MatrixSDK/Core (0.26.7): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - - MatrixSDKCrypto (= 0.3.2) + - MatrixSDKCrypto (= 0.3.3) - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.26.6): + - MatrixSDK/JingleCallStack (0.26.7): - JitsiMeetSDKLite (= 7.0.1-lite) - MatrixSDK/Core - - MatrixSDKCrypto (0.3.2) + - MatrixSDKCrypto (0.3.3) - OLMKit (3.2.12): - OLMKit/olmc (= 3.2.12) - OLMKit/olmcpp (= 3.2.12) @@ -102,8 +102,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.26.6) - - MatrixSDK/JingleCallStack (= 0.26.6) + - MatrixSDK (= 0.26.7) + - MatrixSDK/JingleCallStack (= 0.26.7) - OLMKit - PostHog (~> 2.0.0) - ReadMoreTextView (~> 3.0.1) @@ -187,8 +187,8 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 8179c184d819782282f47dab16ce6c2b68ef8a74 - MatrixSDKCrypto: 7073c382c484cb8ba7dba0a83e112ead96d3bbfd + MatrixSDK: 1de7cd06bef00fabf5693eabcdcdbf2aa1978063 + MatrixSDKCrypto: 427dbb126a3e3f97cadf9fc407abf17d365b4b39 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -208,6 +208,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 54848168ab5303c9126626395886cd85f27a44b3 +PODFILE CHECKSUM: c063d05ddb39617ab9a259c4c9c6b57da2e6d8b6 COCOAPODS: 1.11.3 From 3fea9fade2858054b634bb8813bb8335b779fbcb Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 12 Apr 2023 17:38:47 +0100 Subject: [PATCH 029/149] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index dea3ab8b7..5f74246b2 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.10 -CURRENT_PROJECT_VERSION = 1.10.10 +MARKETING_VERSION = 1.10.11 +CURRENT_PROJECT_VERSION = 1.10.11 From 152ddeaf94d90fc032db5b486ee2f174dbecd1e4 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 13 Apr 2023 17:40:58 +0200 Subject: [PATCH 030/149] Fix user suggestion list item height on iOS 16+ --- .../View/UserSuggestionList.swift | 33 +++++++++++++++++-- changelog.d/7492.bugfix | 1 + 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7492.bugfix diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 9c32b892f..e509a58b3 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -23,6 +23,15 @@ struct UserSuggestionList: View { static let lineSpacing: CGFloat = 10.0 static let maxHeight: CGFloat = 300.0 static let maxVisibleRows = 4 + + /* + As of iOS 16.0, SwiftUI's List uses `UICollectionView` instead + of `UITableView` internally, this value is an adjustment to apply + to the list items in order to be as close as possible as the + `UITableView` display. + */ + @available (iOS 16.0, *) + static let collectionViewPaddingCorrection: CGFloat = -5.0 } // MARK: - Properties @@ -72,8 +81,7 @@ struct UserSuggestionList: View { displayName: item.displayName, userId: item.id ) - .padding(.bottom, Constants.listItemPadding) - .padding(.top, viewModel.viewState.items.first?.id == item.id ? Constants.listItemPadding + Constants.topPadding : Constants.listItemPadding) + .modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id)) } } .listStyle(PlainListStyle()) @@ -82,6 +90,27 @@ struct UserSuggestionList: View { contentHeightForRowCount(viewModel.viewState.items.count)))) .id(UUID()) // Rebuild the whole list on item changes. Fixes performance issues. } + + private struct ListItemPaddingModifier: ViewModifier { + private let isFirst: Bool + + init(isFirst: Bool) { + self.isFirst = isFirst + } + + func body(content: Content) -> some View { + var topPadding: CGFloat = isFirst ? Constants.listItemPadding + Constants.topPadding : Constants.listItemPadding + var bottomPadding: CGFloat = Constants.listItemPadding + if #available(iOS 16.0, *) { + topPadding += Constants.collectionViewPaddingCorrection + bottomPadding += Constants.collectionViewPaddingCorrection + } + + return content + .padding(.top, topPadding) + .padding(.bottom, bottomPadding) + } + } } private struct BackgroundView: View { diff --git a/changelog.d/7492.bugfix b/changelog.d/7492.bugfix new file mode 100644 index 000000000..a9ff595c9 --- /dev/null +++ b/changelog.d/7492.bugfix @@ -0,0 +1 @@ +Fix user suggestion list item height on iOS 16+ From 09245a0b5c6c6f3049a4a268ffd4e57f5038e0e6 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sun, 9 Apr 2023 09:09:34 +0000 Subject: [PATCH 031/149] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (8 of 8 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/zh_Hant/ --- Riot/Assets/zh_Hant.lproj/InfoPlist.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/zh_Hant.lproj/InfoPlist.strings b/Riot/Assets/zh_Hant.lproj/InfoPlist.strings index e236e4a3d..30c871e37 100644 --- a/Riot/Assets/zh_Hant.lproj/InfoPlist.strings +++ b/Riot/Assets/zh_Hant.lproj/InfoPlist.strings @@ -1,8 +1,8 @@ // Permissions usage explanations "NSCameraUsageDescription" = "給予相機權限會用來進行視訊通話或是拍攝並上傳照片與影片。"; -"NSPhotoLibraryUsageDescription" = "同意使用圖片的權限會用來上傳您圖庫的照片與影片。"; -"NSMicrophoneUsageDescription" = "Element 需要麥克風的權限來接受通話、拍攝影片以及錄製語音訊息。"; -"NSContactsUsageDescription" = "他們會與您的身分伺服器共享以找到您在Matrix上的聯絡人。"; +"NSPhotoLibraryUsageDescription" = "請允許存取「照片」,來上傳圖庫當中的照片或影片。"; +"NSMicrophoneUsageDescription" = "Element 需要麥克風的權限來通話、拍攝影片以及錄製語音訊息。"; +"NSContactsUsageDescription" = "會將此資訊分享給您的身分伺服器,以幫助您尋找 Matrix 聯絡人。"; "NSLocationAlwaysAndWhenInUseUsageDescription" = "當您與其他人分享您的位置,Element 需要權限將位置顯示在地圖上。"; "NSLocationWhenInUseUsageDescription" = "當您與其他人分享您的位置,Element 需要權限將位置顯示在地圖上。"; "NSFaceIDUsageDescription" = "您可以使用 Face ID 來登入您的應用程式。"; From 596246cd60fbce47122fe3f142e419401c228588 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 14 Apr 2023 17:35:25 +0200 Subject: [PATCH 032/149] Fix: add missing foreground color attribute --- Riot/Utils/EventFormatter.m | 3 ++- changelog.d/pr-7501.bugfix | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/pr-7501.bugfix diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 5ea1d5f2d..278d902a5 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -103,7 +103,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; @"event_id": event.eventId ?: @"unknown" }); string = [[NSAttributedString alloc] initWithString:[VectorL10n noticeErrorUnformattableEvent] attributes:@{ - NSFontAttributeName: [self encryptedMessagesTextFont] + NSFontAttributeName: [self encryptedMessagesTextFont], + NSForegroundColorAttributeName: [self encryptingTextColor] }]; } } diff --git a/changelog.d/pr-7501.bugfix b/changelog.d/pr-7501.bugfix new file mode 100644 index 000000000..2d6150507 --- /dev/null +++ b/changelog.d/pr-7501.bugfix @@ -0,0 +1 @@ +Add a foreground color attribute for the unformattable event error message. From 326cd1e2d709fad5bb1ff7e316ac884d268f8804 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 21 Mar 2023 18:13:00 +0000 Subject: [PATCH 033/149] Translated using Weblate (German) Currently translated at 100.0% (2391 of 2391 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 27481e778..7461173c6 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2742,3 +2742,9 @@ // MARK: - Launch loading "launch_loading_generic" = "Synchronisiere deine Unterhaltungen"; +"pill_message_in" = "Nachricht in %@"; +"pill_message_from" = "Nachricht von %@"; +"pill_message" = "Nachricht"; + +// Pills +"pill_room_fallback_display_name" = "Space/Raum"; From 236d1cbc851498178820d07deb65c25f9063ed7e Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Tue, 21 Mar 2023 14:29:13 +0000 Subject: [PATCH 034/149] Translated using Weblate (Chinese (Traditional)) Currently translated at 99.9% (2390 of 2391 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hant/ --- Riot/Assets/zh_Hant.lproj/Vector.strings | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index 91fc0beee..00f561e7e 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -622,7 +622,7 @@ "store_full_description" = "Element 是一套新型的通訊和協作應用程式,它提供下列功能:\n\n1. 您可以自行掌控隱私\n2. 可以與 Matrix 網路中的任何人進行通訊,甚至可以與 Slack 等應用程式整合\n3. 保護您免受廣告、資料探勘、後門和封閉平台的侵害\n4. 透過端到端加密和交叉簽署來驗證彼此,互相確保安全\n\nElement 是去中心化的開源軟體,因此與其他通訊和協作應用程式完全不同。\n\nElement 允許您自行架設(或選擇託管)伺服器,使您可針對隱私權,所有權以及對資料和對話內容的完整控制權。您可以連線到所有開放的網路,所以您不是只能與其他 Element 使用者聊天。而且還非常安全。\n\nElement 之所以能夠做到所有這些目標,是因為它使用 Matrix(一套開放、去中心化的通訊標準)運作。\n\nElement 讓您可以自行選擇要將對話放在哪一台伺服器來讓您可自行控制自己的訊息和資料。在 Element 應用程式中,您可以選擇以不同方式託管您的訊息:\n\n1. 在 matrix.org 公開伺服器註冊免費帳號\n2. 使用自行架設的硬體主機上的伺服器來註冊帳號\n3. 訂閱 Element Matrix Services 代管平台,註冊自己的伺服器\n\n為什麼要選擇 Element?\n\n自己擁有自己資料:由您決定將資料與訊息保留在何處。您自己擁有並管理這些資料,而不用讓某些「超大型企業」來探勘您的資料,或將資料提供給第三方。\n\n開放的通訊與協作機制:您可以與 Matrix 網路中的任何人聊天,不管他們使用的是 Element 還是其他 Matrix 應用程式,甚至他們也可以使用像 Slack 、IRC 或 XMPP 之類的其他通訊系統。\n\n超級安全:真正的端對端加密(只有對話中的人才能解開訊息內容),並進行交叉簽署以驗證對話參與者的設備。\n\n完整的通訊:傳訊息、進行語音或視訊通話、分享檔案、畫面,還有大量整合、機器人與小工具。建立聊天室、社群,保持聯繫並完成工作。\n\n無論您身在何處都可保持聯繫:無論您身在何處,都可以透過 https://element.io/app 在所有裝置與網路取得完全同步的訊息記錄來保持聯繫。"; // String for App Store "store_short_description" = "去中心化的安全通訊/VoIP 軟體"; -"settings_three_pids_management_information_part1" = "在此管理您用作登入或恢復帳號的電子郵件地址或電話號碼。您也可控制誰可以用這些資料找到您 "; +"settings_three_pids_management_information_part1" = "您可以在 "; "external_link_confirmation_message" = "此連結 %@ 將帶您到另一網頁:%@\n\n確定要前往嗎?"; "external_link_confirmation_title" = "請確認此連結"; "media_type_accessibility_sticker" = "貼圖"; @@ -888,7 +888,7 @@ "settings_default" = "預設通知"; "settings_notifications_disabled_alert_title" = "已停用通知"; "settings_device_notifications" = "裝置通知"; -"settings_three_pids_management_information_part3" = "。"; +"settings_three_pids_management_information_part3" = "管理您用作登入或恢復帳號的電子郵件地址或電話號碼。您也可控制誰可以用這些資料找到您。"; "room_join_group_call" = "加入"; "room_place_voice_call" = "語音通話"; "room_accessibility_video_call" = "視訊通話"; @@ -2733,7 +2733,7 @@ // Social login -"social_login_list_title_continue" = "繼續"; +"social_login_list_title_continue" = "使用下列方式繼續"; "network_offline_message" = "您已離線,請確認您的網路連線。"; "network_offline_title" = "您已離線"; "event_formatter_jitsi_widget_removed_by_you" = "您已刪除 VoIP 會議"; @@ -2856,3 +2856,9 @@ // MARK: - Launch loading "launch_loading_generic" = "正在同步對話"; +"pill_message_in" = "在 %@ 的訊息"; +"pill_message_from" = "來自 %@ 的訊息"; +"pill_message" = "訊息"; + +// Pills +"pill_room_fallback_display_name" = "聊天空間/聊天室"; From 3b4603b6e13005ed933ce74a91c894b680784a51 Mon Sep 17 00:00:00 2001 From: random Date: Tue, 21 Mar 2023 14:57:22 +0000 Subject: [PATCH 035/149] Translated using Weblate (Italian) Currently translated at 100.0% (2391 of 2391 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 930ace277..50678a2e4 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2708,3 +2708,9 @@ // MARK: - Launch loading "launch_loading_generic" = "Sincronizzazione delle tue conversazioni"; +"pill_message_in" = "Messaggio in %@"; +"pill_message_from" = "Messaggio da %@"; +"pill_message" = "Messaggio"; + +// Pills +"pill_room_fallback_display_name" = "Spazio/Stanza"; From d3bb43e0dec7686977dfbcafce48d2c2e5a97ff1 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 21 Mar 2023 20:43:38 +0000 Subject: [PATCH 036/149] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2391 of 2391 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 7b321d90b..f63ae10c6 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2933,3 +2933,9 @@ // MARK: - Launch loading "launch_loading_generic" = "Синхронізація ваших розмов"; +"pill_message_in" = "Повідомлення у %@"; +"pill_message_from" = "Повідомлення від %@"; +"pill_message" = "Повідомлення"; + +// Pills +"pill_room_fallback_display_name" = "Простір/кімната"; From 57991f27b895af4968770e23520023ce28efcbb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 22 Mar 2023 07:30:49 +0000 Subject: [PATCH 037/149] Translated using Weblate (Estonian) Currently translated at 100.0% (2391 of 2391 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index a3750917e..6ff1adf2e 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2680,3 +2680,9 @@ // MARK: - Launch loading "launch_loading_generic" = "Sinu vestlused on sünkroniseerimisel"; +"pill_message_in" = "Sõnum jututoas %@"; +"pill_message_from" = "Sõnum kasutajalt %@"; +"pill_message" = "Sõnum"; + +// Pills +"pill_room_fallback_display_name" = "Kogukond/jututuba"; From a553d21d1f0095bd58f7c48c073316d1c85db53b Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 21 Mar 2023 21:51:11 +0000 Subject: [PATCH 038/149] Translated using Weblate (Slovak) Currently translated at 100.0% (2391 of 2391 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 7f2255f4b..7a81166c1 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2931,3 +2931,9 @@ // MARK: - Launch loading "launch_loading_generic" = "Synchronizácia vašich konverzácií"; +"pill_message_in" = "Správa v %@"; +"pill_message_from" = "Správa od %@"; +"pill_message" = "Správa"; + +// Pills +"pill_room_fallback_display_name" = "Priestor/miestnosť"; From ad7060bc92ae48be1dfe250c4bc5aff2a811eae9 Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 23 Mar 2023 09:38:53 +0000 Subject: [PATCH 039/149] Translated using Weblate (Indonesian) Currently translated at 100.0% (2391 of 2391 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, 6 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 5990734f3..b5077050d 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2935,3 +2935,9 @@ "device_verification_self_verify_open_on_other_device_information" = "Anda harus memverifikasi sesi ini supaya dapat membaca riwayat pesan aman Anda.\n\nBuka Element di salah satu perangkat Anda yang lain dan ikuti petunjuknya."; "device_verification_self_verify_open_on_other_device_title" = "Buka %@ di perangkat Anda yang lain"; "room_creation_only_one_email_invite" = "Amda hanya dapat mengundang satu surel satu-satu"; +"pill_message_in" = "Pesan di %@"; +"pill_message_from" = "Pesan dari %@"; +"pill_message" = "Pesan"; + +// Pills +"pill_room_fallback_display_name" = "Space/Ruangan"; From 6b886bbdc2c643733c9776399d80e4cb5842b874 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 28 Mar 2023 20:45:52 +0000 Subject: [PATCH 040/149] Translated using Weblate (German) Currently translated at 99.9% (2393 of 2394 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 7461173c6..6bd0096b3 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2748,3 +2748,8 @@ // Pills "pill_room_fallback_display_name" = "Space/Raum"; +"key_verification_self_verify_security_upgrade_alert_message" = "Verschlüsselte Kommunikation wurde mit der neuesten Aktualisierung verbessert. Bitte verifiziere deine Geräte erneut."; + +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "App aktualisiert"; From edceacbf3317109382ff435d8906b0b676687d0a Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Wed, 29 Mar 2023 07:48:34 +0000 Subject: [PATCH 041/149] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2394 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hant/ --- Riot/Assets/zh_Hant.lproj/Vector.strings | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index 00f561e7e..499d51111 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -2743,7 +2743,7 @@ // Events formatter with you "event_formatter_widget_added_by_you" = "您新增了小工具:%@"; "event_formatter_message_deleted" = "訊息已刪除"; -"event_formatter_group_call_incoming" = "%@ 在 %@"; +"event_formatter_group_call_incoming" = "%@ (來自 %@)"; "event_formatter_call_decline" = "拒絕"; "event_formatter_call_connection_failed" = "連線失敗"; "event_formatter_call_missed_video" = "未接聽的視訊通話"; @@ -2862,3 +2862,9 @@ // Pills "pill_room_fallback_display_name" = "聊天空間/聊天室"; +"key_verification_self_verify_security_upgrade_alert_message" = "最新版本中已改進加密訊息傳輸功能,請重新驗證您的裝置。"; + +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "已更新程式"; +"settings_acceptable_use" = "可接受使用政策"; From 8856faf147184df6c6a6e68b5dd2661ccf261da0 Mon Sep 17 00:00:00 2001 From: random Date: Wed, 29 Mar 2023 12:32:22 +0000 Subject: [PATCH 042/149] Translated using Weblate (Italian) Currently translated at 100.0% (2394 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 50678a2e4..d07f51b64 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2714,3 +2714,9 @@ // Pills "pill_room_fallback_display_name" = "Spazio/Stanza"; +"key_verification_self_verify_security_upgrade_alert_message" = "La messaggistica sicura è stata migliorata con l'aggiornamento più recente. Ri-verifica il tuo dispositivo."; + +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "App aggiornata"; +"settings_acceptable_use" = "Politica di utilizzo accettabile"; From b5b2ff0584eaa65d5e1387e4d479cedf338e51ff Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 28 Mar 2023 18:01:13 +0000 Subject: [PATCH 043/149] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2394 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index f63ae10c6..65ff758cb 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2939,3 +2939,9 @@ // Pills "pill_room_fallback_display_name" = "Простір/кімната"; +"key_verification_self_verify_security_upgrade_alert_message" = "В останньому оновленні було вдосконалено захищений обмін повідомленнями. Перевірте свій пристрій ще раз."; + +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "Застосунок оновлено"; +"settings_acceptable_use" = "Політика прийнятного користування"; From ff950fb7a9fa230145952ba13195d50fc67d6a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 29 Mar 2023 07:09:01 +0000 Subject: [PATCH 044/149] Translated using Weblate (Estonian) Currently translated at 100.0% (2394 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 6ff1adf2e..12a8f9557 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2686,3 +2686,9 @@ // Pills "pill_room_fallback_display_name" = "Kogukond/jututuba"; +"settings_acceptable_use" = "Vastuvõetava kasutamise põhimõtted"; +"key_verification_self_verify_security_upgrade_alert_message" = "Turvalisele sõnumivahetusele on lisandunud palju täiendusi. Palun verifitseeri oma seade uuesti."; + +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "Rakendus on uuendatud"; From 5ce8f10a7d6e7dd1dc4a8526ffc24650a9d1bf8e Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 28 Mar 2023 23:49:36 +0000 Subject: [PATCH 045/149] Translated using Weblate (Indonesian) Currently translated at 100.0% (2394 of 2394 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, 6 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index b5077050d..1aea826f3 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2941,3 +2941,9 @@ // Pills "pill_room_fallback_display_name" = "Space/Ruangan"; +"key_verification_self_verify_security_upgrade_alert_message" = "Perpesanan aman telah ditingkatkan dengan pembaruan terkini. Silakan verifikasi ulang perangkat Anda."; + +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "Aplikasi diperbarui"; +"settings_acceptable_use" = "Kebijakan Penggunaan yang Dapat Diterima"; From 561ab9b6d63ea797cfa0bc8a4dc9bba1b68ac103 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sun, 9 Apr 2023 09:49:23 +0000 Subject: [PATCH 046/149] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (50 of 50 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/zh_Hant/ --- Riot/Assets/zh_Hant.lproj/Localizable.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/zh_Hant.lproj/Localizable.strings b/Riot/Assets/zh_Hant.lproj/Localizable.strings index 07a5f408b..9bc6bfb82 100644 --- a/Riot/Assets/zh_Hant.lproj/Localizable.strings +++ b/Riot/Assets/zh_Hant.lproj/Localizable.strings @@ -75,15 +75,15 @@ "USER_MEMBERSHIP_UPDATED" = "%@ 更新了個人資料"; /* A user has change their name to a new name which we don't know */ -"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ 變更了名字"; +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ 變更了名稱"; /** Membership Updates **/ /* A user has change their name to a new name */ -"USER_UPDATED_DISPLAYNAME" = "%@ 把名稱變更為 %@"; +"USER_UPDATED_DISPLAYNAME" = "%@ 將名稱變更為 %@"; /* A user has change their avatar */ -"USER_UPDATED_AVATAR" = "%@ 變更了他們的頭像"; +"USER_UPDATED_AVATAR" = "%@ 變更了大頭照"; /* A user has reacted to a message, but the reaction content is unknown */ "GENERIC_REACTION_FROM_USER" = "%@ 送出了一個反應"; From 75b2a209d73947639c0a83cc61ba08e81152a2f0 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 28 Mar 2023 23:44:02 +0000 Subject: [PATCH 047/149] Translated using Weblate (Slovak) Currently translated at 100.0% (2394 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 7a81166c1..8399cd6ff 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2937,3 +2937,9 @@ // Pills "pill_room_fallback_display_name" = "Priestor/miestnosť"; +"key_verification_self_verify_security_upgrade_alert_message" = "Najnovšou aktualizáciou sa zlepšilo bezpečné zasielanie správ. Overte prosím znova svoje zariadenie."; + +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "Aplikácia bola aktualizovaná"; +"settings_acceptable_use" = "Zásady prijateľného používania"; From 55b2dd1b0b23e448796a59359649ca31d14f0223 Mon Sep 17 00:00:00 2001 From: Vri Date: Fri, 31 Mar 2023 08:07:53 +0000 Subject: [PATCH 048/149] Translated using Weblate (German) Currently translated at 100.0% (2394 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 6bd0096b3..6a4f9c018 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2753,3 +2753,4 @@ // Legacy to Rust security upgrade "key_verification_self_verify_security_upgrade_alert_title" = "App aktualisiert"; +"settings_acceptable_use" = "Nutzungsbedingungen"; From 76cfac7bf98094ee40078f267b2ab24a90a6641d Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 3 Apr 2023 16:23:28 +0000 Subject: [PATCH 049/149] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2394 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hant/ --- Riot/Assets/zh_Hant.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index 499d51111..e16253fdf 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -2474,7 +2474,7 @@ "identity_server_settings_alert_change_title" = "變更身分伺服器"; "identity_server_settings_alert_no_terms" = "您選擇的身分伺服器沒有任何服務條款。僅在您信任服務擁有者時才繼續。"; "identity_server_settings_alert_no_terms_title" = "身分伺服器無使用條款"; -"identity_server_settings_disconnect_info" = "如果您未連線到您的身分伺服器,其他的使用者將無法找到您,您也無法經由電子郵件和電話找到其他使用者。"; +"identity_server_settings_disconnect_info" = "與您的身分伺服器中斷連線後,其他使用者就無法再探索到您,您也不能透過電子郵件地址或電話號碼邀請其他人。"; "identity_server_settings_place_holder" = "輸入一個身分伺服器"; "identity_server_settings_no_is_description" = "您目前未使用身分伺服器。若想要尋找或被您認識的聯絡人找到,請在上方加入伺服器。"; "identity_server_settings_description" = "您正在使用 %@ 來讓其他現有的聯絡人和您能夠找到彼此。"; @@ -2533,7 +2533,7 @@ "settings_discovery_three_pid_details_information_email" = "在此管理讓其他使用者尋找您以及邀請您進入聊天室的電子郵件地址偏好設定。您可以在「帳號」中加入或刪除電子郵件地址。"; "settings_discovery_three_pids_management_information_part1" = "選擇您希望其他使用者用哪一個電子郵件(或電話)聯絡您,以及邀請您進入聊天室。您可以在此清單加入/移除電子郵件(或電話)。 "; "settings_discovery_accept_terms" = "同意身分伺服器的使用條款"; -"settings_discovery_terms_not_signed" = "同意身分伺服器(%@)的使用條款,讓其他人可以用您的電子郵件或電話號碼找到您。"; +"settings_discovery_terms_not_signed" = "需同意身分伺服器(%@)的使用條款,讓其他人可以用電子郵件地址或電話號碼找到您。"; "settings_discovery_no_identity_server" = "您目前未使用身分伺服器。若想要被您認識的聯絡人找到,請加入伺服器。"; "settings_devices_description" = "所有與您通訊的聯絡人都能看到此工作階段的公開名稱"; "settings_key_backup_delete_confirmation_prompt_msg" = "您確定嗎?如果您的金鑰沒有正確備份的話,將會遺失所有加密訊息。"; @@ -2649,7 +2649,7 @@ "settings_room_invitations" = "聊天室邀請"; "settings_messages_containing_at_room" = "@room"; "settings_notify_me_for" = "通知我"; -"settings_mentions_and_keywords" = "僅有被提及與出現關鍵字時"; +"settings_mentions_and_keywords" = "提及與關鍵字"; "settings_notifications_disabled_alert_message" = "如需啟用通知,請前往裝置設定。"; "settings_security" = "安全性"; "settings_confirm_media_size_description" = "開啟此選項後,傳送檔案前,會先向您確認準備傳送的圖片與影片大小。"; From e6c8b663529016cf62c4a52a849c07104969e646 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 3 Apr 2023 18:12:25 +0000 Subject: [PATCH 050/149] Translated using Weblate (Chinese (Traditional)) Currently translated at 99.9% (2392 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hant/ --- Riot/Assets/zh_Hant.lproj/Vector.strings | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index e16253fdf..d19d880cf 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -386,7 +386,7 @@ "room_details_mute_notifs" = "將通知靜音"; "room_details_access_section_no_address_warning" = "要連結一個聊天室,該聊天室必須要有位址"; "room_details_access_section_directory_toggle" = "將此聊天室列入聊天室目錄"; -"room_details_history_section_prompt_msg" = "對可閱讀歷史訊息的使用者的變更,將僅適用於此聊天室的新訊息。現有訊息的顯示狀態將保持不變。"; +"room_details_history_section_prompt_msg" = "對可閱讀歷史訊息的使用者的變更,僅適用於此聊天室的新訊息。現有訊息的顯示狀態將保持不變。"; "room_details_new_address" = "新增位址"; "room_details_new_address_placeholder" = "新增位址(例如 #foo%@)"; "room_details_addresses_invalid_address_prompt_msg" = "%@ 不是有效的別名格式"; @@ -826,7 +826,7 @@ "event_formatter_call_answer" = "接聽"; "event_formatter_call_back" = "回撥"; "event_formatter_call_has_ended" = "通話結束"; -"event_formatter_call_connecting" = "正在連接…"; +"event_formatter_call_connecting" = "連線中…"; "event_formatter_message_edited_mention" = "(已編輯)"; "image_picker_action_library" = "從媒體庫挑選"; @@ -1291,7 +1291,7 @@ // Settings keys // call string -"call_connecting" = "正在連接…"; +"call_connecting" = "連線中…"; "notification_settings_notify_all_other" = "其他訊息/聊天室的通知"; "notification_settings_by_default" = "按預設…"; "notification_settings_suppress_from_bots" = "限制來自機器人的通知"; @@ -1870,7 +1870,7 @@ "home_context_menu_normal_priority" = "一般優先度"; "home_context_menu_low_priority" = "低優先度"; "home_context_menu_unfavourite" = "從我的最愛移除"; -"home_context_menu_favourite" = "我的最愛"; +"home_context_menu_favourite" = "加入我的最愛"; "home_context_menu_unmute" = "解除靜音"; "home_context_menu_mute" = "靜音"; "home_context_menu_notifications" = "通知"; @@ -2576,7 +2576,7 @@ // Sessions list "user_verification_sessions_list_user_trust_level_trusted_title" = "受信任"; -"user_verification_start_additional_information" = "要確定安全,請面對面進行或使用其他方式來通訊。"; +"user_verification_start_additional_information" = "為了確保安全,請面對面進行驗證,或使用其他方式來通訊。"; "user_verification_start_waiting_partner" = "正在等待 %@…"; "user_verification_start_information_part2" = " 雙方裝置上顯示的單次驗證碼。"; "user_verification_start_information_part1" = "為了加強安全性,請確認 "; From 7c5c162bdd9dbccaad0da5af18a45ae347e1b6a8 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Thu, 6 Apr 2023 17:29:25 +0000 Subject: [PATCH 051/149] Translated using Weblate (Chinese (Traditional)) Currently translated at 99.9% (2393 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hant/ --- Riot/Assets/zh_Hant.lproj/Vector.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index d19d880cf..04b8c5dfd 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -2128,7 +2128,7 @@ // User -"key_verification_verified_user_information" = "與此使用者的訊息是端到端加密的,無法被第三方讀取。"; +"key_verification_verified_user_information" = "與此使用者的訊息有端對端加密,無法被第三方讀取。"; "key_verification_verified_this_session_information" = "您現在可以在此裝置上閱讀您的加密訊息,其他使用者也會知道他們能夠信任此裝置。"; "key_verification_verified_new_session_information" = "您現在也可以在新的裝置上閱讀您的加密訊息,其他使用者也會知道他們能夠信任此裝置。"; "key_verification_verified_other_session_information" = "您現在也可以在其他的工作階段閱讀您的加密訊息,其他使用者也會知道他們能夠信任此工作階段。"; @@ -2300,7 +2300,7 @@ "secure_key_backup_setup_intro_use_security_passphrase_info" = "輸入只有您知道的安全密語,並產生備份的金鑰。"; "secure_key_backup_setup_intro_use_security_passphrase_title" = "使用安全密語"; "secure_key_backup_setup_intro_use_security_key_info" = "產生安全金鑰後,請儲存在密碼管理員或保險箱等安全的地方。"; -"secure_key_backup_setup_intro_info" = "透過備份您伺服器上的加密金鑰,來防止失去對您已加密的訊息與資料的存取權。"; +"secure_key_backup_setup_intro_info" = "透過備份您伺服器上的加密金鑰,來防止失去對加密訊息與資料的存取權。"; "service_terms_modal_information_description_integration_manager" = "整合管理員能夠讓您加入第三方服務的功能。"; "service_terms_modal_information_description_identity_server" = "身分伺服器讓您能夠用電話或電子郵件,查詢您的聯絡人是否已經申請帳號。"; "service_terms_modal_information_title_integration_manager" = "整合管理員"; From 92e8d48c82db702a1964912c1589112dcaf9167a Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 5 Apr 2023 16:06:11 +0000 Subject: [PATCH 052/149] Translated using Weblate (Hungarian) Currently translated at 100.0% (2394 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index c3f99464a..a77322f08 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2654,7 +2654,6 @@ "voice_broadcast_stop_alert_title" = "Megszakítod az élő közvetítést?"; "voice_broadcast_buffering" = "Pufferelés…"; "voice_broadcast_time_left" = "%@ van vissza"; - "password_policy_pwd_in_dict_error" = "Ez a jelszó megtalálható a szótárban ezért nem engedélyezett."; "password_policy_weak_pwd_error" = "Ez a jelszó túl gyenge. Legalább 8 karakternek kell lennie és minden típusból legalább egy: nagybetű, kisbetű, szám és speciális karakter."; @@ -2701,7 +2700,6 @@ "poll_history_no_past_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez"; "poll_history_no_active_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez"; "poll_history_loading_text" = "Szavazások megjelenítése"; - "settings_labs_disable_crypto_sdk" = "Rust végpontok közötti titkosítás (kikapcsoláshoz kijelentkezés szükséges)"; "settings_labs_confirm_crypto_sdk" = "Ez a funkció még kísérleti fázisban van. Lehet, hogy nem az elvártnak megfelelően fog működni és előre nem látható következménye lehet. A funkció kikapcsolásához egyszerű ki-, és bejelentkezés szükséges. Használata csak saját felelősségre."; "settings_labs_enable_crypto_sdk" = "Rust végpontok közötti titkosítás"; @@ -2720,3 +2718,25 @@ "device_verification_self_verify_wait_recover_secrets_additional_help" = "Nem férsz hozzá létező munkamenethez, %@?"; "device_verification_self_verify_open_on_other_device_title" = "Nyisd meg ezt: %@ a másik eszközön"; "room_creation_only_one_email_invite" = "E-mail meghívóból egyszerre csak egy küldhető"; +"pill_message_in" = "Üzenet itt: %@"; +"pill_message_from" = "Üzenet tőle: %@"; +"pill_message" = "Üzenet"; + +// Pills +"pill_room_fallback_display_name" = "Tér/Szoba"; +"launch_loading_delay_warning" = "Ez egy kicsit tovább tarthat.\nKöszönjük a türelmet."; + +// MARK: - Launch loading + +"launch_loading_generic" = "Beszélgetések szinkronizálása"; +"key_verification_scan_qr_code_information_new_session" = "Az új munkameneted ellenőrzéséhez irányítsd a kamerádat a másik eszközödön megjelenő QR kódra"; +"key_verification_scan_qr_code_information_other_session" = "A munkameneted ellenőrzéséhez irányítsd a kamerádat a másik eszközödön megjelenő QR kódra"; +"key_verification_scan_qr_code_information_other_device" = "A munkamenet ellenőrzéséhez irányítsd a kamerádat a másik eszközödön megjelenő QR kódra"; +"key_verification_scan_qr_code_information_other_user" = "A munkamenetük ellenőrzéséhez irányítsd a kamerádat az eszközükön megjelenő QR kódra"; +"device_verification_self_verify_open_on_other_device_information" = "Ennek a munkamenetnek az ellenőrzésére szükséged van a régi titkosított üzenetek olvasásához.\n\nNyisd meg az Elementet egy másik eszközödön és kövesd az utasításokat."; +"key_verification_self_verify_security_upgrade_alert_message" = "A biztonságos üzenetküldés a legutolsó fejlesztésekkel frissült. Kérjük ellenőrizzed újra az eszközt."; + +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "Alkalmazás frissítve"; +"settings_acceptable_use" = "Elfogadható felhasználói feltételek"; From 2101d1284f36612441b3ade0892d5d35568a795c Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 5 Apr 2023 17:11:42 +0000 Subject: [PATCH 053/149] Translated using Weblate (Albanian) Currently translated at 99.6% (2386 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ --- Riot/Assets/sq.lproj/Vector.strings | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 4274f5dc5..fa2860895 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -2718,3 +2718,16 @@ "device_verification_self_verify_open_on_other_device_information" = "Lypset të verifikoni këtë sesion, që të mund të lexoni historikun e mesazheve tuaj të siguruar.\n\nHapeni Element-in në një nga pajisjet tuaja të tjera dhe ndiqni udhëzimet."; "device_verification_self_verify_open_on_other_device_title" = "Hapeni %@ në pajisjen tuaj tjetër"; "room_creation_only_one_email_invite" = "Mund të ftoni vetëm një email në herë"; +"pill_message_in" = "Mesazh te %@"; +"pill_message_from" = "Mesazh nga %@"; +"pill_message" = "Mesazh"; + +// Pills +"pill_room_fallback_display_name" = "Hapësirë/Dhomë"; +"key_verification_self_verify_security_upgrade_alert_message" = "Me përditësimin e fundit shkëmbimi i siguruar i mesazheve është përmirësuar. Ju lutemi, riverifikoni pajisjen tuaj."; + +// Legacy to Rust security upgrade + +"key_verification_self_verify_security_upgrade_alert_title" = "Aplikacioni u përditësua"; +"settings_acceptable_use" = "Rregull Përdorimi të Pranueshëm"; +"accessibility_selected" = "përzgjedhur"; From ab4ae24d9674f31d8c40096ee90251c780939d53 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sun, 9 Apr 2023 09:52:37 +0000 Subject: [PATCH 054/149] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2394 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hant/ --- Riot/Assets/zh_Hant.lproj/Vector.strings | 116 +++++++++++------------ 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index 04b8c5dfd..167652a1d 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -98,7 +98,7 @@ "directory_title" = "目錄"; "auth_recaptcha_message" = "這個家伺服器想要確認您不是機器人"; "auth_reset_password_missing_email" = "必須輸入和您帳號綁定的電子郵件地址。"; -"auth_reset_password_missing_password" = "必須輸入一個新密碼。"; +"auth_reset_password_missing_password" = "必須輸入新密碼。"; "auth_reset_password_next_step_button" = "我已經驗證了電子郵件地址"; "auth_reset_password_error_unauthorized" = "電子郵件地址驗證失敗:請確認您已點擊郵件中的連結"; "auth_reset_password_error_not_found" = "您的電子郵件地址似乎並未與這台家伺服器上的任何 Matrix ID 相關聯。"; @@ -384,9 +384,9 @@ "room_details_photo" = "聊天室圖片"; "room_details_room_name" = "聊天室名稱"; "room_details_mute_notifs" = "將通知靜音"; -"room_details_access_section_no_address_warning" = "要連結一個聊天室,該聊天室必須要有位址"; +"room_details_access_section_no_address_warning" = "要連結聊天室,該聊天室必須要有位址"; "room_details_access_section_directory_toggle" = "將此聊天室列入聊天室目錄"; -"room_details_history_section_prompt_msg" = "對可閱讀歷史訊息的使用者的變更,僅適用於此聊天室的新訊息。現有訊息的顯示狀態將保持不變。"; +"room_details_history_section_prompt_msg" = "對可閱讀訊息紀錄的使用者的變更,僅適用於此聊天室的新訊息。現有訊息的顯示狀態將保持不變。"; "room_details_new_address" = "新增位址"; "room_details_new_address_placeholder" = "新增位址(例如 #foo%@)"; "room_details_addresses_invalid_address_prompt_msg" = "%@ 不是有效的別名格式"; @@ -454,7 +454,7 @@ "directory_server_type_homeserver" = "輸入一個家伺服器來列出所有公開聊天室"; "directory_server_placeholder" = "matrix.org"; // Events formatter -"event_formatter_member_updates" = "變更 %tu 成員身分"; +"event_formatter_member_updates" = "%tu 筆成員狀態變更"; "event_formatter_widget_added" = "%@ 小工具已由 %@ 新增"; "event_formatter_widget_removed" = "%@ 小工具已由 %@ 移除"; "event_formatter_jitsi_widget_added" = "VoIP 群組通話已由 %@ 新增"; @@ -526,7 +526,7 @@ "room_message_reply_to_placeholder" = "傳送回覆(未加密)…"; "encrypted_room_message_reply_to_placeholder" = "傳送加密的回覆…"; "room_message_reply_to_short_placeholder" = "傳送回覆…"; -"room_event_action_view_decrypted_source" = "檢視已解密的來源"; +"room_event_action_view_decrypted_source" = "檢視解密的原始碼"; "room_predecessor_link" = "點擊此處以檢視更早以前的訊息。"; "room_replacement_information" = "這個聊天室已被取代,且不再使用。"; "room_replacement_link" = "對話在此繼續。"; @@ -881,9 +881,9 @@ "settings_messages_containing_keywords" = "關鍵字"; "settings_messages_containing_user_name" = "我的使用者名稱"; "settings_messages_containing_display_name" = "我的顯示名稱"; -"settings_encrypted_group_messages" = "已加密的群組訊息"; +"settings_encrypted_group_messages" = "加密的群組訊息"; "settings_group_messages" = "群組訊息"; -"settings_encrypted_direct_messages" = "已加密的私人訊息"; +"settings_encrypted_direct_messages" = "加密的私人訊息"; "settings_direct_messages" = "私人訊息"; "settings_default" = "預設通知"; "settings_notifications_disabled_alert_title" = "已停用通知"; @@ -924,7 +924,7 @@ "room_event_encryption_info_event" = "事件資訊\n"; "room_event_encryption_info_event_user_id" = "使用者 ID\n"; "room_event_encryption_info_event_identity_key" = "Curve25519 身分認證金鑰\n"; -"room_event_encryption_info_event_fingerprint_key" = "已聲請之 Ed25519 指紋金鑰\n"; +"room_event_encryption_info_event_fingerprint_key" = "聲稱的 Ed25519 指紋金鑰\n"; "room_event_encryption_info_event_algorithm" = "演算法\n"; "room_event_encryption_info_event_session_id" = "工作階段 ID\n"; "room_event_encryption_info_event_decryption_error" = "解密錯誤\n"; @@ -993,14 +993,14 @@ "notice_room_ban" = "%@ 已封鎖 %@"; "notice_room_withdraw" = "%@ 已撤回 %@ 的邀請"; "notice_room_reason" = ",原因:%@"; -"notice_avatar_url_changed" = "%@ 已變更大頭照"; -"notice_display_name_set" = "%@ 已將他們的顯示名稱設定為 %@"; -"notice_display_name_changed_from" = "%@ 將自己的顯示名稱從 %@ 改為 %@"; -"notice_display_name_removed" = "%@ 已移除他們的顯示名稱"; -"notice_topic_changed" = "%@ 已經將主題變更為:%@。"; +"notice_avatar_url_changed" = "%@ 變更了大頭照"; +"notice_display_name_set" = "%@ 將顯示名稱設定為 %@"; +"notice_display_name_changed_from" = "%@ 將顯示名稱從 %@ 改為 %@"; +"notice_display_name_removed" = "%@ 移除了顯示名稱"; +"notice_topic_changed" = "%@ 將主題變更為「%@」。"; "notice_room_name_changed" = "%@ 將聊天室名稱變更為 %@。"; -"notice_placed_voice_call" = "%@ 已播出語音通話"; -"notice_placed_video_call" = "%@ 已播出視訊通話"; +"notice_placed_voice_call" = "%@ 已撥出語音通話"; +"notice_placed_video_call" = "%@ 已撥出視訊通話"; "notice_answered_video_call" = "%@ 已接聽通話"; "notice_ended_video_call" = "%@ 已結束通話"; "notice_conference_call_request" = "%@ 已請求 VoIP 會議"; @@ -1098,7 +1098,7 @@ "notice_room_topic_removed" = "%@ 移除了該主題"; "notice_event_redacted_by" = " 由 %@"; "notice_event_redacted_reason" = " [理由:%@]"; -"notice_profile_change_redacted" = "%@ 已更新他的個人檔案 %@"; +"notice_profile_change_redacted" = "%@ 更新了個人檔案 %@"; "notice_room_created" = "%@ 已建立並設定該聊天室。"; "notice_room_join_rule" = "加入規則: %@"; "notice_room_power_level_intro" = "聊天室成員們的權限级别是:"; @@ -1138,7 +1138,7 @@ "notice_error_unexpected_event" = "意外事件"; "notice_error_unknown_event_type" = "未知的事件類型"; "notice_room_history_visible_to_anyone" = "%@ 讓任何人都能看到未來的聊天室歷史記錄。"; -"notice_room_history_visible_to_members" = "%@ 讓所有聊天室成員都能看到聊天室之後的歷史記錄。"; +"notice_room_history_visible_to_members" = "%@ 讓所有成員都能看到聊天室之後的歷史記錄。"; "stop" = "停止"; "joining" = "正在加入"; "enable" = "啟用"; @@ -1297,12 +1297,12 @@ "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_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_select_room" = "選擇一個聊天室"; +"notification_settings_select_room" = "請選擇聊天室"; "notification_settings_sender_hint" = "@user:domain.com"; "notification_settings_per_sender_notifications" = "寄件人通知"; "notification_settings_per_room_notifications" = "聊天室的通知"; @@ -1346,11 +1346,11 @@ "login_error_already_logged_in" = "已經登入"; "message_unsaved_changes" = "還有變更未儲存。現在離開的話,您將會放棄這些變動。"; "notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "您讓所有人在加入後,就能看到未來的聊天室歷史紀錄。"; -"notice_room_history_visible_to_members_from_joined_point_by_you" = "您讓所有聊天室成員在加入後,都能看到未來的聊天室歷史紀錄。"; +"notice_room_history_visible_to_members_from_joined_point_by_you" = "您讓所有成員在加入後,都能看到未來的聊天室歷史紀錄。"; "notice_room_history_visible_to_members_from_invited_point_by_you_for_dm" = "您讓所有人收到邀請後,都能看到未來的聊天室歷史紀錄。"; -"notice_room_history_visible_to_members_from_invited_point_by_you" = "您讓所有聊天室成員被邀請後,都能看到未來的聊天室歷史紀錄。"; -"notice_room_history_visible_to_members_by_you_for_dm" = "您讓所有聊天室成員都能看到聊天室未來的歷史記錄。"; -"notice_room_history_visible_to_members_by_you" = "您讓所有聊天室成員都能看到聊天室未來的歷史記錄。"; +"notice_room_history_visible_to_members_from_invited_point_by_you" = "您讓所有成員被邀請後,都能看到未來的聊天室歷史紀錄。"; +"notice_room_history_visible_to_members_by_you_for_dm" = "您讓所有成員都能看到聊天室未來的歷史記錄。"; +"notice_room_history_visible_to_members_by_you" = "您讓所有成員都能看到聊天室未來的歷史記錄。"; "notice_room_history_visible_to_anyone_by_you" = "您讓任何人都能看到未來的聊天室歷史記錄。"; "notice_redaction_by_you" = "您已取消一个事件(id: %@)"; "notice_encryption_enabled_unknown_algorithm_by_you" = "您已開啟端到端加密(無法識別的演算法 %@)。"; @@ -1366,14 +1366,14 @@ "notice_declined_video_call_by_you" = "您已拒絕此通話"; "notice_ended_video_call_by_you" = "您已結束通話"; "notice_answered_video_call_by_you" = "您已接聽此通話"; -"notice_placed_video_call_by_you" = "您已播出視訊通話"; -"notice_placed_voice_call_by_you" = "您已播出語音通話"; -"notice_room_name_changed_by_you_for_dm" = "您已將名稱變更為 %@。"; -"notice_room_name_changed_by_you" = "您已將聊天室名稱變更為 %@。"; -"notice_topic_changed_by_you" = "您已經將主題變更為:%@。"; +"notice_placed_video_call_by_you" = "您已撥出視訊通話"; +"notice_placed_voice_call_by_you" = "您已撥出語音通話"; +"notice_room_name_changed_by_you_for_dm" = "您將名稱變更為 %@。"; +"notice_room_name_changed_by_you" = "您將聊天室名稱變更為 %@。"; +"notice_topic_changed_by_you" = "您將主題更改為:「%@」。"; "notice_display_name_removed_by_you" = "您已移除自己的顯示名稱"; "notice_display_name_changed_from_by_you" = "您已將顯示名稱從 %@ 變更為 %@"; -"notice_display_name_set_by_you" = "您已將顯示名稱設定為 %@"; +"notice_display_name_set_by_you" = "您將您的顯示名稱設定為 %@"; "notice_avatar_url_changed_by_you" = "您已變更您的大頭照"; "notice_room_withdraw_by_you" = "您已撤回 %@ 的邀請"; "notice_room_ban_by_you" = "您已封鎖 %@"; @@ -1392,12 +1392,12 @@ // Notice Events with "You" "notice_room_invite_by_you" = "您已邀請 %@"; "notice_declined_video_call" = "%@ 已拒絕此通話"; -"notice_room_name_changed_for_dm" = "%@ 把名稱變更為 %@。"; +"notice_room_name_changed_for_dm" = "%@ 將名稱變更為 %@。"; "notice_room_third_party_revoked_invite_for_dm" = "%@ 已撤銷對 %@ 的邀請"; "notice_room_third_party_revoked_invite" = "%@ 已撤銷對 %@ 加入聊天室的邀請"; "notice_room_third_party_invite_for_dm" = "%@ 已邀請 %@"; "microphone_access_not_granted_for_voice_message" = "語音簡訊需要使用麥克風的權限,但是 %@ 沒有存取權限"; -"error_common_message" = "發生了一個錯誤。請重新再試。"; +"error_common_message" = "發生錯誤。請稍後再試。"; "e2e_passphrase_create" = "建立安全密語"; "e2e_passphrase_too_short" = "安全密語太短(至少要 %d 字母的長度)"; "e2e_passphrase_confirm" = "確認安全密語"; @@ -1445,7 +1445,7 @@ "room_member_ignore_prompt" = "您確定要隱藏所有來自此使用者的訊息嗎?"; "message_reply_to_message_to_reply_to_prefix" = "回覆給"; "message_reply_to_sender_sent_their_live_location" = "即時位置。"; -"message_reply_to_sender_sent_their_location" = "已經分享了他們的位置。"; +"message_reply_to_sender_sent_their_location" = "分享了他們的位置。"; "message_reply_to_sender_sent_a_file" = "已傳送檔案。"; "message_reply_to_sender_sent_a_voice_message" = "已傳送語音訊息。"; "message_reply_to_sender_sent_an_audio_file" = "已傳送音訊檔。"; @@ -1531,7 +1531,7 @@ "user_session_rename_session_title" = "正在重新命名工作階段"; "user_session_inactive_session_description" = "不活躍的工作階段是您有一段時間未使用的工作階段,但它們會繼續接收加密金鑰。\n\n移除不活躍的工作階段可以改善安全性與效能,並讓您可以更容易地識別新的工作階段是否可疑。"; "user_session_inactive_session_title" = "不活躍的工作階段"; -"user_session_permanently_unverified_session_description" = "此工作階段無法對此對話進行加密,因此無法驗證。\n\n您無法進入已加密的聊天室中。\n\n為了安全與隱私,建議使用支援加密的 Matrix 客戶端。"; +"user_session_permanently_unverified_session_description" = "此工作階段不支援加密功能,所以無法驗證。\n\n您無法使用此工作階段進入有開啟加密的聊天室中。\n\n為了安全與隱私,建議使用支援加密的 Matrix 客戶端。"; "user_session_unverified_session_description" = "未驗證的工作階段是使用您的憑證登入但交叉叉驗證的工作階段。\n\n您應特別確定您可以識別這些工作階段,因為它們可能代表未經授權使用您的帳號。"; "user_session_unverified_session_title" = "未經驗證的工作階段"; "user_session_verified_session_description" = "已驗證的工作階段,是您輸入安全密語或透過另一個已驗證工作階段確認您的身分後,使用此 Element 帳號的任何地方。\n\n這代表了您擁有解鎖加密訊息,並向其他使用者確認您信任此工作階段所需的所有金鑰。"; @@ -1638,8 +1638,8 @@ "poll_timeline_total_one_vote_not_voted" = "已投 1 票。投票後即可檢視結果"; "poll_timeline_total_votes" = "共計 %lu 票"; "poll_timeline_total_one_vote" = "共計 1 票"; -"poll_timeline_total_no_votes" = "尚未投票"; -"poll_timeline_votes_count" = "%lu 張票"; +"poll_timeline_total_no_votes" = "尚無投票"; +"poll_timeline_votes_count" = "%lu 票"; "poll_timeline_one_vote" = "1 票"; "poll_edit_form_poll_type_closed_description" = "結果僅在您結束投票後顯示"; "poll_edit_form_poll_type_closed" = "秘密投票"; @@ -1683,12 +1683,12 @@ "all_chats_nothing_found_placeholder_title" = "找不到任何結果。"; "all_chats_empty_unreads_placeholder_message" = "當您有一些未讀的訊息時,這裡會顯示您的未讀訊息。"; "all_chats_empty_list_placeholder_title" = "您都看完了。"; -"all_chats_empty_view_information" = "適用於團隊、朋友與組織的多合一安全聊天應用程式。建立聊天室,或加入一個既有的聊天室。"; +"all_chats_empty_view_information" = "適用於團隊、朋友與組織的多合一安全聊天應用程式。建立聊天室,或加入現有的聊天室。"; "all_chats_empty_space_information" = "聊天空間是一種為聊天室與人們分組的新方式。使用右下角的按鈕新增既有的聊天室或建立新的。"; "all_chats_empty_view_title" = "%@\n看起來有點空。"; "all_chats_all_filter" = "全部"; -"all_chats_edit_layout_alphabetical_order" = "按 A-Z 排列"; -"all_chats_edit_layout_activity_order" = "按活動排列"; +"all_chats_edit_layout_alphabetical_order" = "按名稱 A-Z 排序"; +"all_chats_edit_layout_activity_order" = "按頻道最新活動排列"; "all_chats_edit_layout_show_filters" = "顯示過濾條件"; "all_chats_edit_layout_show_recents" = "顯示最近的"; "all_chats_edit_layout_sorting_options_title" = "分類您的訊息"; @@ -1996,10 +1996,10 @@ "notice_crypto_error_unknown_inbound_session_id" = "傳送者的工作階段,尚未傳送傳給我們這則訊息的金鑰。"; "notice_crypto_unable_to_decrypt" = "** 無法解密:%@ **"; "notice_room_history_visible_to_members_from_joined_point_for_dm" = "%@ 您讓所有人被邀請後,都能看到未來的聊天室歷史紀錄。"; -"notice_room_history_visible_to_members_from_joined_point" = "%@ 讓所有聊天室成員被邀請後開始,都能看到未來的聊天室歷史紀錄。"; -"notice_room_history_visible_to_members_from_invited_point_for_dm" = "%@ 從他們被邀請開始,讓未來的聊天室歷史紀錄顯示給所有聊天室成員。"; -"notice_room_history_visible_to_members_from_invited_point" = "%@ 從他們被邀請開始,讓未來的聊天室歷史紀錄顯示給所有聊天室成員。"; -"notice_room_history_visible_to_members_for_dm" = "%@ 讓所有聊天室成員都能看到聊天室之後的歷史記錄。"; +"notice_room_history_visible_to_members_from_joined_point" = "%@ 讓所有成員被邀請後開始,都能看到未來的聊天紀錄。"; +"notice_room_history_visible_to_members_from_invited_point_for_dm" = "%@ 從他們被邀請開始,讓未來的聊天紀錄顯示給所有成員。"; +"notice_room_history_visible_to_members_from_invited_point" = "%@ 從他們被邀請開始,讓未來的聊天紀錄顯示給所有成員。"; +"notice_room_history_visible_to_members_for_dm" = "%@ 讓所有成員都能看到聊天室之後的歷史記錄。"; "notice_error_unformattable_event" = "** 無法顯示這則訊息。請回報此錯誤"; "notice_encryption_enabled_unknown_algorithm" = "%1$@ 已開啟端到端加密(無法識別的演算法 %2$@)。"; "notice_encryption_enabled_ok" = "%@ 已開啟端到端加密。"; @@ -2009,11 +2009,11 @@ "notice_room_join_rule_public_by_you" = "您已公開此聊天室。"; "notice_room_join_rule_public_for_dm" = "%@ 公開這個。"; "notice_room_join_rule_public" = "%@ 公開此聊天室。"; -"notice_room_join_rule_invite_by_you_for_dm" = "您讓此變為邀請制。"; +"notice_room_join_rule_invite_by_you_for_dm" = "您將此處變為邀請制。"; "notice_room_join_rule_invite_by_you" = "您讓聊天室變為邀請才可加入。"; -"notice_room_join_rule_invite_for_dm" = "%@讓此變為邀請制。"; +"notice_room_join_rule_invite_for_dm" = "%@ 將此處變為邀請制。"; // New -"notice_room_join_rule_invite" = "%@讓聊天室變為邀請才可加入。"; +"notice_room_join_rule_invite" = "%@ 將聊天室變為邀請制。"; "notice_room_created_for_dm" = "%@ 已加入。"; "notice_room_name_removed_for_dm" = "%@ 移除了該聊天室的名稱"; "ignore_user" = "忽略使用者"; @@ -2114,7 +2114,7 @@ // Generic errors -"error_invite_3pid_with_no_identity_server" = "在設定加入一個身分伺服器,才能用電子郵件寄送邀請。"; +"error_invite_3pid_with_no_identity_server" = "在設定加入身分伺服器後,才能用電子郵件寄送邀請。"; "emoji_picker_flags_category" = "旗幟"; "emoji_picker_symbols_category" = "符號"; "emoji_picker_places_category" = "旅遊與景點"; @@ -2309,7 +2309,7 @@ "service_terms_modal_information_title_identity_server" = "身分伺服器"; "service_terms_modal_description_integration_manager" = "這會讓您可以使用聊天機器人、橋接、小工具和貼圖包。"; "service_terms_modal_description_identity_server" = "這會讓手機上儲存您電話或電子郵件的人能找到您。"; -"service_terms_modal_table_header_integration_manager" = "管理整合服務使用條款"; +"service_terms_modal_table_header_integration_manager" = "整合管理員使用條款"; "service_terms_modal_table_header_identity_server" = "身分伺服器條款"; "service_terms_modal_footer" = "您可以隨時在設定中取消。"; @@ -2362,10 +2362,10 @@ "leave_space_action" = "離開聊天空間"; "spaces_add_room_missing_permission_message" = "您沒有權限在此聊天空間中新增聊天室。"; -"spaces_creation_in_one_space" = "在一個聊天空間"; +"spaces_creation_in_one_space" = "在 1 個聊天空間"; "spaces_creation_in_many_spaces" = "在 %@ 個聊天空間"; "spaces_creation_in_spacename_plus_many" = "在 %@ 加入 %@ 個聊天空間"; -"spaces_creation_in_spacename_plus_one" = "在 %@ 加入一個聊天空間"; +"spaces_creation_in_spacename_plus_one" = "在 %@ 加入 1 個聊天空間"; "spaces_creation_in_spacename" = "在 %@"; "spaces_creation_post_process_inviting_users" = "邀請 %@ 位使用者"; "spaces_creation_post_process_adding_rooms" = "加入 %@ 個聊天室"; @@ -2424,7 +2424,7 @@ "room_notifs_settings_mentions_and_keywords" = "僅提及和關鍵字"; // Room Notification Settings -"room_notifs_settings_notify_me_for" = "通知我"; +"room_notifs_settings_notify_me_for" = "收到下列訊息時通知我"; "room_suggestion_settings_screen_message" = "將向聊天空間中的成員推薦建議的聊天室。"; "room_suggestion_settings_screen_title" = "將聊天室設為聊天空間中的建議聊天室"; @@ -2437,7 +2437,7 @@ "room_access_settings_screen_upgrade_alert_upgrading" = "升級聊天室"; "room_access_settings_screen_upgrade_alert_upgrade_button" = "升級"; "room_access_settings_screen_upgrade_alert_auto_invite_switch" = "自動邀請成員到新的聊天室"; -"room_access_settings_screen_upgrade_alert_note" = "請注意,升級會創造一個新版本的聊天室。目前所有的訊息都會放在已封存的聊天室。"; +"room_access_settings_screen_upgrade_alert_note" = "請注意,升級會建立新版的聊天室。目前的所有訊息都將封存在此聊天室中。"; "room_access_settings_screen_upgrade_alert_message_no_param" = "母聊天空間中的任何人都可以找到並加入此聊天室,不需要手動邀請所有人。您隨時都可以在聊天室設定中變更此設定。"; "room_access_settings_screen_upgrade_alert_message" = "任何在 %@ 的人都能找到並加入此聊天室,不需手動邀請所有人。您可以在聊天室的設定中隨時變更此設定。"; "room_access_settings_screen_upgrade_alert_title" = "升級聊天室"; @@ -2531,7 +2531,7 @@ "settings_discovery_three_pid_details_revoke_action" = "撤回"; "settings_discovery_three_pid_details_information_phone_number" = "在此管理讓其他使用者尋找您以及邀請您進入聊天室的電話號碼偏好設定。您可以在「帳號」中加入或刪除電話號碼。"; "settings_discovery_three_pid_details_information_email" = "在此管理讓其他使用者尋找您以及邀請您進入聊天室的電子郵件地址偏好設定。您可以在「帳號」中加入或刪除電子郵件地址。"; -"settings_discovery_three_pids_management_information_part1" = "選擇您希望其他使用者用哪一個電子郵件(或電話)聯絡您,以及邀請您進入聊天室。您可以在此清單加入/移除電子郵件(或電話)。 "; +"settings_discovery_three_pids_management_information_part1" = "選擇您希望其他使用者用哪一個電子郵件地址(或電話號碼)聯絡您,以及邀請您進入聊天室。您可以在此清單加入/移除電子郵件地址(或電話號碼)。 "; "settings_discovery_accept_terms" = "同意身分伺服器的使用條款"; "settings_discovery_terms_not_signed" = "需同意身分伺服器(%@)的使用條款,讓其他人可以用電子郵件地址或電話號碼找到您。"; "settings_discovery_no_identity_server" = "您目前未使用身分伺服器。若想要被您認識的聯絡人找到,請加入伺服器。"; @@ -2648,7 +2648,7 @@ "settings_call_invitations" = "通話邀請"; "settings_room_invitations" = "聊天室邀請"; "settings_messages_containing_at_room" = "@room"; -"settings_notify_me_for" = "通知我"; +"settings_notify_me_for" = "收到下列訊息時通知我"; "settings_mentions_and_keywords" = "提及與關鍵字"; "settings_notifications_disabled_alert_message" = "如需啟用通知,請前往裝置設定。"; "settings_security" = "安全性"; @@ -2824,8 +2824,8 @@ "wysiwyg_composer_format_action_unordered_list" = "切換項目符號清單"; "wysiwyg_composer_format_action_inline_code" = "套用內嵌程式碼格式"; "user_other_session_security_recommendation_title" = "其他工作階段"; -"poll_timeline_reply_ended_poll" = "結束投票"; -"poll_timeline_ended_text" = "結束投票"; +"poll_timeline_reply_ended_poll" = "已結束投票"; +"poll_timeline_ended_text" = "投票已結束"; "poll_timeline_decryption_error" = "因為解密錯誤,不會計算部份投票"; "poll_history_load_more" = "載入更多投票"; "poll_history_detail_view_in_timeline" = "在時間軸中檢視投票"; From 357c89137e674e9ab58f98be915be27f35b8058f Mon Sep 17 00:00:00 2001 From: SmallJinn Date: Mon, 17 Apr 2023 09:22:12 +0000 Subject: [PATCH 055/149] Translated using Weblate (Russian) Currently translated at 83.0% (1988 of 2394 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ --- Riot/Assets/ru.lproj/Vector.strings | 67 ++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index 857845707..0f2f21fd4 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -239,7 +239,7 @@ "settings_ignored_users" = "ИГНОРИРУЕМЫЕ ПОЛЬЗОВАТЕЛИ"; "settings_contacts" = "КОНТАКТЫ УСТРОЙСТВА"; "settings_advanced" = "ДОПОЛНИТЕЛЬНО"; -"settings_other" = "ДРУГИЕ"; +"settings_other" = "Другие"; "settings_labs" = "ЛАБОРАТОРИЯ"; "settings_devices" = "СЕАНСЫ"; "settings_cryptography" = "КРИПТОГРАФИЯ"; @@ -272,11 +272,11 @@ "settings_send_crash_report" = "Отправка данных о сбоях и использовании"; "settings_clear_cache" = "Очистить кэш"; "settings_change_password" = "Изменить пароль"; -"settings_old_password" = "старый пароль"; -"settings_new_password" = "новый пароль"; -"settings_confirm_password" = "подтвердить пароль"; -"settings_fail_to_update_password" = "Не удалось обновить пароль"; -"settings_password_updated" = "Ваш пароль был обновлен"; +"settings_old_password" = "Старый пароль"; +"settings_new_password" = "Новый пароль"; +"settings_confirm_password" = "Подтвердить пароль"; +"settings_fail_to_update_password" = "Не удалось обновить пароль аккаунта Matrix"; +"settings_password_updated" = "Ваш пароль аккаунта Matrix был обновлен"; "settings_crypto_device_name" = "Имя сеанса: "; "settings_crypto_device_id" = "\nID сеанса: "; "settings_crypto_device_key" = "\nКлюч сеанса:\n"; @@ -512,7 +512,7 @@ "room_action_send_photo_or_video" = "Отправить фото или видео"; "room_action_send_sticker" = "Отправить стикер"; "settings_deactivate_account" = "ДЕАКТИВАЦИЯ АККАУНТА"; -"settings_deactivate_my_account" = "Деактивировать мой аккаунт"; +"settings_deactivate_my_account" = "Деактивировать аккаунт навсегда"; "widget_sticker_picker_no_stickerpacks_alert_add_now" = "Добавить сейчас?"; "deactivate_account_title" = "Деактивировать аккаунт"; "deactivate_account_informations_part1" = "Это действие сделает вашу учетную запись непригодной для дальнейшего использования. Вы не сможете войти в систему и никто другой не сможет заново зарегистрировать учетную запись с вашим идентификатором. Также, это приведет к тому, что вы покинете все комнаты, в которых участвовали и данные о вашей учетной записи будут удалены с сервера идентификации. "; @@ -525,7 +525,7 @@ "deactivate_account_forget_messages_information_part3" = ": будущие участники увидят неполное представление разговоров)"; "deactivate_account_validate_action" = "Деактивировать аккаунт"; "deactivate_account_password_alert_title" = "Деактивировать аккаунт"; -"deactivate_account_password_alert_message" = "Чтобы продолжить, введите пароль"; +"deactivate_account_password_alert_message" = "Чтобы продолжить, введите пароль аккаунта Matrix"; "widget_sticker_picker_no_stickerpacks_alert" = "У вас пока нет включенных пакетов стикеров."; "event_formatter_rerequest_keys_part1_link" = "Повторно запросить ключи шифрования"; "event_formatter_rerequest_keys_part2" = " из других ваших сеансов."; @@ -583,7 +583,7 @@ "key_backup_setup_intro_title" = "Никогда не теряйте зашифрованных сообщений"; "key_backup_setup_intro_info" = "Сообщения в зашифрованных комнатах защищены сквозным шифрованием. Только вы и получатель(и) имеют ключи для чтения этих сообщений.\n\nСохраните ключи надежно, чтобы не потерять их."; "key_backup_setup_intro_setup_action" = "Настроить"; -"key_backup_setup_passphrase_info" = "Мы будем хранить зашифрованную копию ваших ключей на нашем сервере. Для безопасности, защитите резервную копию секретной фразой.\n\nДля обеспечения максимальной безопасности она должна отличаться от пароля учетной записи."; +"key_backup_setup_passphrase_info" = "Мы будем хранить зашифрованную копию ваших ключей на нашем сервере. Для безопасности, защитите резервную копию секретной фразой.\n\nДля обеспечения максимальной безопасности она должна отличаться от пароля учётной записи Matrix."; "key_backup_setup_passphrase_passphrase_title" = "Ввод"; "key_backup_setup_passphrase_passphrase_placeholder" = "Введите секретную фразу"; "key_backup_setup_passphrase_passphrase_valid" = "Отлично!"; @@ -864,7 +864,7 @@ "identity_server_settings_alert_error_invalid_identity_server" = "%@ не является действительным сервером идентификации."; "settings_add_3pid_password_title_email" = "Добавить адрес электронной почты"; "settings_add_3pid_password_title_msidsn" = "Добавить номер телефона"; -"settings_add_3pid_password_message" = "Для продолжения, задайте пароль"; +"settings_add_3pid_password_message" = "Для продолжения, введите пароль аккаунта Matrix"; "settings_add_3pid_invalid_password_message" = "Недействительные данные"; "settings_discovery_three_pid_details_title_phone_number" = "Управление номера телефона"; "settings_identity_server_no_is" = "Сервер идентификации не настроен"; @@ -915,7 +915,7 @@ "security_settings_title" = "Безопасность"; "security_settings_crypto_sessions" = "МОИ СЕАНСЫ"; "security_settings_crypto_sessions_loading" = "Загрузка сеансов…"; -"security_settings_crypto_sessions_description_2" = "Если вы не узнали логин, измените пароль и сбросьте Безопасное резервное копирование."; +"security_settings_crypto_sessions_description_2" = "Если вы не узнали логин, измените пароль аккаунта Matrix и сбросьте Безопасное резервное копирование."; "security_settings_secure_backup" = "БЕЗОПАСНОЕ РЕЗЕРВНОЕ КОПИРОВАНИЕ"; "security_settings_secure_backup_description" = "Сделайте резервную копию ключей шифрования с данными вашей учетной записи на случай, если вы потеряете доступ к своим сеансам. Ваши ключи будут защищены уникальным электронным ключом."; "security_settings_secure_backup_setup" = "Настроить"; @@ -938,7 +938,7 @@ "security_settings_complete_security_alert_title" = "Завершите настройку безопасности"; "security_settings_complete_security_alert_message" = "Сначала вы должны завершить настройку безопасности текущего сеанса."; "security_settings_coming_soon" = "Извините. Это действие пока недоступно в %@ iOS. Пожалуйста, используйте другой клиент Matrix для его настройки. %@ iOS будет его использовать."; -"security_settings_user_password_description" = "Подтвердите свою личность, введя пароль учетной записи"; +"security_settings_user_password_description" = "Подтвердите свою личность, введя пароль учётной записи Matrix"; // Manage session "manage_session_title" = "Управление сеансами"; "manage_session_info" = "ИНФОРМАЦИЯ О СЕАНСЕ"; @@ -1130,7 +1130,7 @@ "secrets_setup_recovery_key_storage_alert_message" = "✓ Распечатайте и храните в безопасном месте\n✓ Сохраните его на USB-носителе или резервном носителе\n✓ Скопируйте его в свое личное облачное хранилище"; "secrets_setup_recovery_passphrase_title" = "Задайте мнемоническую фразу"; "secrets_setup_recovery_passphrase_information" = "Введите секретную фразу, известную только вам, для защиты данных на вашем сервере."; -"secrets_setup_recovery_passphrase_additional_information" = "Не используйте пароль своей учетной записи."; +"secrets_setup_recovery_passphrase_additional_information" = "Не используйте пароль своей учётной записи Matrix."; "secrets_setup_recovery_passphrase_validate_action" = "Готово"; "secrets_setup_recovery_passphrase_confirm_information" = "Введите мнемоническую фразу ещё раз, чтобы подтвердить её."; "secrets_setup_recovery_passphrase_confirm_passphrase_title" = "Подтвердить"; @@ -1183,7 +1183,7 @@ "searchable_directory_x_network" = "%@ Сеть"; "searchable_directory_search_placeholder" = "Имя или ID"; "create_room_title" = "Новая комната"; -"create_room_section_header_name" = "Имя комнаты"; +"create_room_section_header_name" = "НАЗВАНИЕ"; "create_room_placeholder_name" = "Имя"; "create_room_section_header_topic" = "Тема комнаты (опционально)"; "create_room_placeholder_topic" = "Тема"; @@ -1218,7 +1218,7 @@ "room_details_advanced_e2e_encryption_enabled_for_dm" = "Шифрование включено"; "room_details_advanced_e2e_encryption_disabled_for_dm" = "Шифрование не включено."; "pin_protection_kick_user_alert_message" = "Слишком много ошибок, вы вышли из системы"; -"secrets_reset_authentication_message" = "Введите пароль своей учётной записи для подтверждения"; +"secrets_reset_authentication_message" = "Введите пароль своей учётной записи Matrix для подтверждения"; "secrets_reset_reset_action" = "Сброс"; "secrets_reset_warning_message" = "Вы перезапустите приложение без истории, сообщений, доверенных устройств или доверенных пользователей."; "secrets_reset_warning_title" = "Если сбросить все"; @@ -2224,3 +2224,40 @@ "threads_notice_information" = "Все потоки созданные во время экспериментального периода теперь отображаются как обычные ответы.

Это разовый переход, так как потоки теперь часть спецификации Matrix."; "authentication_qr_login_failure_device_not_supported" = "Связь с этим устройством не поддерживается."; "accessibility_selected" = "выбранный"; +"room_access_settings_screen_message" = "Решите, кто может найти и присоединиться к %@."; +"room_access_settings_screen_title" = "Кто может получить доступ к этой комнате?"; +"room_details_promote_room_suggest_title" = "Предложить участникам пространства"; +/* The placeholder will be replaces with manage_session_name_info_link */ +"manage_session_name_info" = "Учитывайте, что имена сессий также видны людям, с которыми вы общаетесь. %@"; +"settings_labs_enable_new_client_info_feature" = "Запишите имя клиента, версию и URL-адрес, чтобы упростить распознавание сеансов в диспетчере сеансов"; +"sign_out_confirmation_message" = "Вы уверены, что хотите выйти?"; +"share_extension_send_now" = "Отправить сейчас"; +"share_extension_low_quality_video_title" = "Видео будет отправлено в низком качестве"; +"analytics_prompt_stop" = "Прекратить делиться"; +"analytics_prompt_not_now" = "Не сейчас"; +"analytics_prompt_point_3" = "Вы можете отключить это в любое время в настройках"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Мы не передаем информацию третьим лицам"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Мы не записываем и не профилируем никакие данные учётной записи"; +"analytics_prompt_message_upgrade" = "Ранее вы дали согласие на передачу нам анонимных данных об использовании. Теперь, чтобы помочь понять, как люди используют несколько устройств, мы сгенерируем случайный идентификатор, общий для всех ваших устройств."; +"analytics_prompt_message_new_user" = "Помогите нам выявить проблемы и улучшить %@, поделившись анонимными данными об использовании. Чтобы понять, как люди используют несколько устройств, мы сгенерируем случайный идентификатор, общий для всех ваших устройств."; + +// Analytics +"analytics_prompt_title" = "Помогите улучшить %@"; +"call_jitsi_unable_to_start" = "Невозможно начать конференц-звонок"; +"network_offline_message" = "Вы не в сети, проверьте ваше соединение."; +"network_offline_title" = "Вы не в сети"; +"event_formatter_message_deleted" = "Сообщение удалено"; +"room_access_space_chooser_other_spaces_section" = "Другие пространства или комнаты"; +"room_access_settings_screen_setting_room_access" = "Настройка доступа к комнате"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Автоматически приглашать участников в новую комнату"; +"room_access_settings_screen_upgrade_alert_note" = "Обратите внимание, что при обновлении будет создана новая версия комнаты. Все текущие сообщения останутся в этой архивной комнате."; +"room_access_settings_screen_upgrade_alert_message_no_param" = "Любой в родительском пространстве сможет найти и присоединиться к этой комнате - нет необходимости вручную приглашать всех вручную. Вы сможете изменить это в настройках комнаты в любое время."; +"room_access_settings_screen_upgrade_alert_message" = "Любой человек в %@ сможет найти и присоединиться к этой комнате - нет необходимости вручную приглашать всех. Вы сможете изменить это в настройках комнаты в любое время."; +"room_access_settings_screen_public_message" = "Любой желающий может найти и присоединиться."; +"room_access_settings_screen_edit_spaces" = "Редактировать пространства"; +"room_access_settings_screen_restricted_message" = "Позволяет всем, кто находится в пространстве, найти его и присоединиться.\nВам будет предложено подтвердить к каким пространствам."; +"room_access_settings_screen_private_message" = "Только приглашенные люди могут найти и присоединиться."; +"manage_session_name_hint" = "Индивидуальные имена сеансов помогут Вам легче распознавать свои устройства."; +"settings_labs_confirm_crypto_sdk" = "Имейте ввиду, что эта функция все ещё на экспериментальной стадии, поэтому она может работать не так, как ожидается, и потенциально может иметь непредвиденные последствия. Для отмены функции выйдите из системы и войдите снова. Используйте её по своему усмотрению и с осторожностью."; From 4e626f48f16870787704ffd171f3fe7c1faf8e83 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 18 Apr 2023 11:06:25 +0100 Subject: [PATCH 056/149] Deprecate MXLegacyCrypto --- Config/CommonConfiguration.swift | 25 +- Config/Configurable.swift | 3 - Riot/Assets/en.lproj/Vector.strings | 3 - .../MXBugReportRestClient+Riot.swift | 1 - Riot/Experiments/CryptoSDKFeature.swift | 116 --------- Riot/Generated/Strings.swift | 12 - Riot/Modules/Analytics/Analytics.swift | 2 +- .../Analytics/SentryMonitoringClient.swift | 3 - Riot/Modules/Application/LegacyAppDelegate.m | 242 +----------------- .../AuthenticationCoordinator.swift | 9 +- .../LegacyAuthenticationCoordinator.swift | 9 +- .../SessionVerificationListener.swift | 15 +- Riot/Modules/Call/CallViewController.m | 28 +- .../AllChats/AllChatsViewController.swift | 3 +- .../LaunchLoading/LaunchLoadingView.swift | 3 - .../MatrixKit/Models/Account/MXKAccount.m | 10 +- Riot/Modules/Room/RoomViewController.m | 17 +- .../RoomKeyRequestViewController.h | 62 ----- .../RoomKeyRequestViewController.m | 195 -------------- .../Modules/Settings/SettingsViewController.m | 44 +--- .../UserDevices/UsersDevicesViewController.m | 18 +- RiotNSE/NotificationService.swift | 14 +- RiotShareExtension/Shared/ShareManager.m | 5 - .../Service/MatrixSDK/QRLoginService.swift | 11 - .../Experiments/CryptoSDKFeatureTests.swift | 79 ------ .../SendMessage/SendMessageIntentHandler.m | 6 - changelog.d/pr-7508.change | 1 + 27 files changed, 39 insertions(+), 897 deletions(-) delete mode 100644 Riot/Experiments/CryptoSDKFeature.swift delete mode 100644 Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h delete mode 100644 Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m delete mode 100644 RiotTests/Experiments/CryptoSDKFeatureTests.swift create mode 100644 changelog.d/pr-7508.change diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index b00f18831..4a2c05785 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -92,8 +92,7 @@ class CommonConfiguration: NSObject, Configurable { sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature - // Configure Crypto SDK feature deciding which crypto module to use - sdkOptions.cryptoSDKFeature = CryptoSDKFeature.shared + sdkOptions.cryptoMigrationDelegate = self } private func makeASCIIUserAgent() -> String? { @@ -168,14 +167,16 @@ class CommonConfiguration: NSObject, Configurable { if RiotSettings.shared.allowStunServerFallback, let stunServerFallback = BuildSettings.stunServerFallbackUrlString { callManager.fallbackSTUNServer = stunServerFallback } - } - - - // MARK: - Per loaded matrix session settings - - func setupSettingsWhenLoaded(for matrixSession: MXSession) { - // Do not warn for unknown devices. We have cross-signing now - (matrixSession.crypto as? MXLegacyCrypto)?.warnOnUnknowDevices = false - } - + } +} + +extension CommonConfiguration: MXCryptoV2MigrationDelegate { + var needsVerificationUpgrade: Bool { + get { + RiotSettings.shared.showVerificationUpgradeAlert + } + set { + RiotSettings.shared.showVerificationUpgradeAlert = newValue + } + } } diff --git a/Config/Configurable.swift b/Config/Configurable.swift index acfb97605..2f1c46a03 100644 --- a/Config/Configurable.swift +++ b/Config/Configurable.swift @@ -24,7 +24,4 @@ import MatrixSDK // MARK: - Per matrix session settings func setupSettings(for matrixSession: MXSession) - - // MARK: - Per loaded matrix session settings - func setupSettingsWhenLoaded(for matrixSession: MXSession) } diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index d88b99b9d..c1099f168 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -807,9 +807,6 @@ Tap the + to start adding people."; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast"; -"settings_labs_enable_crypto_sdk" = "Rust end-to-end encryption"; -"settings_labs_confirm_crypto_sdk" = "Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution."; -"settings_labs_disable_crypto_sdk" = "Rust end-to-end encryption (log out to disable)"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Categories/MXBugReportRestClient+Riot.swift b/Riot/Categories/MXBugReportRestClient+Riot.swift index fef876f92..b836f1ab4 100644 --- a/Riot/Categories/MXBugReportRestClient+Riot.swift +++ b/Riot/Categories/MXBugReportRestClient+Riot.swift @@ -70,7 +70,6 @@ extension MXBugReportRestClient { // SDKs userInfo["matrix_sdk_version"] = MatrixSDKVersion - userInfo["crypto_module"] = MXSDKOptions.sharedInstance().cryptoModuleId if let crypto = mainAccount?.mxSession?.crypto { userInfo["crypto_module_version"] = crypto.version } diff --git a/Riot/Experiments/CryptoSDKFeature.swift b/Riot/Experiments/CryptoSDKFeature.swift deleted file mode 100644 index e52fc637b..000000000 --- a/Riot/Experiments/CryptoSDKFeature.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// Copyright 2023 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 MatrixSDKCrypto - -/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status -/// of `CryptoSDK`, and which uses feature flags to control rollout availability. -/// -/// The implementation uses both remote and local feature flags to control the availability of `CryptoSDK`. -/// Whilst remote is more convenient in that it allows changes to the rollout without new app releases, -/// it is not available to all users because it requires data tracking user consent. Remote therefore -/// represents the safer, albeit limited rollout strategy, whereas the local feature flags allows eventually -/// targetting all users, but each target change requires new app release. -/// -/// Additionally users can manually enable this feature from the settings if they are not already in the -/// feature group. -@objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature { - @objc static let shared = CryptoSDKFeature() - - var isEnabled: Bool { - RiotSettings.shared.enableCryptoSDK - } - - var needsVerificationUpgrade: Bool { - get { - return RiotSettings.shared.showVerificationUpgradeAlert - } - set { - RiotSettings.shared.showVerificationUpgradeAlert = newValue - } - } - - private static let FeatureName = "ios-crypto-sdk" - private static let FeatureNameV2 = "ios-crypto-sdk-v2" - - private let remoteFeature: RemoteFeaturesClientProtocol - private let localFeature: PhasedRolloutFeature - - init( - remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared, - localTargetPercentage: Double = 1 - ) { - self.remoteFeature = remoteFeature - self.localFeature = PhasedRolloutFeature( - name: Self.FeatureName, - targetPercentage: localTargetPercentage - ) - } - - func enable() { - RiotSettings.shared.enableCryptoSDK = true - Analytics.shared.trackCryptoSDKEnabled() - - MXLog.debug("[CryptoSDKFeature] Crypto SDK enabled") - } - - func enableIfAvailable(forUserId userId: String!) { - guard !isEnabled else { - MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is already enabled") - return - } - - guard let userId else { - MXLog.failure("[CryptoSDKFeature] enableIfAvailable: Missing user id") - return - } - - guard isFeatureEnabled(userId: userId) else { - MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is currently not available for this user") - return - } - - MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature has become available for this user and will be enabled") - enable() - } - - @objc func canManuallyEnable(forUserId userId: String!) -> Bool { - guard let userId else { - MXLog.failure("[CryptoSDKFeature] canManuallyEnable: Missing user id") - return false - } - - // User can manually enable only if not already within the automatic feature group - return !isFeatureEnabled(userId: userId) - } - - @objc func reset() { - RiotSettings.shared.enableCryptoSDK = false - MXLog.debug("[CryptoSDKFeature] Crypto SDK disabled") - } - - private func isFeatureEnabled(userId: String) -> Bool { - // This feature includes app version with a bug, and thus will not be rolled out to 100% users - remoteFeature.isFeatureEnabled(Self.FeatureName) - - // Second version of the remote feature with a bugfix and released eventually to 100% users - || remoteFeature.isFeatureEnabled(Self.FeatureNameV2) - - // Local feature - || localFeature.isEnabled(userId: userId) - } -} diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index c02a605c6..0cdd03d2d 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7647,18 +7647,10 @@ public class VectorL10n: NSObject { public static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") } - /// Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution. - public static var settingsLabsConfirmCryptoSdk: String { - return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk") - } /// Create conference calls with jitsi public static var settingsLabsCreateConferenceWithJitsi: String { return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") } - /// Rust end-to-end encryption (log out to disable) - public static var settingsLabsDisableCryptoSdk: String { - return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk") - } /// End-to-End Encryption public static var settingsLabsE2eEncryption: String { return VectorL10n.tr("Vector", "settings_labs_e2e_encryption") @@ -7671,10 +7663,6 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableAutoReportDecryptionErrors: String { return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") } - /// Rust end-to-end encryption - public static var settingsLabsEnableCryptoSdk: String { - return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk") - } /// Live location sharing - share current location (active development, and temporarily, locations persist in room history) public static var settingsLabsEnableLiveLocationSharing: String { return VectorL10n.tr("Vector", "settings_labs_enable_live_location_sharing") diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index c48b447e5..1a30841b9 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -274,7 +274,7 @@ extension Analytics { func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { let event = AnalyticsEvent.Error( context: context, - cryptoModule: MXSDKOptions.sharedInstance().enableCryptoSDK ? .Rust : .Native, + cryptoModule: .Rust, domain: .E2EE, name: reason.errorName ) diff --git a/Riot/Modules/Analytics/SentryMonitoringClient.swift b/Riot/Modules/Analytics/SentryMonitoringClient.swift index 78450551b..54933a7ab 100644 --- a/Riot/Modules/Analytics/SentryMonitoringClient.swift +++ b/Riot/Modules/Analytics/SentryMonitoringClient.swift @@ -46,9 +46,6 @@ struct SentryMonitoringClient { if let message = event.message?.formatted { event.fingerprint = [message] } - event.tags = [ - "crypto_module": MXSDKOptions.sharedInstance().cryptoModuleId - ] MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)") return event } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 208cb46eb..8678f5ab8 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -33,7 +33,6 @@ #import "ContactDetailsViewController.h" #import "BugReportViewController.h" -#import "RoomKeyRequestViewController.h" #import "DecryptionFailureTracker.h" #import "Tools.h" @@ -114,11 +113,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni id roomKeyRequestObserver; id roomKeyRequestCancellationObserver; - /** - If any the currently displayed sharing key dialog - */ - RoomKeyRequestViewController *roomKeyRequestViewController; - /** Incoming key verification requests observers */ @@ -1823,8 +1817,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // start the call service [self.callPresenter start]; - [self.configuration setupSettingsWhenLoadedFor:mxSession]; - // Register to user new device sign in notification [self registerUserDidSignInOnNewDeviceNotificationForSession:mxSession]; @@ -1833,8 +1825,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Register to new key verification request [self registerNewRequestNotificationForSession:mxSession]; - [self checkLocalPrivateKeysInSession:mxSession]; - [self.pushNotificationService checkPushKitPushersInSession:mxSession]; } else if (mxSession.state == MXSessionStateRunning) @@ -2031,9 +2021,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // If any, disable the no VoIP support workaround [self disableNoVoIPOnMatrixSession:mxSession]; - // Disable listening of incoming key share requests - [self disableRoomKeyRequestObserver:mxSession]; - // Disable listening of incoming key verification requests [self disableIncomingKeyVerificationObserver:mxSession]; @@ -2183,9 +2170,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Clear cache [self clearCache]; - // Reset Crypto SDK configuration (labs flag for which crypto module to use) - [CryptoSDKFeature.shared reset]; - // Reset key backup banner preferences [SecureBackupBannerPreferences.shared reset]; @@ -2296,11 +2280,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni case MXSessionStateSyncInProgress: // Stay in launching during the first server sync if the store is empty. isLaunching = (mainSession.rooms.count == 0 && launchAnimationContainerView); - - if (mainSession.crypto.crossSigning && mainSession.crypto.crossSigning.state == MXCrossSigningStateCrossSigningExists && [mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - [(MXLegacyCrypto *)mainSession.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil]; - } break; case MXSessionStateRunning: self.clearingCache = NO; @@ -2360,7 +2339,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // This is the time to check existing requests MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests"); - [self checkPendingRoomKeyRequests]; [self checkPendingIncomingKeyVerificationsInSession:mainSession]; // TODO: When we will have an application state, we will do all of this in a dedicated initialisation state @@ -2369,9 +2347,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { MXLogDebug(@"[AppDelegate] handleAppState: Set up observers for the crypto module"); - // Enable listening of incoming key share requests - [self enableRoomKeyRequestObserver:mainSession]; - // Enable listening of incoming key verification requests [self enableIncomingKeyVerificationObserver:mainSession]; } @@ -2397,16 +2372,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { MXLogDebug(@"[AppDelegate] showLaunchAnimation"); - LaunchLoadingView *launchLoadingView; - if (MXSDKOptions.sharedInstance.enableStartupProgress) - { - MXSession *mainSession = self.mxSessions.firstObject; - launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; - } - else - { - launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:nil]; - } + MXSession *mainSession = self.mxSessions.firstObject; + LaunchLoadingView *launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; launchLoadingView.frame = window.bounds; [launchLoadingView updateWithTheme:ThemeService.shared.theme]; @@ -2520,38 +2487,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni #endif } -- (void)checkLocalPrivateKeysInSession:(MXSession*)mxSession -{ - if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - return; - } - MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto; - - MXRecoveryService *recoveryService = mxSession.crypto.recoveryService; - NSUInteger keysCount = 0; - if ([recoveryService hasSecretWithSecretId:MXSecretId.keyBackup]) - { - keysCount++; - } - if ([recoveryService hasSecretWithSecretId:MXSecretId.crossSigningUserSigning]) - { - keysCount++; - } - if ([recoveryService hasSecretWithSecretId:MXSecretId.crossSigningSelfSigning]) - { - keysCount++; - } - - if ((keysCount > 0 && keysCount < 3) - || (mxSession.crypto.crossSigning.canTrustCrossSigning && !mxSession.crypto.crossSigning.canCrossSign)) - { - // We should have 3 of them. If not, request them again as mitigation - MXLogDebug(@"[AppDelegate] checkLocalPrivateKeysInSession: request keys because keysCount = %@", @(keysCount)); - [crypto requestAllPrivateKeys]; - } -} - - (void)authenticationDidComplete { [self handleAppState]; @@ -3461,173 +3396,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } - -#pragma mark - Incoming room key requests handling - -- (void)enableRoomKeyRequestObserver:(MXSession*)mxSession -{ - roomKeyRequestObserver = - [[NSNotificationCenter defaultCenter] addObserverForName:kMXCryptoRoomKeyRequestNotification - object:mxSession.crypto - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification *notif) - { - [self checkPendingRoomKeyRequestsInSession:mxSession]; - }]; - - roomKeyRequestCancellationObserver = - [[NSNotificationCenter defaultCenter] addObserverForName:kMXCryptoRoomKeyRequestCancellationNotification - object:mxSession.crypto - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification *notif) - { - [self checkPendingRoomKeyRequestsInSession:mxSession]; - }]; -} - -- (void)disableRoomKeyRequestObserver:(MXSession*)mxSession -{ - if (roomKeyRequestObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:roomKeyRequestObserver]; - roomKeyRequestObserver = nil; - } - - if (roomKeyRequestCancellationObserver) - { - [[NSNotificationCenter defaultCenter] removeObserver:roomKeyRequestCancellationObserver]; - roomKeyRequestCancellationObserver = nil; - } -} - -// Check if a key share dialog must be displayed for the given session -- (void)checkPendingRoomKeyRequestsInSession:(MXSession*)mxSession -{ - if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession called while the app is not active. Ignore it."); - return; - } - - if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Only legacy crypto allows manually accepting/rejecting key requests"); - return; - } - MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto; - - MXWeakify(self); - [crypto pendingKeyRequests:^(MXUsersDevicesMap *> *pendingKeyRequests) { - - MXStrongifyAndReturnIfNil(self); - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: cross-signing state: %ld, pendingKeyRequests.count: %@. Already displayed: %@", - crypto.crossSigning.state, - @(pendingKeyRequests.count), - self->roomKeyRequestViewController ? @"YES" : @"NO"); - - if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) - { - if (self->roomKeyRequestViewController) - { - // Check if the current RoomKeyRequestViewController is still valid - MXSession *currentMXSession = self->roomKeyRequestViewController.mxSession; - NSString *currentUser = self->roomKeyRequestViewController.device.userId; - NSString *currentDevice = self->roomKeyRequestViewController.device.deviceId; - - NSArray *currentPendingRequest = [pendingKeyRequests objectForDevice:currentDevice forUser:currentUser]; - - if (currentMXSession == mxSession && currentPendingRequest.count == 0) - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Cancel current dialog"); - - // The key request has been probably cancelled, remove the popup - [self->roomKeyRequestViewController hide]; - self->roomKeyRequestViewController = nil; - } - } - } - - if (!self->roomKeyRequestViewController && pendingKeyRequests.count) - { - // Pick the first coming user/device pair - NSString *userId = pendingKeyRequests.userIds.firstObject; - NSString *deviceId = [pendingKeyRequests deviceIdsForUser:userId].firstObject; - - // Give the client a chance to refresh the device list - MXWeakify(self); - [crypto downloadKeys:@[userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { - - MXStrongifyAndReturnIfNil(self); - MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:deviceId forUser:userId]; - if (deviceInfo) - { - if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) - { - BOOL wasNewDevice = (deviceInfo.trustLevel.localVerificationStatus == MXDeviceUnknown); - - void (^openDialog)(void) = ^void() - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Open dialog for %@", deviceInfo); - - self->roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession crypto:crypto onComplete:^{ - - self->roomKeyRequestViewController = nil; - - // Check next pending key request, if any - [self checkPendingRoomKeyRequests]; - }]; - - [self->roomKeyRequestViewController show]; - }; - - // If the device was new before, it's not any more. - if (wasNewDevice) - { - [crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil]; - } - else - { - openDialog(); - } - } - else if (deviceInfo.trustLevel.isVerified) - { - [crypto acceptAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ - [self checkPendingRoomKeyRequests]; - }]; - } - else - { - [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ - [self checkPendingRoomKeyRequests]; - }]; - } - } - else - { - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: No details found for device %@:%@", userId, deviceId); - [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ - [self checkPendingRoomKeyRequests]; - }]; - } - } failure:^(NSError *error) { - // Retry later - MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Failed to download device keys. Retry"); - [self checkPendingRoomKeyRequests]; - }]; - } - }]; -} - -// Check all opened MXSessions for key share dialog -- (void)checkPendingRoomKeyRequests -{ - for (MXSession *mxSession in mxSessionArray) - { - [self checkPendingRoomKeyRequestsInSession:mxSession]; - } -} - #pragma mark - Incoming key verification handling - (void)enableIncomingKeyVerificationObserver:(MXSession*)mxSession @@ -3785,12 +3553,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId { - id crypto = coordinatorBridgePresenter.session.crypto; - if ([crypto isKindOfClass:[MXLegacyCrypto class]] && (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled)) - { - MXLogDebug(@"[AppDelegate][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys"); - [(MXLegacyCrypto *)crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil]; - } [self dismissKeyVerificationCoordinatorBridgePresenter]; } diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 9f2e7083b..a245147cd 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -613,8 +613,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc /// Replace the contents of the navigation router with a loading animation. private func showLoadingAnimation() { - let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil - let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) + let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation @@ -759,12 +758,6 @@ extension AuthenticationCoordinator: AuthenticationServiceDelegate { // MARK: - KeyVerificationCoordinatorDelegate extension AuthenticationCoordinator: KeyVerificationCoordinatorDelegate { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { - if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup, - !backup.hasPrivateKeyInCryptoStore || !backup.enabled { - MXLog.debug("[AuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys") - crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) - } - navigationRouter.dismissModule(animated: true) { [weak self] in self?.authenticationDidComplete() } diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift index d6270edae..4aea0b8b9 100644 --- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift @@ -106,8 +106,7 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator // MARK: - Private private func showLoadingAnimation() { - let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil - let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) + let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation @@ -220,12 +219,6 @@ extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate // MARK: - KeyVerificationCoordinatorDelegate extension LegacyAuthenticationCoordinator: KeyVerificationCoordinatorDelegate { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { - if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup, - !backup.hasPrivateKeyInCryptoStore || !backup.enabled { - MXLog.debug("[LegacyAuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys") - crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) - } - navigationRouter.dismissModule(animated: true) { [weak self] in self?.authenticationDidComplete() } diff --git a/Riot/Modules/Authentication/SessionVerificationListener.swift b/Riot/Modules/Authentication/SessionVerificationListener.swift index 214c76695..ffefd839a 100644 --- a/Riot/Modules/Authentication/SessionVerificationListener.swift +++ b/Riot/Modules/Authentication/SessionVerificationListener.swift @@ -68,14 +68,7 @@ class SessionVerificationListener { return } - if session.state == .storeDataReady { - if let crypto = session.crypto as? MXLegacyCrypto { - // Do not make key share requests while the "Complete security" is not complete. - // If the device is self-verified, the SDK will restore the existing key backup. - // Then, it will re-enable outgoing key share requests - crypto.setOutgoingKeyRequestsEnabled(false, onComplete: nil) - } - } else if session.state == .running { + if session.state == .running { unregisterSessionStateChangeNotification() if let crypto = session.crypto { @@ -101,7 +94,6 @@ class SessionVerificationListener { self.completion?(.authenticationIsComplete) } failure: { error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed", context: error) - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } else { @@ -111,12 +103,10 @@ class SessionVerificationListener { self.completion?(.authenticationIsComplete) } failure: { error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Do not know how to bootstrap cross-signing. Skip it.") - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } } else { - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } case .crossSigningExists: @@ -124,13 +114,10 @@ class SessionVerificationListener { self.completion?(.needsVerification) default: MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Nothing to do") - - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } failure: { [weak self] error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state", context: error) - (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self?.completion?(.authenticationIsComplete) } } else { diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m index 3e8227e7c..680d330fa 100644 --- a/Riot/Modules/Call/CallViewController.m +++ b/Riot/Modules/Call/CallViewController.m @@ -370,28 +370,16 @@ CallAudioRouteMenuViewDelegate> { typeof(self) self = weakSelf; self->currentAlert = nil; - - // Acknowledge the existence of all devices - [self startActivityIndicator]; - if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + + // Retry the call + if (call.isIncoming) { - MXLogFailure(@"[CallViewController] call: Only legacy crypto supports manual setting of known devices"); - return; + [call answer]; + } + else + { + [call callWithVideo:call.isVideoCall]; } - [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{ - - [self stopActivityIndicator]; - - // Retry the call - if (call.isIncoming) - { - [call answer]; - } - else - { - [call callWithVideo:call.isVideoCall]; - } - }]; } }]]; diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index ea96873ba..6108d01c2 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -988,8 +988,7 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { let title: String let message: String - if let feature = MXSDKOptions.sharedInstance().cryptoSDKFeature, - feature.isEnabled && feature.needsVerificationUpgrade { + if MXSDKOptions.sharedInstance().cryptoMigrationDelegate?.needsVerificationUpgrade == true { title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage } else { diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift index 8398c659d..c4cdee422 100644 --- a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift +++ b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift @@ -69,9 +69,6 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable { extension LaunchLoadingView: MXSessionStartupProgressDelegate { func sessionDidUpdateStartupProgress(state: MXSessionStartupProgress.State) { - guard MXSDKOptions.sharedInstance().enableStartupProgress else { - return - } update(with: state) } diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 548442ab7..1d0382375 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -946,15 +946,7 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes; [MXKRoomDataSourceManager removeSharedManagerForMatrixSession:mxSession]; if (clearStore) - { - // Force a reload of device keys at the next session start, unless we are just about to migrate - // all data and device keys into CryptoSDK. - // This will fix potential UISIs other peoples receive for our messages. - if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]] && !MXSDKOptions.sharedInstance.enableCryptoSDK) - { - [(MXLegacyCrypto *)mxSession.crypto resetDeviceKeys]; - } - + { // Clean other stores [mxSession.scanManager deleteAllAntivirusScans]; [mxSession.aggregations resetData]; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 398e12e8f..70b8d974c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6356,21 +6356,10 @@ static CGSize kThreadListBarButtonItemImageSize; self->currentAlert = nil; // Acknowledge the existence of all devices - [self startActivityIndicator]; + self->unknownDevices = nil; - if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - MXLogFailure(@"[RoomVC] eventDidChangeSentState: Only legacy crypto supports manual setting of known devices"); - return; - } - [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:self->unknownDevices complete:^{ - - self->unknownDevices = nil; - [self stopActivityIndicator]; - - // And resend pending messages - [self resendAllUnsentMessages]; - }]; + // And resend pending messages + [self resendAllUnsentMessages]; } }]]; diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h deleted file mode 100644 index e9db3a583..000000000 --- a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - Copyright 2017 Vector Creations 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 - -#import - -/** - The `RoomKeyRequestViewController` display a modal dialog at the top of the - application asking the user if he wants to share room keys with a user's device. - For the moment, the user is himself. - */ -@interface RoomKeyRequestViewController : NSObject - -/** - The UIAlertController instance which handles the dialog. - */ -@property (nonatomic, readonly) UIAlertController *alertController; - -@property (nonatomic, readonly) MXSession *mxSession; -@property (nonatomic, readonly) MXDeviceInfo *device; - -/** - Initialise an `RoomKeyRequestViewController` instance. - - @param deviceInfo the device to share keys to. - @param wasNewDevice flag indicating whether this is the first time we meet the device. - @param session the related matrix session. - @param crypto the related (legacy) crypto module - @param onComplete a block called when the the dialog is closed. - @return the newly created instance. - */ -- (instancetype)initWithDeviceInfo:(MXDeviceInfo*)deviceInfo - wasNewDevice:(BOOL)wasNewDevice - andMatrixSession:(MXSession*)session - crypto:(MXLegacyCrypto *)crypto - onComplete:(void (^)(void))onComplete; - -/** - Show the dialog in a modal way. - */ -- (void)show; - -/** - Hide the dialog. - */ -- (void)hide; - -@end diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m deleted file mode 100644 index 6f638bd78..000000000 --- a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m +++ /dev/null @@ -1,195 +0,0 @@ -/* - Copyright 2017 Vector Creations 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 "RoomKeyRequestViewController.h" - -#import "GeneratedInterface-Swift.h" - -@interface RoomKeyRequestViewController () -{ - void (^onComplete)(void); - - KeyVerificationCoordinatorBridgePresenter *keyVerificationCoordinatorBridgePresenter; - - BOOL wasNewDevice; -} - -@property (nonatomic, strong) MXLegacyCrypto *crypto; - -@end - -@implementation RoomKeyRequestViewController - -- (instancetype)initWithDeviceInfo:(MXDeviceInfo *)deviceInfo - wasNewDevice:(BOOL)theWasNewDevice - andMatrixSession:(MXSession *)session - crypto:(MXLegacyCrypto *)crypto - onComplete:(void (^)(void))onCompleteBlock -{ - self = [super init]; - if (self) - { - _mxSession = session; - _crypto = crypto; - _device = deviceInfo; - wasNewDevice = theWasNewDevice; - onComplete = onCompleteBlock; - } - return self; -} - -- (void)show -{ - // Show it modally on the root view controller - UIViewController *rootViewController = [AppDelegate theDelegate].window.rootViewController; - if (rootViewController) - { - NSString *title = [VectorL10n e2eRoomKeyRequestTitle]; - NSString *message; - if (wasNewDevice) - { - message = [VectorL10n e2eRoomKeyRequestMessageNewDevice:_device.displayName]; - } - else - { - message = [VectorL10n e2eRoomKeyRequestMessage:_device.displayName]; - } - - _alertController = [UIAlertController alertControllerWithTitle:title - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - __weak typeof(self) weakSelf = self; - - [_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestStartVerification] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self->_alertController = nil; - [self showVerificationView]; - } - }]]; - - [_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestShareWithoutVerifying] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self->_alertController = nil; - - // Accept the received requests from this device - [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ - - self->onComplete(); - }]; - } - }]]; - - [_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestIgnoreRequest] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self->_alertController = nil; - - // Ignore all pending requests from this device - [self.crypto ignoreAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ - - self->onComplete(); - }]; - } - }]]; - - [rootViewController presentViewController:_alertController animated:YES completion:nil]; - } -} - -- (void)hide -{ - if (_alertController) - { - [_alertController dismissViewControllerAnimated:YES completion:nil]; - _alertController = nil; - } -} - - -- (void)showVerificationView -{ - // Show it modally on the root view controller - UIViewController *rootViewController = [AppDelegate theDelegate].window.rootViewController; - if (rootViewController) - { - keyVerificationCoordinatorBridgePresenter = [[KeyVerificationCoordinatorBridgePresenter alloc] initWithSession:_mxSession]; - keyVerificationCoordinatorBridgePresenter.delegate = self; - - [keyVerificationCoordinatorBridgePresenter presentFrom:rootViewController otherUserId:_device.userId otherDeviceId:_device.deviceId animated:YES]; - } -} - -#pragma mark - DeviceVerificationCoordinatorBridgePresenterDelegate - -- (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId -{ - [self dismissKeyVerificationCoordinatorBridgePresenter]; -} - -- (void)keyVerificationCoordinatorBridgePresenterDelegateDidCancel:(KeyVerificationCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [self dismissKeyVerificationCoordinatorBridgePresenter]; -} - -- (void)dismissKeyVerificationCoordinatorBridgePresenter -{ - [keyVerificationCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; - keyVerificationCoordinatorBridgePresenter = nil; - - // Check device new status - [self.crypto downloadKeys:@[self.device.userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { - - MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:self.device.deviceId forUser:self.device.userId]; - if (deviceInfo && deviceInfo.trustLevel.localVerificationStatus == MXDeviceVerified) - { - // Accept the received requests from this device - // As the device is now verified, all other key requests will be automatically accepted. - [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ - - self->onComplete(); - }]; - } - else - { - // Come back to self.alertController - ie, reopen it - [self show]; - } - } failure:^(NSError *error) { - - // Should not happen (the device is in the crypto db) - [self show]; - }]; -} - -@end diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 244e28be0..055841f3f 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -176,8 +176,7 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) LABS_ENABLE_NEW_SESSION_MANAGER, LABS_ENABLE_NEW_CLIENT_INFO_FEATURE, LABS_ENABLE_WYSIWYG_COMPOSER, - LABS_ENABLE_VOICE_BROADCAST, - LABS_ENABLE_CRYPTO_SDK + LABS_ENABLE_VOICE_BROADCAST }; typedef NS_ENUM(NSUInteger, SECURITY) @@ -588,11 +587,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> if (BuildSettings.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; - if ([CryptoSDKFeature.shared canManuallyEnableForUserId:self.mainSession.myUserId]) - { - [sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK]; - } - [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS]; @@ -2587,18 +2581,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableVoiceBroadcastFeature:) forControlEvents:UIControlEventTouchUpInside]; - cell = labelAndSwitchCell; - } - else if (row == LABS_ENABLE_CRYPTO_SDK) - { - MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK; - labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk; - labelAndSwitchCell.mxkSwitch.on = isEnabled; - [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled]; - labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; - cell = labelAndSwitchCell; } } @@ -3372,30 +3354,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.enableVoiceBroadcast = sender.isOn; } -- (void)enableCryptoSDKFeature:(UISwitch *)sender -{ - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:VectorL10n.settingsLabsEnableCryptoSdk - message:VectorL10n.settingsLabsConfirmCryptoSdk - preferredStyle:UIAlertControllerStyleAlert]; - - MXWeakify(self); - [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - self->currentAlert = nil; - - [sender setOn:NO animated:YES]; - }]]; - - [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - [CryptoSDKFeature.shared enable]; - [[AppDelegate theDelegate] reloadMatrixSessions:YES]; - }]]; - - [self presentViewController:confirmationAlert animated:YES completion:nil]; - currentAlert = confirmationAlert; -} - - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; diff --git a/Riot/Modules/UserDevices/UsersDevicesViewController.m b/Riot/Modules/UserDevices/UsersDevicesViewController.m index 3b5b8c9a8..fcd7bd567 100644 --- a/Riot/Modules/UserDevices/UsersDevicesViewController.m +++ b/Riot/Modules/UserDevices/UsersDevicesViewController.m @@ -273,22 +273,12 @@ - (IBAction)onDone:(id)sender { // Acknowledge the existence of all devices before leaving this screen - [self startActivityIndicator]; - if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + [self dismissViewControllerAnimated:YES completion:nil]; + + if (self->onCompleteBlock) { - MXLogFailure(@"[UsersDevicesViewController] onDone: Only legacy crypto supports manual setting of known devices"); - return; + self->onCompleteBlock(YES); } - [(MXLegacyCrypto *)mxSession.crypto setDevicesKnown:usersDevices complete:^{ - - [self stopActivityIndicator]; - [self dismissViewControllerAnimated:YES completion:nil]; - - if (self->onCompleteBlock) - { - self->onCompleteBlock(YES); - } - }]; } - (IBAction)onCancel:(id)sender diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 0c7257dba..5880165e8 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -41,7 +41,6 @@ class NotificationService: UNNotificationServiceExtension { private var ongoingVoIPPushRequests: [String: Bool] = [:] private var userAccount: MXKAccount? - private var isCryptoSDKEnabled = false /// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's private var bestAttemptContents: [String: UNMutableNotificationContent] = [:] @@ -196,13 +195,12 @@ class NotificationService: UNNotificationServiceExtension { self.userAccount = MXKAccountManager.shared()?.activeAccounts.first if let userAccount = userAccount { Self.backgroundServiceInitQueue.sync { - if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { + if NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") self.logMemory() NotificationService.backgroundSyncService = MXBackgroundSyncService( withCredentials: userAccount.mxCredentials, - isCryptoSDKEnabled: isCryptoSDKEnabled, persistTokenDataHandler: { persistTokenDataHandler in MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in @@ -219,16 +217,6 @@ class NotificationService: UNNotificationServiceExtension { } } - /// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require - /// rebuilding `MXBackgroundSyncService` - private func hasChangedCryptoSDK() -> Bool { - guard isCryptoSDKEnabled != MXSDKOptions.sharedInstance().enableCryptoSDK else { - return false - } - isCryptoSDKEnabled = MXSDKOptions.sharedInstance().enableCryptoSDK - return true - } - /// Attempts to preprocess payload and attach room display name to the best attempt content /// - Parameters: /// - eventId: Event identifier to mutate best attempt content diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m index 22d0063be..0e1c74cf7 100644 --- a/RiotShareExtension/Shared/ShareManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -102,11 +102,6 @@ static MXSession *fakeSession; [session setStore:self.fileStore success:^{ MXStrongifyAndReturnIfNil(session); - if ([session.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - ((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now - } - self.selectedRooms = [NSMutableArray array]; for (NSString *roomIdentifier in roomIdentifiers) { MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift index 9c9100087..9cf127bb7 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift @@ -267,17 +267,6 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { let session = sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false) -// MXLog.debug("[QRLoginService] Session created without E2EE support. Inform the interlocutor of finishing") -// guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginFinish, outcome: .success)), -// case .success = await rendezvousService.send(data: requestData) else { -// await teardownRendezvous(state: .failed(error: .rendezvousFailed)) -// return -// } -// -// MXLog.debug("[QRLoginService] Login flow finished, returning session") -// state = .completed(session: session, securityCompleted: false) -// return - let cryptoResult = await withCheckedContinuation { continuation in session.enableCrypto(true) { response in continuation.resume(returning: response) diff --git a/RiotTests/Experiments/CryptoSDKFeatureTests.swift b/RiotTests/Experiments/CryptoSDKFeatureTests.swift deleted file mode 100644 index a512b71c6..000000000 --- a/RiotTests/Experiments/CryptoSDKFeatureTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Copyright 2023 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 XCTest -@testable import Element - -class CryptoSDKFeatureTests: XCTestCase { - class RemoteFeatureClient: RemoteFeaturesClientProtocol { - var isEnabled = false - func isFeatureEnabled(_ feature: String) -> Bool { - isEnabled - } - } - - var remote: RemoteFeatureClient! - var feature: CryptoSDKFeature! - - override func setUp() { - RiotSettings.shared.enableCryptoSDK = false - remote = RemoteFeatureClient() - feature = CryptoSDKFeature(remoteFeature: remote, localTargetPercentage: 0) - } - - override func tearDown() { - RiotSettings.shared.enableCryptoSDK = false - } - - func test_disabledByDefault() { - XCTAssertFalse(feature.isEnabled) - } - - func test_enable() { - feature.enable() - XCTAssertTrue(feature.isEnabled) - } - - func test_enableIfAvailable_remainsEnabledWhenRemoteClientDisabled() { - feature.enable() - remote.isEnabled = false - - feature.enableIfAvailable(forUserId: "alice") - - XCTAssertTrue(feature.isEnabled) - } - - func test_enableIfAvailable_notEnabledIfRemoteFeatureDisabled() { - remote.isEnabled = false - feature.enableIfAvailable(forUserId: "alice") - XCTAssertFalse(feature.isEnabled) - } - - func test_canManuallyEnable() { - remote.isEnabled = false - XCTAssertTrue(feature.canManuallyEnable(forUserId: "alice")) - - remote.isEnabled = true - XCTAssertFalse(feature.canManuallyEnable(forUserId: "alice")) - } - - func test_reset() { - feature.enable() - feature.reset() - XCTAssertFalse(RiotSettings.shared.enableCryptoSDK) - } -} diff --git a/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m index 34ebb66e9..5bc037790 100644 --- a/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m +++ b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m @@ -117,12 +117,6 @@ self.selectedRoom = [MXRoom loadRoomFromStore:fileStore withRoomId:roomID matrixSession:session]; - // Do not warn for unknown devices. We have cross-signing now - if ([session.crypto isKindOfClass:[MXLegacyCrypto class]]) - { - ((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO; - } - MXWeakify(self); [self.selectedRoom sendTextMessage:intent.content threadId:nil diff --git a/changelog.d/pr-7508.change b/changelog.d/pr-7508.change new file mode 100644 index 000000000..dbe206b34 --- /dev/null +++ b/changelog.d/pr-7508.change @@ -0,0 +1 @@ +Crypto: Deprecate MXLegacyCrypto From 5ef0b273a0476666f24bb5aa1cc639d6bb6eb25b Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 17:41:11 +0100 Subject: [PATCH 057/149] changelog.d: Upgrade MatrixSDK version ([v0.26.9](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.9)). --- 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 f716233ae..f4cbeda08 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,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.26.7' +$matrixSDKVersion = '= 0.26.9' # $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..056205056 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixSDK version ([v0.26.9](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.9)). \ No newline at end of file From 0d33a7d96e1075fc3a8f4f7a4ada984ceeb7869e Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 17:41:11 +0100 Subject: [PATCH 058/149] version++ --- CHANGES.md | 19 +++++++++++++++++++ changelog.d/7442.change | 1 - changelog.d/7451.bugfix | 1 - changelog.d/7476.build | 1 - changelog.d/7492.bugfix | 1 - changelog.d/pr-7482.bugfix | 1 - changelog.d/pr-7501.bugfix | 1 - changelog.d/x-nolink-0.change | 1 - 8 files changed, 19 insertions(+), 7 deletions(-) delete mode 100644 changelog.d/7442.change delete mode 100644 changelog.d/7451.bugfix delete mode 100644 changelog.d/7476.build delete mode 100644 changelog.d/7492.bugfix delete mode 100644 changelog.d/pr-7482.bugfix delete mode 100644 changelog.d/pr-7501.bugfix delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index af7fdc039..428bd30f7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,22 @@ +## Changes in 1.10.11 (2023-04-18) + +🙌 Improvements + +- Upgrade MatrixSDK version ([v0.26.9](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.9)). +- Labs: Rich Text Editor: Integrate version 2.0.0 with mention Pills support. ([#7442](https://github.com/vector-im/element-ios/issues/7442)) + +🐛 Bugfixes + +- Continue to display pills for matrix.to permalinks if a custom permalinkBaseUrl is set. ([#7482](https://github.com/vector-im/element-ios/pull/7482)) +- Add a foreground color attribute for the unformattable event error message. ([#7501](https://github.com/vector-im/element-ios/pull/7501)) +- Fixed a bug that prevented audio messages that were not .mp4 to be played in the timeline ([#7451](https://github.com/vector-im/element-ios/issues/7451)) +- Fix user suggestion list item height on iOS 16+ ([#7492](https://github.com/vector-im/element-ios/issues/7492)) + +🧱 Build + +- Pinned used Xcode version to 14.2 as newer version fail ASC validation ([#7476](https://github.com/vector-im/element-ios/issues/7476)) + + ## Changes in 1.10.10 (2023-04-12) 🙌 Improvements diff --git a/changelog.d/7442.change b/changelog.d/7442.change deleted file mode 100644 index f8ae96d5b..000000000 --- a/changelog.d/7442.change +++ /dev/null @@ -1 +0,0 @@ -Labs: Rich Text Editor: Integrate version 2.0.0 with mention Pills support. diff --git a/changelog.d/7451.bugfix b/changelog.d/7451.bugfix deleted file mode 100644 index 3a80a3b90..000000000 --- a/changelog.d/7451.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug that prevented audio messages that were not .mp4 to be played in the timeline \ No newline at end of file diff --git a/changelog.d/7476.build b/changelog.d/7476.build deleted file mode 100644 index 09ac016f1..000000000 --- a/changelog.d/7476.build +++ /dev/null @@ -1 +0,0 @@ -Pinned used Xcode version to 14.2 as newer version fail ASC validation \ No newline at end of file diff --git a/changelog.d/7492.bugfix b/changelog.d/7492.bugfix deleted file mode 100644 index a9ff595c9..000000000 --- a/changelog.d/7492.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix user suggestion list item height on iOS 16+ diff --git a/changelog.d/pr-7482.bugfix b/changelog.d/pr-7482.bugfix deleted file mode 100644 index 842d621b2..000000000 --- a/changelog.d/pr-7482.bugfix +++ /dev/null @@ -1 +0,0 @@ -Continue to display pills for matrix.to permalinks if a custom permalinkBaseUrl is set. diff --git a/changelog.d/pr-7501.bugfix b/changelog.d/pr-7501.bugfix deleted file mode 100644 index 2d6150507..000000000 --- a/changelog.d/pr-7501.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add a foreground color attribute for the unformattable event error message. diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index 056205056..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixSDK version ([v0.26.9](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.9)). \ No newline at end of file From c009d11e950f82e1c97c898cf07154c7db189bd9 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 12:02:01 +0100 Subject: [PATCH 059/149] Tidy up event formatter issues. --- .../Utils/EventFormatter/MXKEventFormatter.m | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 9c9d7f428..e2a9e4a01 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -344,7 +344,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* if ((RiotSettings.shared.enableThreads && [mxSession.threadingService isEventThreadRoot:event]) || _settings.showRedactionsInRoomHistory) { - MXLogDebug(@"[MXKEventFormatter] Redacted event %@ (%@)", event.description, event.redactedBecause); + MXLogDebug(@"[MXKEventFormatter] Redacted event %@ (%@)", event.eventId, event.redactedBecause); NSString *redactorId = event.redactedBecause[@"sender"]; NSString *redactedBy = @""; @@ -1316,7 +1316,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* // Check attachment validity if (![self isSupportedAttachment:event]) { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment %@", event.description); + MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment in event %@", event.eventId); body = [VectorL10n noticeInvalidAttachment]; *error = MXKEventFormatterErrorUnsupported; } @@ -1326,7 +1326,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* body = body? body : [VectorL10n noticeAudioAttachment]; if (![self isSupportedAttachment:event]) { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment %@", event.description); + MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment in event %@", event.eventId); if (_isForSubtitle || !_settings.showUnsupportedEventsInRoomHistory) { body = [VectorL10n noticeInvalidAttachment]; @@ -1343,7 +1343,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* body = body? body : [VectorL10n noticeVideoAttachment]; if (![self isSupportedAttachment:event]) { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment %@", event.description); + MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment in event %@", event.eventId); if (_isForSubtitle || !_settings.showUnsupportedEventsInRoomHistory) { body = [VectorL10n noticeInvalidAttachment]; @@ -1374,14 +1374,14 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* } else { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported m.file format: %@", event.description); + MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported m.file format in event: %@", event.eventId); *error = MXKEventFormatterErrorUnsupported; } } } else { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment %@", event.description); + MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment in event %@", event.eventId); body = [VectorL10n noticeInvalidAttachment]; *error = MXKEventFormatterErrorUnsupported; } @@ -1620,7 +1620,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* // Check sticker validity if (![self isSupportedAttachment:event]) { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported sticker %@", event.description); + MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported sticker in event %@", event.eventId); body = [VectorL10n noticeInvalidAttachment]; *error = MXKEventFormatterErrorUnsupported; } @@ -1674,7 +1674,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* if (!attributedDisplayText) { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported event %@)", event.description); + MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported event %@)", event.eventId); if (_settings.showUnsupportedEventsInRoomHistory) { if (MXKEventFormatterErrorNone == *error) @@ -1914,7 +1914,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* // No message content in a non-redacted event. Formatter should use fallback. if (!repliedEventContent) { - MXLogWarning(@"[MXKEventFormatter] Unable to retrieve content from replied event %@", repliedEvent.description) + MXLogWarning(@"[MXKEventFormatter] Unable to retrieve content from replied event %@", repliedEvent.eventId) return nil; } } @@ -1949,7 +1949,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* } else { - MXLogWarning(@"[MXKEventFormatter] Unable to build reply event %@", event.description) + MXLogWarning(@"[MXKEventFormatter] Unable to build reply event %@", event.eventId) } return html; From 6007acac0c97f6be22b0106a0a31ab53a1a4aa4a Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 20:11:34 +0100 Subject: [PATCH 060/149] finish version++ --- Podfile.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index c4d534f96..f47ccb4f3 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -39,20 +39,20 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.26.7): - - MatrixSDK/Core (= 0.26.7) - - MatrixSDK/Core (0.26.7): + - MatrixSDK (0.26.9): + - MatrixSDK/Core (= 0.26.9) + - MatrixSDK/Core (0.26.9): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - - MatrixSDKCrypto (= 0.3.3) + - MatrixSDKCrypto (= 0.3.4) - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.26.7): + - MatrixSDK/JingleCallStack (0.26.9): - JitsiMeetSDKLite (= 7.0.1-lite) - MatrixSDK/Core - - MatrixSDKCrypto (0.3.3) + - MatrixSDKCrypto (0.3.4) - OLMKit (3.2.12): - OLMKit/olmc (= 3.2.12) - OLMKit/olmcpp (= 3.2.12) @@ -102,8 +102,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.26.7) - - MatrixSDK/JingleCallStack (= 0.26.7) + - MatrixSDK (= 0.26.9) + - MatrixSDK/JingleCallStack (= 0.26.9) - OLMKit - PostHog (~> 2.0.0) - ReadMoreTextView (~> 3.0.1) @@ -187,8 +187,8 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 1de7cd06bef00fabf5693eabcdcdbf2aa1978063 - MatrixSDKCrypto: 427dbb126a3e3f97cadf9fc407abf17d365b4b39 + MatrixSDK: 2f6222978156818cf4c6ba590762ade601ba72f9 + MatrixSDKCrypto: ac805c22c24f79f349cdbfa065855c73a4c81b51 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -208,6 +208,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: c063d05ddb39617ab9a259c4c9c6b57da2e6d8b6 +PODFILE CHECKSUM: a55fb48d3bef5f5e24fcaf8c39d1eae1ed8c1603 COCOAPODS: 1.11.3 From ec010204f7a8ff2e28dbb23b5c465bab694d246c Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 20:11:40 +0100 Subject: [PATCH 061/149] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 5f74246b2..3e986e4b1 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.11 -CURRENT_PROJECT_VERSION = 1.10.11 +MARKETING_VERSION = 1.10.12 +CURRENT_PROJECT_VERSION = 1.10.12 From fc350b546dfcbbefaf0d6bf2db30a94c39f3fe00 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 13 Mar 2023 16:18:11 +0000 Subject: [PATCH 062/149] Refactor encryption trust level --- Riot/Categories/MXRoom+Riot.m | 30 +-- Riot/Categories/MXRoomSummary+Riot.h | 12 +- Riot/Categories/MXRoomSummary+Riot.m | 29 +-- .../EncryptionInfo/EncryptionInfoView.h | 0 .../EncryptionInfo/EncryptionInfoView.m | 0 .../EncryptionInfo/EncryptionInfoView.xib | 0 .../Encryption/EncryptionTrustLevel.swift | 68 +++++++ ...EncryptionTrustLevelBadgeImageHelper.swift | 0 .../Encryption/RoomEncryptionTrustLevel.h | 25 +++ .../UserEncryptionTrustLevel.h | 0 Riot/SupportingFiles/Riot-Bridging-Header.h | 1 + .../RiotShareExtension-Bridging-Header.h | 2 + RiotShareExtension/target.yml | 1 + .../EncryptionTrustLevelTests.swift | 177 ++++++++++++++++++ 14 files changed, 286 insertions(+), 59 deletions(-) rename Riot/Modules/{ => Encryption}/EncryptionInfo/EncryptionInfoView.h (100%) rename Riot/Modules/{ => Encryption}/EncryptionInfo/EncryptionInfoView.m (100%) rename Riot/Modules/{ => Encryption}/EncryptionInfo/EncryptionInfoView.xib (100%) create mode 100644 Riot/Modules/Encryption/EncryptionTrustLevel.swift rename Riot/{Utils => Modules/Encryption}/EncryptionTrustLevelBadgeImageHelper.swift (100%) create mode 100644 Riot/Modules/Encryption/RoomEncryptionTrustLevel.h rename Riot/Modules/{Room/Members/Detail => Encryption}/UserEncryptionTrustLevel.h (100%) create mode 100644 RiotTests/Modules/Encryption/EncryptionTrustLevelTests.swift diff --git a/Riot/Categories/MXRoom+Riot.m b/Riot/Categories/MXRoom+Riot.m index 04538ba80..b81da1759 100644 --- a/Riot/Categories/MXRoom+Riot.m +++ b/Riot/Categories/MXRoom+Riot.m @@ -20,7 +20,7 @@ #import "AvatarGenerator.h" #import "MatrixKit.h" - +#import "GeneratedInterface-Swift.h" #import @implementation MXRoom (Riot) @@ -331,30 +331,10 @@ { [self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] forceDownload:NO success:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) { - UserEncryptionTrustLevel userEncryptionTrustLevel; - double trustedDevicesPercentage = usersTrustLevelSummary.trustedDevicesProgress.fractionCompleted; - - if (trustedDevicesPercentage >= 1.0) - { - userEncryptionTrustLevel = UserEncryptionTrustLevelTrusted; - } - else if (trustedDevicesPercentage == 0.0) - { - // Verify if the user has the user has cross-signing enabled - if ([self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId]) - { - userEncryptionTrustLevel = UserEncryptionTrustLevelNotVerified; - } - else - { - userEncryptionTrustLevel = UserEncryptionTrustLevelNoCrossSigning; - } - } - else - { - userEncryptionTrustLevel = UserEncryptionTrustLevelWarning; - } - + MXCrossSigningInfo *crossSigningInfo = [self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId]; + EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init]; + UserEncryptionTrustLevel userEncryptionTrustLevel = [encryption userTrustLevelWithCrossSigning:crossSigningInfo + trustedDevicesProgress:usersTrustLevelSummary.trustedDevicesProgress]; onComplete(userEncryptionTrustLevel); } failure:^(NSError *error) { diff --git a/Riot/Categories/MXRoomSummary+Riot.h b/Riot/Categories/MXRoomSummary+Riot.h index d25cdee5f..324a7f369 100644 --- a/Riot/Categories/MXRoomSummary+Riot.h +++ b/Riot/Categories/MXRoomSummary+Riot.h @@ -15,17 +15,7 @@ */ #import "MatrixKit.h" - -/** - RoomEncryptionTrustLevel represents the trust level in an encrypted room. - */ -typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) { - RoomEncryptionTrustLevelTrusted, - RoomEncryptionTrustLevelWarning, - RoomEncryptionTrustLevelNormal, - RoomEncryptionTrustLevelUnknown -}; - +#import "RoomEncryptionTrustLevel.h" /** Define a `MXRoomSummary` category at Riot level. diff --git a/Riot/Categories/MXRoomSummary+Riot.m b/Riot/Categories/MXRoomSummary+Riot.m index c6a55a230..b2c1eeb40 100644 --- a/Riot/Categories/MXRoomSummary+Riot.m +++ b/Riot/Categories/MXRoomSummary+Riot.m @@ -33,32 +33,15 @@ - (RoomEncryptionTrustLevel)roomEncryptionTrustLevel { - RoomEncryptionTrustLevel roomEncryptionTrustLevel = RoomEncryptionTrustLevelUnknown; - if (self.trust) + MXUsersTrustLevelSummary *trust = self.trust; + if (!trust) { - double trustedUsersPercentage = self.trust.trustedUsersProgress.fractionCompleted; - double trustedDevicesPercentage = self.trust.trustedDevicesProgress.fractionCompleted; - - if (trustedUsersPercentage >= 1.0) - { - if (trustedDevicesPercentage >= 1.0) - { - roomEncryptionTrustLevel = RoomEncryptionTrustLevelTrusted; - } - else - { - roomEncryptionTrustLevel = RoomEncryptionTrustLevelWarning; - } - } - else - { - roomEncryptionTrustLevel = RoomEncryptionTrustLevelNormal; - } - - roomEncryptionTrustLevel = roomEncryptionTrustLevel; + MXLogError(@"[MXRoomSummary] roomEncryptionTrustLevel: trust is missing"); + return RoomEncryptionTrustLevelUnknown; } - return roomEncryptionTrustLevel; + EncryptionTrustLevel *encryption = [[EncryptionTrustLevel alloc] init]; + return [encryption roomTrustLevelWithSummary:trust]; } - (BOOL)isJoined diff --git a/Riot/Modules/EncryptionInfo/EncryptionInfoView.h b/Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.h similarity index 100% rename from Riot/Modules/EncryptionInfo/EncryptionInfoView.h rename to Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.h diff --git a/Riot/Modules/EncryptionInfo/EncryptionInfoView.m b/Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.m similarity index 100% rename from Riot/Modules/EncryptionInfo/EncryptionInfoView.m rename to Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.m diff --git a/Riot/Modules/EncryptionInfo/EncryptionInfoView.xib b/Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.xib similarity index 100% rename from Riot/Modules/EncryptionInfo/EncryptionInfoView.xib rename to Riot/Modules/Encryption/EncryptionInfo/EncryptionInfoView.xib diff --git a/Riot/Modules/Encryption/EncryptionTrustLevel.swift b/Riot/Modules/Encryption/EncryptionTrustLevel.swift new file mode 100644 index 000000000..275d74ffc --- /dev/null +++ b/Riot/Modules/Encryption/EncryptionTrustLevel.swift @@ -0,0 +1,68 @@ +// +// Copyright 2023 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 + +/// Object responsible for calculating user and room trust level +/// +/// For legacy reasons, the trust of multiple items is represented as `Progress` object, +/// where `completedUnitCount` represents the number of trusted users / devices. +@objc class EncryptionTrustLevel: NSObject { + struct TrustSummary { + let totalCount: Int64 + let trustedCount: Int64 + let areAllTrusted: Bool + + init(progress: Progress) { + totalCount = max(progress.totalUnitCount, progress.completedUnitCount) + trustedCount = progress.completedUnitCount + areAllTrusted = trustedCount == totalCount + } + } + + + /// Calculate trust level for a single user given their cross-signing info + @objc func userTrustLevel( + crossSigning: MXCrossSigningInfo?, + trustedDevicesProgress: Progress + ) -> UserEncryptionTrustLevel { + let devices = TrustSummary(progress: trustedDevicesProgress) + + // If we could cross-sign but we haven't, the user is simply not verified + if let crossSigning, !crossSigning.trustLevel.isVerified { + return .notVerified + + // If we cannot cross-sign the user (legacy behaviour) and have not signed + // any devices manually, the user is not verified + } else if crossSigning == nil && devices.trustedCount == 0 { + return .notVerified + } + + // In all other cases we check devices for trust level + return devices.areAllTrusted ? .trusted : .warning + } + + /// Calculate trust level for a room given trust level of users and their devices + @objc func roomTrustLevel(summary: MXUsersTrustLevelSummary) -> RoomEncryptionTrustLevel { + let users = TrustSummary(progress: summary.trustedUsersProgress) + let devices = TrustSummary(progress: summary.trustedDevicesProgress) + + guard users.totalCount > 0 && users.areAllTrusted else { + return .normal + } + return devices.areAllTrusted ? .trusted : .warning + } +} diff --git a/Riot/Utils/EncryptionTrustLevelBadgeImageHelper.swift b/Riot/Modules/Encryption/EncryptionTrustLevelBadgeImageHelper.swift similarity index 100% rename from Riot/Utils/EncryptionTrustLevelBadgeImageHelper.swift rename to Riot/Modules/Encryption/EncryptionTrustLevelBadgeImageHelper.swift diff --git a/Riot/Modules/Encryption/RoomEncryptionTrustLevel.h b/Riot/Modules/Encryption/RoomEncryptionTrustLevel.h new file mode 100644 index 000000000..a942f5360 --- /dev/null +++ b/Riot/Modules/Encryption/RoomEncryptionTrustLevel.h @@ -0,0 +1,25 @@ +// +// Copyright 2023 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. +// + +/** + RoomEncryptionTrustLevel represents the trust level in an encrypted room. + */ +typedef NS_ENUM(NSUInteger, RoomEncryptionTrustLevel) { + RoomEncryptionTrustLevelTrusted, + RoomEncryptionTrustLevelWarning, + RoomEncryptionTrustLevelNormal, + RoomEncryptionTrustLevelUnknown +}; diff --git a/Riot/Modules/Room/Members/Detail/UserEncryptionTrustLevel.h b/Riot/Modules/Encryption/UserEncryptionTrustLevel.h similarity index 100% rename from Riot/Modules/Room/Members/Detail/UserEncryptionTrustLevel.h rename to Riot/Modules/Encryption/UserEncryptionTrustLevel.h diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index e86152e1c..296545a4e 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -18,6 +18,7 @@ #import "RoomBubbleCellData.h" #import "MXKRoomBubbleTableViewCell+Riot.h" #import "UserEncryptionTrustLevel.h" +#import "RoomEncryptionTrustLevel.h" #import "RoomReactionsViewSizer.h" #import "RoomEncryptedDataBubbleCell.h" #import "LegacyAppDelegate.h" diff --git a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h index 9a7cf3af1..618849c4d 100644 --- a/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h +++ b/RiotShareExtension/SupportingFiles/RiotShareExtension-Bridging-Header.h @@ -6,6 +6,8 @@ #import "AvatarGenerator.h" #import "BuildInfo.h" #import "ShareItemSender.h" +#import "UserEncryptionTrustLevel.h" +#import "RoomEncryptionTrustLevel.h" // MatrixKit imports #import "MatrixKit-Bridging-Header.h" diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index eaf51ce3c..b289f234b 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -87,3 +87,4 @@ targets: - "**/*.md" # excludes all files with the .md extension - path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK + - path: ../Riot/Modules/Encryption/EncryptionTrustLevel.swift diff --git a/RiotTests/Modules/Encryption/EncryptionTrustLevelTests.swift b/RiotTests/Modules/Encryption/EncryptionTrustLevelTests.swift new file mode 100644 index 000000000..f038ba5e2 --- /dev/null +++ b/RiotTests/Modules/Encryption/EncryptionTrustLevelTests.swift @@ -0,0 +1,177 @@ +// +// Copyright 2023 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 XCTest +@testable import Element +@testable import MatrixSDK + +class EncryptionTrustLevelTests: XCTestCase { + + var encryption: EncryptionTrustLevel! + override func setUp() { + encryption = EncryptionTrustLevel() + } + + // MARK: - Helpers + + func makeCrossSigning(isVerified: Bool) -> MXCrossSigningInfo { + return .init( + userIdentity: .init( + identity: .other( + userId: "Bob", + masterKey: "MSK", + selfSigningKey: "SSK" + ), + isVerified: isVerified + ) + ) + } + + func makeProgress(trusted: Int, total: Int) -> Progress { + let progress = Progress(totalUnitCount: Int64(total)) + progress.completedUnitCount = Int64(trusted) + return progress + } + + // MARK: - Users + + func test_userTrustLevel_whenCrossSigningDisabled() { + let devicesToTrustLevel: [(Progress, UserEncryptionTrustLevel)] = [ + (makeProgress(trusted: 0, total: 0), .notVerified), + (makeProgress(trusted: 0, total: 2), .notVerified), + (makeProgress(trusted: 1, total: 2), .warning), + (makeProgress(trusted: 3, total: 4), .warning), + (makeProgress(trusted: 5, total: 5), .trusted), + (makeProgress(trusted: 10, total: 5), .trusted) + ] + + for (devices, expected) in devicesToTrustLevel { + let trustLevel = encryption.userTrustLevel( + crossSigning: nil, + trustedDevicesProgress: devices + ) + XCTAssertEqual(trustLevel, expected, "\(devices.completedUnitCount) trusted device(s) out of \(devices.totalUnitCount)") + } + } + + func test_userTrustLevel_whenCrossSigningNotVerified() { + let devicesToTrustLevel: [(Progress, UserEncryptionTrustLevel)] = [ + (makeProgress(trusted: 0, total: 0), .notVerified), + (makeProgress(trusted: 0, total: 2), .notVerified), + (makeProgress(trusted: 1, total: 2), .notVerified), + (makeProgress(trusted: 3, total: 4), .notVerified), + (makeProgress(trusted: 5, total: 5), .notVerified), + (makeProgress(trusted: 10, total: 5), .notVerified) + ] + + for (devices, expected) in devicesToTrustLevel { + let trustLevel = encryption.userTrustLevel( + crossSigning: makeCrossSigning(isVerified: false), + trustedDevicesProgress: devices + ) + XCTAssertEqual(trustLevel, expected, "\(devices.completedUnitCount) trusted device(s) out of \(devices.totalUnitCount)") + } + } + + func test_userTrustLevel_whenCrossSigningVerified() { + let devicesToTrustLevel: [(Progress, UserEncryptionTrustLevel)] = [ + (makeProgress(trusted: 0, total: 0), .trusted), + (makeProgress(trusted: 0, total: 2), .warning), + (makeProgress(trusted: 1, total: 2), .warning), + (makeProgress(trusted: 3, total: 4), .warning), + (makeProgress(trusted: 5, total: 5), .trusted), + (makeProgress(trusted: 10, total: 5), .trusted) + ] + + for (devices, expected) in devicesToTrustLevel { + let trustLevel = encryption.userTrustLevel( + crossSigning: makeCrossSigning(isVerified: true), + trustedDevicesProgress: devices + ) + XCTAssertEqual(trustLevel, expected, "\(devices.completedUnitCount) trusted device(s) out of \(devices.totalUnitCount)") + } + } + + // MARK: - Rooms + + func test_roomTrustLevel() { + let usersDevicesToTrustLevel: [(Progress, Progress, RoomEncryptionTrustLevel)] = [ + // No users verified + (makeProgress(trusted: 0, total: 0), makeProgress(trusted: 0, total: 0), .normal), + + // Only some users verified + (makeProgress(trusted: 0, total: 1), makeProgress(trusted: 0, total: 1), .normal), + (makeProgress(trusted: 3, total: 4), makeProgress(trusted: 5, total: 5), .normal), + (makeProgress(trusted: 3, total: 4), makeProgress(trusted: 5, total: 5), .normal), + + // All users verified + (makeProgress(trusted: 2, total: 2), makeProgress(trusted: 0, total: 0), .trusted), + (makeProgress(trusted: 3, total: 3), makeProgress(trusted: 0, total: 1), .warning), + (makeProgress(trusted: 3, total: 3), makeProgress(trusted: 3, total: 4), .warning), + (makeProgress(trusted: 4, total: 4), makeProgress(trusted: 5, total: 5), .trusted), + (makeProgress(trusted: 10, total: 4), makeProgress(trusted: 10, total: 5), .trusted), + ] + + for (users, devices, expected) in usersDevicesToTrustLevel { + let trustLevel = encryption.roomTrustLevel( + summary: MXUsersTrustLevelSummary( + trustedUsersProgress: users, + andTrustedDevicesProgress: devices + ) + ) + XCTAssertEqual(trustLevel, expected, "\(users.completedUnitCount)/\(users.totalUnitCount) trusted users(s), \(devices.completedUnitCount)/\(devices.totalUnitCount) trusted device(s)") + } + } +} + +extension UserEncryptionTrustLevel: CustomStringConvertible { + public var description: String { + switch self { + case .trusted: + return "trusted" + case .warning: + return "warning" + case .notVerified: + return "notVerified" + case .noCrossSigning: + return "noCrossSigning" + case .none: + return "none" + case .unknown: + return "unknown" + @unknown default: + return "unknown" + } + } +} + +extension RoomEncryptionTrustLevel: CustomStringConvertible { + public var description: String { + switch self { + case .trusted: + return "trusted" + case .warning: + return "warning" + case .normal: + return "normal" + case .unknown: + return "unknown" + @unknown default: + return "unknown" + } + } +} From 23e34f06dff9054968cae985f2dccf1c382ee5b8 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 18 Apr 2023 11:37:34 +0200 Subject: [PATCH 063/149] Fix: Calculation of the frame for a bubble component --- .../MXKRoomBubbleTableViewCell+Riot.m | 38 ++++++++++++------- .../Models/Room/MXKRoomBubbleCellData.h | 9 +++++ .../Models/Room/MXKRoomBubbleCellData.m | 17 +++++++-- ...eOutgoingWithoutSenderInfoBubbleCell.swift | 9 +++++ changelog.d/pr-7512.bugfix | 1 + 5 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 changelog.d/pr-7512.bugfix diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index a7bfd69f1..907dd5ff2 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -600,36 +600,47 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = } else if (roomBubbleTableViewCell.messageTextView) { + // Force the textView used underneath to layout its frame properly + [roomBubbleTableViewCell setNeedsLayout]; + [roomBubbleTableViewCell layoutIfNeeded]; + + // Compute the height CGFloat textMessageHeight = 0; - if ([bubbleCellData isKindOfClass:[RoomBubbleCellData class]]) { RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleCellData; if (!roomBubbleCellData.attachment && selectedComponent.attributedTextMessage) { - textMessageHeight = [roomBubbleCellData rawTextHeight:selectedComponent.attributedTextMessage]; + // Get the width of messageTextView to compute the needed height + CGFloat maxTextWidth = CGRectGetWidth(roomBubbleTableViewCell.messageTextView.bounds); + + // Compute text message height + textMessageHeight = [roomBubbleCellData rawTextHeight:selectedComponent.attributedTextMessage withMaxWidth:maxTextWidth]; } } - - selectedComponentPositionY = selectedComponent.position.y; - + + // Get the messageText frame in the cell content view (as the messageTextView may be inside a stackView and not directly a child of the tableViewCell) + UITextView *messageTextView = roomBubbleTableViewCell.messageTextView; + CGRect messageTextViewFrame = [messageTextView convertRect:messageTextView.bounds toView:roomBubbleTableViewCell.contentView]; + if (textMessageHeight > 0) { selectedComponentHeight = textMessageHeight; } else { - selectedComponentHeight = roomBubbleTableViewCell.frame.size.height - selectedComponentPositionY; + // if we don't have a height, use the messageTextView height without the text container vertical insets to stay aligned with the text. + selectedComponentHeight = CGRectGetHeight(messageTextViewFrame) - messageTextView.textContainerInset.top - messageTextView.textContainerInset.bottom; } - // Force the textView used underneath to layout its frame properly - [roomBubbleTableViewCell setNeedsLayout]; - [roomBubbleTableViewCell layoutIfNeeded]; - - selectedComponenContentViewYOffset = roomBubbleTableViewCell.messageTextView.frame.origin.y; + // Get the vertical position of the messageTextView relative to the contentView + selectedComponenContentViewYOffset = CGRectGetMinY(messageTextViewFrame); + + // Get the position of the component inside the messageTextView + selectedComponentPositionY = selectedComponent.position.y; } - + if (roomBubbleTableViewCell.attachmentView || roomBubbleTableViewCell.messageTextView) { CGFloat x = 0; @@ -801,8 +812,7 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = - (void)addTickView:(UIView *)tickView atIndex:(NSInteger)index { - CGRect componentFrame = [self componentFrameInContentViewForIndex: index]; - + CGRect componentFrame = [self componentFrameInContentViewForIndex:index]; tickView.frame = CGRectMake(self.contentView.bounds.size.width - tickView.frame.size.width - 2 * PlainRoomCellLayoutConstants.readReceiptsViewRightMargin, CGRectGetMaxY(componentFrame) - tickView.frame.size.height, tickView.frame.size.width, tickView.frame.size.height); [self.contentView addSubview:tickView]; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h index e934567b7..df9d12900 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h @@ -144,6 +144,15 @@ */ - (CGFloat)rawTextHeight:(NSAttributedString*)attributedText; +/** + Return the raw height of the provided text by removing any vertical margin/inset and constraining the width. + + @param attributedText the attributed text to measure + @param maxTextViewWidth the maximum text width + @return the computed height + */ +- (CGFloat)rawTextHeight:(NSAttributedString*)attributedText withMaxWidth:(CGFloat)maxTextViewWidth; + /** Return the content size of a text view initialized with the provided attributed text. CAUTION: This method runs only on main thread. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index 53b084c3a..c9a13d979 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -500,23 +500,34 @@ // Return the raw height of the provided text by removing any margin - (CGFloat)rawTextHeight: (NSAttributedString*)attributedText +{ + return [self rawTextHeight:attributedText withMaxWidth:_maxTextViewWidth]; +} + +// Return the raw height of the provided text by removing any vertical margin/inset and constraining the width. +- (CGFloat)rawTextHeight: (NSAttributedString*)attributedText withMaxWidth:(CGFloat)maxTextViewWidth { __block CGSize textSize; if ([NSThread currentThread] != [NSThread mainThread]) { dispatch_sync(dispatch_get_main_queue(), ^{ - textSize = [self textContentSize:attributedText removeVerticalInset:YES]; + textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth]; }); } else { - textSize = [self textContentSize:attributedText removeVerticalInset:YES]; + textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth]; } return textSize.height; } - (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset +{ + return [self textContentSize:attributedText removeVerticalInset:removeVerticalInset maxTextViewWidth:_maxTextViewWidth]; +} + +- (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset maxTextViewWidth:(CGFloat)maxTextViewWidth { static UITextView* measurementTextView = nil; static UITextView* measurementTextViewWithoutInset = nil; @@ -536,7 +547,7 @@ // Select the right text view for measurement UITextView *selectedTextView = (removeVerticalInset ? measurementTextViewWithoutInset : measurementTextView); - selectedTextView.frame = CGRectMake(0, 0, _maxTextViewWidth, 0); + selectedTextView.frame = CGRectMake(0, 0, maxTextViewWidth, 0); selectedTextView.attributedText = attributedText; // Force the layout manager to layout the text, fixes problems starting iOS 16 diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Outgoing/TextMessageOutgoingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Outgoing/TextMessageOutgoingWithoutSenderInfoBubbleCell.swift index de15e91d3..f3f00f12f 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Outgoing/TextMessageOutgoingWithoutSenderInfoBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Outgoing/TextMessageOutgoingWithoutSenderInfoBubbleCell.swift @@ -35,6 +35,15 @@ class TextMessageOutgoingWithoutSenderInfoBubbleCell: TextMessageBaseBubbleCell, self.textMessageContentView?.bubbleBackgroundView?.backgroundColor = theme.roomCellOutgoingBubbleBackgroundColor } + override func render(_ cellData: MXKCellData!) { + // This cell displays an outgoing message without any sender information. + // However, we need to set the following properties to our cellData, otherwise, to make room for the timestamp, a whitespace could be added when calculating the position of the components. + // If we don't, the component frame calculation will not work for this cell. + (cellData as? RoomBubbleCellData)?.shouldHideSenderName = false + (cellData as? RoomBubbleCellData)?.shouldHideSenderInformation = false + super.render(cellData) + } + // MARK: - Private private func setupBubbleConstraints() { diff --git a/changelog.d/pr-7512.bugfix b/changelog.d/pr-7512.bugfix new file mode 100644 index 000000000..1c6d3a98d --- /dev/null +++ b/changelog.d/pr-7512.bugfix @@ -0,0 +1 @@ +Fix the position of the send confirmation icon. From 9c172184bfc501b680a7af2b22edb50290122bbb Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 19 Apr 2023 10:45:40 +0200 Subject: [PATCH 064/149] Add basic slash commands support to UserSuggestion module --- Riot/Modules/Room/RoomViewController.m | 8 ++ Riot/Modules/Room/RoomViewController.swift | 16 ++++ .../WysiwygInputToolbarView.swift | 4 + .../UserSuggestionCoordinator.swift | 42 ++++++++- .../UserSuggestionCoordinatorBridge.swift | 5 + .../Service/UserSuggestionService.swift | 93 ++++++++++++++----- .../UserSuggestionServiceProtocol.swift | 11 ++- .../UserSuggestion/UserSuggestionModels.swift | 16 +++- .../UserSuggestionScreenState.swift | 13 ++- .../UserSuggestionViewModel.swift | 18 +++- .../View/UserSuggestionList.swift | 17 ++-- .../View/UserSuggestionListItem.swift | 43 +++++---- 12 files changed, 226 insertions(+), 60 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 70b8d974c..57a431d2e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -8076,6 +8076,14 @@ static CGSize kThreadListBarButtonItemImageSize; [self.inputToolbarView pasteText:[UserSuggestionID.room stringByAppendingString:@" "]]; } +- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator + didRequestCommand:(NSString *)command + textTrigger:(NSString *)textTrigger +{ + [self removeTriggerTextFromComposer:textTrigger]; + [self setCommand:command]; +} + - (void)removeTriggerTextFromComposer:(NSString *)textTrigger { RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView; diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 3fec13de9..c94111be4 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -58,6 +58,22 @@ extension RoomViewController { } } + @objc func setCommand(_ command: String) { + if let wysiwygInputToolbar, wysiwygInputToolbar.textFormattingEnabled { + wysiwygInputToolbar.command(command) + wysiwygInputToolbar.becomeFirstResponder() + } else { + guard let attributedText = inputToolbarView.attributedTextMessage else { return } + + let newAttributedString = NSMutableAttributedString(attributedString: attributedText) + newAttributedString.append(NSAttributedString(string: "\(command) ", + attributes: [.font: inputToolbarView.defaultFont])) + + inputToolbarView.attributedTextMessage = newAttributedString + inputToolbarView.becomeFirstResponder() + } + } + /// Send the formatted text message and its raw counterpart to the room /// diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index f3fc1111b..5700909fa 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -195,6 +195,10 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp name: member.displayname, mentionType: .user) } + + func command(_ command: String) { + self.wysiwygViewModel.setCommand(name: command) + } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index a2156cd89..1999e6c07 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -23,6 +23,7 @@ import WysiwygComposer protocol UserSuggestionCoordinatorDelegate: AnyObject { func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) } @@ -52,6 +53,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { private var userSuggestionService: UserSuggestionServiceProtocol private var userSuggestionViewModel: UserSuggestionViewModelProtocol private var roomMemberProvider: UserSuggestionCoordinatorRoomMemberProvider + private var commandProvider: UserSuggestionCoordinatorCommandProvider private var cancellables = Set() @@ -69,7 +71,8 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { self.parameters = parameters roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID) - userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider) + commandProvider = UserSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID) + userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider) let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) let view = UserSuggestionList(viewModel: viewModel.context) @@ -90,11 +93,11 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { return } - guard let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first else { - return + if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first { + self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger) + } else if let command = self.commandProvider.commands.filter({ $0 == identifier }).first { + self.delegate?.userSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.userSuggestionService.currentTextTrigger) } - - self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger) } } @@ -199,3 +202,32 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr roomMembers.map { RoomMembersProviderMember(userId: $0.userId, displayName: $0.displayname ?? "", avatarUrl: $0.avatarUrl ?? "") } } } + +private class UserSuggestionCoordinatorCommandProvider: CommandsProviderProtocol { + private let room: MXRoom + private let userID: String + + var commands: [String] = [] + + init(room: MXRoom, userID: String) { + self.room = room + self.userID = userID + updateWithPowerLevels() + } + + func updateWithPowerLevels() { + // TODO: filter commands in terms of user power level ? + } + + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { + self.commands = [ + "/ban", + "/invite", + "/join", + "/me" + ] + + // TODO: get real data + commands(self.commands.map { CommandsProviderCommand(name: $0) }) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift index 0d1f6795e..ba1bc75ca 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -20,6 +20,7 @@ import Foundation protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject { func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) func userSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinatorBridge, textTrigger: String?) + func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestCommand command: String, textTrigger: String?) func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat) } @@ -68,6 +69,10 @@ extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate { delegate?.userSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger) } + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) { + delegate?.userSuggestionCoordinatorBridge(self, didRequestCommand: command, textTrigger: textTrigger) + } + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) { delegate?.userSuggestionCoordinatorBridge(self, didUpdateViewHeight: height) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index a790e2845..76d41e700 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -24,6 +24,10 @@ struct RoomMembersProviderMember { var avatarUrl: String } +struct CommandsProviderCommand { + var name: String +} + class UserSuggestionID: NSObject { /// A special case added for suggesting `@room` mentions. @objc static let room = "@room" @@ -34,26 +38,35 @@ protocol RoomMembersProviderProtocol { func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) } +protocol CommandsProviderProtocol { + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) +} + struct UserSuggestionServiceItem: UserSuggestionItemProtocol { let userId: String let displayName: String? let avatarUrl: String? } +struct CommandSuggestionServiceItem: CommandSuggestionItemProtocol { + let name: String +} + class UserSuggestionService: UserSuggestionServiceProtocol { // MARK: - Properties // MARK: Private private let roomMemberProvider: RoomMembersProviderProtocol + private let commandProvider: CommandsProviderProtocol - private var suggestionItems: [UserSuggestionItemProtocol] = [] + private var suggestionItems: [SuggestionItem] = [] private let currentTextTriggerSubject = CurrentValueSubject(nil) private var cancellables = Set() // MARK: Public - var items = CurrentValueSubject<[UserSuggestionItemProtocol], Never>([]) + var items = CurrentValueSubject<[SuggestionItem], Never>([]) var currentTextTrigger: String? { currentTextTriggerSubject.value @@ -61,8 +74,11 @@ class UserSuggestionService: UserSuggestionServiceProtocol { // MARK: - Setup - init(roomMemberProvider: RoomMembersProviderProtocol, shouldDebounce: Bool = true) { + init(roomMemberProvider: RoomMembersProviderProtocol, + commandProvider: CommandsProviderProtocol, + shouldDebounce: Bool = true) { self.roomMemberProvider = roomMemberProvider + self.commandProvider = commandProvider if shouldDebounce { currentTextTriggerSubject @@ -83,7 +99,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol { guard let textMessage = textMessage, textMessage.count > 0, let lastComponent = textMessage.components(separatedBy: .whitespaces).last, - lastComponent.prefix(while: { $0 == "@" }).count == 1 // Partial username should start with one and only one "@" character + lastComponent.prefix(while: { $0 == "@" || $0 == "/" }).count == 1 // Partial username should start with one and only one "@" character else { items.send([]) currentTextTriggerSubject.send(nil) @@ -94,13 +110,22 @@ class UserSuggestionService: UserSuggestionServiceProtocol { } func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) { - guard let suggestionPattern, suggestionPattern.key == .at else { + guard let suggestionPattern else { items.send([]) currentTextTriggerSubject.send(nil) return } - currentTextTriggerSubject.send("@" + suggestionPattern.text) + switch suggestionPattern.key { + case .at: + currentTextTriggerSubject.send("@" + suggestionPattern.text) + case .hash: + // No room suggestion support yet + items.send([]) + currentTextTriggerSubject.send(nil) + case .slash: + currentTextTriggerSubject.send("/" + suggestionPattern.text) + } } // MARK: - Private @@ -109,24 +134,48 @@ class UserSuggestionService: UserSuggestionServiceProtocol { guard var partialName = textTrigger else { return } - - partialName.removeFirst() // remove the '@' prefix - - roomMemberProvider.fetchMembers { [weak self] members in - guard let self = self else { - return + + switch partialName.first { + case "@": + partialName.removeFirst() // remove the '@' prefix + + roomMemberProvider.fetchMembers { [weak self] members in + guard let self = self else { + return + } + + self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in + SuggestionItem.user(value: UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl)) + } + + self.items.send(self.suggestionItems.filter { item in + guard case let .user(userSuggestion) = item else { return false } + + let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased()) + let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) + + return (containedInUsername || containedInDisplayName) + }) } - - self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in - UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl) + case "/": + // TODO: send all commands if only text is "/" + partialName.removeFirst() + + commandProvider.fetchCommands { [weak self] commands in + guard let self else { return } + + self.suggestionItems = commands.map { command in + SuggestionItem.command(value: CommandSuggestionServiceItem(name: command.name)) + } + + self.items.send(self.suggestionItems.filter { item in + guard case let .command(commandSuggestion) = item else { return false } + + return commandSuggestion.name.lowercased().contains(partialName.lowercased()) + }) } - - self.items.send(self.suggestionItems.filter { userSuggestion in - let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased()) - let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) - - return (containedInUsername || containedInDisplayName) - }) + default: + return } } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index 43006dbed..4b5787cff 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -24,8 +24,17 @@ protocol UserSuggestionItemProtocol: Avatarable { var avatarUrl: String? { get } } +protocol CommandSuggestionItemProtocol { + var name: String { get } +} + +enum SuggestionItem { + case command(value: CommandSuggestionItemProtocol) + case user(value: UserSuggestionItemProtocol) +} + protocol UserSuggestionServiceProtocol { - var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> { get } + var items: CurrentValueSubject<[SuggestionItem], Never> { get } var currentTextTrigger: String? { get } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift index d4e984f88..dbaaf9295 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift @@ -24,10 +24,18 @@ enum UserSuggestionViewModelResult { case selectedItemWithIdentifier(String) } -struct UserSuggestionViewStateItem: Identifiable { - let id: String - let avatar: AvatarInputProtocol? - let displayName: String? +enum UserSuggestionViewStateItem: Identifiable { + case command(name: String) + case user(id: String, avatar: AvatarInputProtocol?, displayName: String?) + + var id: String { + switch self { + case .command(let name): + return name + case .user(let id, _, _): + return id + } + } } struct UserSuggestionViewState: BindableState { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift index 0a9395fa5..95aea9dbe 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift @@ -27,7 +27,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let service = UserSuggestionService(roomMemberProvider: self) + let service = UserSuggestionService(roomMemberProvider: self, commandProvider: self) let listViewModel = UserSuggestionViewModel(userSuggestionService: service) let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in @@ -60,3 +60,14 @@ extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { } } } + +extension MockUserSuggestionScreenState: CommandsProviderProtocol { + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { + commands([ + CommandsProviderCommand(name: "/ban"), + CommandsProviderCommand(name: "/invite"), + CommandsProviderCommand(name: "/join"), + CommandsProviderCommand(name: "/me") + ]) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift index 3999447b7..68d573bdf 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift @@ -40,14 +40,28 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo self.userSuggestionService = userSuggestionService let items = userSuggestionService.items.value.map { suggestionItem in - UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName) + switch suggestionItem { + case .command(let commandSuggestionItem): + return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name) + case .user(let userSuggestionItem): + return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId, + avatar: userSuggestionItem, + displayName: userSuggestionItem.displayName) + } } super.init(initialViewState: UserSuggestionViewState(items: items)) userSuggestionService.items.sink { [weak self] items in self?.state.items = items.map { item in - UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName) + switch item { + case .command(let commandSuggestionItem): + return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name) + case .user(let userSuggestionItem): + return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId, + avatar: userSuggestionItem, + displayName: userSuggestionItem.displayName) + } } }.store(in: &cancellables) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index e509a58b3..fe0c21761 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -51,9 +51,12 @@ struct UserSuggestionList: View { EmptyView() } else { ZStack { - UserSuggestionListItem(avatar: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Prototype"), - displayName: "Prototype", - userId: "Prototype") + UserSuggestionListItem(content: UserSuggestionViewStateItem.user( + id: "Prototype", + avatar: AvatarInput(mxContentUri: "", + matrixItemId: "", + displayName: "Prototype"), + displayName: "Prototype")) .background(ViewFrameReader(frame: $prototypeListItemFrame)) .hidden() if showBackgroundShadow { @@ -76,12 +79,8 @@ struct UserSuggestionList: View { Button { viewModel.send(viewAction: .selectedItem(item)) } label: { - UserSuggestionListItem( - avatar: item.avatar, - displayName: item.displayName, - userId: item.id - ) - .modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id)) + UserSuggestionListItem(content: item) + .modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id)) } } .listStyle(PlainListStyle()) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift index 862e7573d..0175c2abe 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -25,26 +25,33 @@ struct UserSuggestionListItem: View { // MARK: Public - let avatar: AvatarInputProtocol? - let displayName: String? - let userId: String + let content: UserSuggestionViewStateItem var body: some View { HStack { - if let avatar = avatar { - AvatarImage(avatarData: avatar, size: .medium) - } - VStack(alignment: .leading) { - Text(displayName ?? "") + switch content { + case .command(let name): + Text(name) .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) - .accessibility(identifier: "displayNameText") - .lineLimit(1) - Text(userId) - .font(theme.fonts.footnote) - .foregroundColor(theme.colors.tertiaryContent) - .accessibility(identifier: "userIdText") + .accessibility(identifier: "nameText") .lineLimit(1) + case .user(let userId, let avatar, let displayName): + if let avatar = avatar { + AvatarImage(avatarData: avatar, size: .medium) + } + VStack(alignment: .leading) { + Text(displayName ?? "") + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + .accessibility(identifier: "displayNameText") + .lineLimit(1) + Text(userId) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.tertiaryContent) + .accessibility(identifier: "userIdText") + .lineLimit(1) + } } } } @@ -54,7 +61,11 @@ struct UserSuggestionListItem: View { struct UserSuggestionHeader_Previews: PreviewProvider { static var previews: some View { - UserSuggestionListItem(avatar: MockAvatarInput.example, displayName: "Alice", userId: "@alice:matrix.org") - .environmentObject(AvatarViewModel.withMockedServices()) + UserSuggestionListItem(content: UserSuggestionViewStateItem.user( + id: "@alice:matrix.org", + avatar: MockAvatarInput.example, + displayName: "Alice" + )) + .environmentObject(AvatarViewModel.withMockedServices()) } } From 0e7d8029205c411b3135c89f3f303b87ac559387 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 19 Apr 2023 14:22:21 +0200 Subject: [PATCH 065/149] Rename `UserSuggestion` module as `CompletionSuggestion` --- Riot/Modules/Room/RoomViewController.h | 2 +- Riot/Modules/Room/RoomViewController.m | 56 ++++++------ Riot/Modules/Room/RoomViewController.xib | 24 ++---- .../Views/InputToolbar/RoomInputToolbarView.h | 4 +- .../WysiwygInputToolbarView.swift | 2 +- .../Modules/Common/Mock/MockAppScreens.swift | 2 +- .../CompletionSuggestionModels.swift} | 12 +-- .../CompletionSuggestionScreenState.swift} | 16 ++-- .../CompletionSuggestionViewModel.swift | 77 +++++++++++++++++ ...mpletionSuggestionViewModelProtocol.swift} | 10 +-- .../CompletionSuggestionCoordinator.swift} | 86 +++++++++---------- ...ompletionSuggestionCoordinatorBridge.swift | 79 +++++++++++++++++ .../CompletionSuggestionService.swift} | 26 +++--- ...CompletionSuggestionServiceProtocol.swift} | 16 ++-- .../UI/CompletionSuggestionUITests.swift} | 6 +- .../CompletionSuggestionServiceTests.swift} | 60 +++++++++---- .../View/CompletionSuggestionList.swift} | 12 +-- .../View/CompletionSuggestionListItem.swift} | 8 +- .../CompletionSuggestionListWithInput.swift} | 14 +-- .../Composer/MockComposerScreenState.swift | 8 +- .../Room/Composer/Model/ComposerModels.swift | 8 +- .../Modules/Room/Composer/View/Composer.swift | 8 +- .../UserSuggestionCoordinatorBridge.swift | 79 ----------------- .../UserSuggestionViewModel.swift | 77 ----------------- 24 files changed, 353 insertions(+), 339 deletions(-) rename RiotSwiftUI/Modules/Room/{UserSuggestion/UserSuggestionModels.swift => CompletionSuggestion/CompletionSuggestionModels.swift} (76%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/UserSuggestionScreenState.swift => CompletionSuggestion/CompletionSuggestionScreenState.swift} (75%) create mode 100644 RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift rename RiotSwiftUI/Modules/Room/{UserSuggestion/UserSuggestionViewModelProtocol.swift => CompletionSuggestion/CompletionSuggestionViewModelProtocol.swift} (67%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/Coordinator/UserSuggestionCoordinator.swift => CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift} (59%) create mode 100644 RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinatorBridge.swift rename RiotSwiftUI/Modules/Room/{UserSuggestion/Service/UserSuggestionService.swift => CompletionSuggestion/Service/CompletionSuggestionService.swift} (80%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/Service/UserSuggestionServiceProtocol.swift => CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift} (71%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/Test/UI/UserSuggestionUITests.swift => CompletionSuggestion/Test/UI/CompletionSuggestionUITests.swift} (79%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift => CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift} (61%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/View/UserSuggestionList.swift => CompletionSuggestion/View/CompletionSuggestionList.swift} (92%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/View/UserSuggestionListItem.swift => CompletionSuggestion/View/CompletionSuggestionListItem.swift} (89%) rename RiotSwiftUI/Modules/Room/{UserSuggestion/View/UserSuggestionListWithInput.swift => CompletionSuggestion/View/CompletionSuggestionListWithInput.swift} (75%) delete mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift delete mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 072a882a6..6cc25bcfe 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -61,7 +61,7 @@ extern NSTimeInterval const kResizeComposerAnimationDuration; // The preview header @property (weak, nonatomic, nullable) IBOutlet UIView *previewHeaderContainer; @property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *previewHeaderContainerHeightConstraint; -@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *userSuggestionContainerHeightConstraint; +@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *completionSuggestionContainerHeightConstraint; // The jump to last unread banner @property (weak, nonatomic, nullable) IBOutlet UIView *jumpToLastUnreadBannerContainer; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 57a431d2e..38f71f8d1 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, CompletionSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate> { // The preview header @@ -223,8 +223,8 @@ static CGSize kThreadListBarButtonItemImageSize; @property (nonatomic, strong) ShareManager *shareManager; @property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder; -@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; -@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; +@property (nonatomic, strong) CompletionSuggestionCoordinatorBridge *completionSuggestionCoordinator; +@property (nonatomic, weak) IBOutlet UIView *completionSuggestionContainerView; @property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration; @@ -416,7 +416,7 @@ static CGSize kThreadListBarButtonItemImageSize; [self setupActions]; - [self setupUserSuggestionViewIfNeeded]; + [self setupCompletionSuggestionViewIfNeeded]; [self.topBannersStackView vc_removeAllSubviews]; } @@ -1088,12 +1088,12 @@ static CGSize kThreadListBarButtonItemImageSize; [VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary]; _voiceMessageController.roomId = dataSource.roomId; - _userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager + _completionSuggestionCoordinator = [[CompletionSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager room:dataSource.room userID:self.roomDataSource.mxSession.myUserId]; - _userSuggestionCoordinator.delegate = self; + _completionSuggestionCoordinator.delegate = self; - [self setupUserSuggestionViewIfNeeded]; + [self setupCompletionSuggestionViewIfNeeded]; [self updateTopBanners]; } @@ -2726,13 +2726,13 @@ static CGSize kThreadListBarButtonItemImageSize; } } -- (void)setupUserSuggestionViewIfNeeded +- (void)setupCompletionSuggestionViewIfNeeded { if(!self.isViewLoaded) { return; } - UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable; + UIViewController *suggestionsViewController = self.completionSuggestionCoordinator.toPresentable; if (!suggestionsViewController) { @@ -2742,12 +2742,12 @@ static CGSize kThreadListBarButtonItemImageSize; [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; [self addChildViewController:suggestionsViewController]; - [self.userSuggestionContainerView addSubview:suggestionsViewController.view]; + [self.completionSuggestionContainerView addSubview:suggestionsViewController.view]; - [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor], - [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor], - [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor], - [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]]; + [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.topAnchor], + [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.leadingAnchor], + [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.trailingAnchor], + [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.bottomAnchor],]]; [suggestionsViewController didMoveToParentViewController:self]; } @@ -5147,17 +5147,17 @@ static CGSize kThreadListBarButtonItemImageSize; - (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView { - [self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage]; + [self.completionSuggestionCoordinator processTextMessage:toolbarView.textMessage]; } - (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern { - [self.userSuggestionCoordinator processSuggestionPattern:suggestionPattern]; + [self.completionSuggestionCoordinator processSuggestionPattern:suggestionPattern]; } -- (UserSuggestionViewModelContextWrapper *)userSuggestionContext +- (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext { - return [self.userSuggestionCoordinator sharedContext]; + return [self.completionSuggestionCoordinator sharedContext]; } - (MXMediaManager *)mediaManager @@ -8059,9 +8059,9 @@ static CGSize kThreadListBarButtonItemImageSize; [[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId]; } -#pragma mark - UserSuggestionCoordinatorBridgeDelegate +#pragma mark - CompletionSuggestionCoordinatorBridgeDelegate -- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator +- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator didRequestMentionForMember:(MXRoomMember *)member textTrigger:(NSString *)textTrigger { @@ -8069,16 +8069,16 @@ static CGSize kThreadListBarButtonItemImageSize; [self mention:member]; } -- (void)userSuggestionCoordinatorBridgeDidRequestMentionForRoom:(UserSuggestionCoordinatorBridge *)coordinator +- (void)completionSuggestionCoordinatorBridgeDidRequestMentionForRoom:(CompletionSuggestionCoordinatorBridge *)coordinator textTrigger:(NSString *)textTrigger { [self removeTriggerTextFromComposer:textTrigger]; - [self.inputToolbarView pasteText:[UserSuggestionID.room stringByAppendingString:@" "]]; + [self.inputToolbarView pasteText:[CompletionSuggestionUserID.room stringByAppendingString:@" "]]; } -- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator - didRequestCommand:(NSString *)command - textTrigger:(NSString *)textTrigger +- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator + didRequestCommand:(NSString *)command + textTrigger:(NSString *)textTrigger { [self removeTriggerTextFromComposer:textTrigger]; [self setCommand:command]; @@ -8097,11 +8097,11 @@ static CGSize kThreadListBarButtonItemImageSize; } } -- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height +- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height { - if (self.userSuggestionContainerHeightConstraint.constant != height) + if (self.completionSuggestionContainerHeightConstraint.constant != height) { - self.userSuggestionContainerHeightConstraint.constant = height; + self.completionSuggestionContainerHeightConstraint.constant = height; [self.view layoutIfNeeded]; } diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index b7a62a8bf..cdb656508 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -1,9 +1,8 @@ - + - - + @@ -13,6 +12,8 @@ + + @@ -32,8 +33,6 @@ - - @@ -48,20 +47,20 @@ - + - + - + - + @@ -237,11 +236,6 @@ - - - - - diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index df71790be..5bbdeaa51 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -22,7 +22,7 @@ @class RoomInputToolbarView; @class LinkActionWrapper; @class SuggestionPatternWrapper; -@class UserSuggestionViewModelContextWrapper; +@class CompletionSuggestionViewModelContextWrapper; /** Destination of the message in the composer @@ -84,7 +84,7 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode) - (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern; -- (UserSuggestionViewModelContextWrapper *)userSuggestionContext; +- (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext; - (MXMediaManager *)mediaManager; diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 5700909fa..9bc02c21e 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -223,7 +223,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp let composer = Composer( viewModel: viewModel.context, wysiwygViewModel: wysiwygViewModel, - userSuggestionSharedContext: toolbarViewDelegate.userSuggestionContext().context, + completionSuggestionSharedContext: toolbarViewDelegate.completionSuggestionContext().context, resizeAnimationDuration: Double(kResizeComposerAnimationDuration), sendMessageAction: { [weak self] content in guard let self = self else { return } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 91bf25a51..0afe12c02 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -51,7 +51,7 @@ enum MockAppScreens { MockStaticLocationViewingScreenState.self, MockLocationSharingScreenState.self, MockAnalyticsPromptScreenState.self, - MockUserSuggestionScreenState.self, + MockCompletionSuggestionScreenState.self, MockPollEditFormScreenState.self, MockSpaceCreationEmailInvitesScreenState.self, MockSpaceSettingsScreenState.self, diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift similarity index 76% rename from RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift index dbaaf9295..91fc4ffeb 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift @@ -16,15 +16,15 @@ import Foundation -enum UserSuggestionViewAction { - case selectedItem(UserSuggestionViewStateItem) +enum CompletionSuggestionViewAction { + case selectedItem(CompletionSuggestionViewStateItem) } -enum UserSuggestionViewModelResult { +enum CompletionSuggestionViewModelResult { case selectedItemWithIdentifier(String) } -enum UserSuggestionViewStateItem: Identifiable { +enum CompletionSuggestionViewStateItem: Identifiable { case command(name: String) case user(id: String, avatar: AvatarInputProtocol?, displayName: String?) @@ -38,6 +38,6 @@ enum UserSuggestionViewStateItem: Identifiable { } } -struct UserSuggestionViewState: BindableState { - var items: [UserSuggestionViewStateItem] +struct CompletionSuggestionViewState: BindableState { + var items: [CompletionSuggestionViewStateItem] } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift similarity index 75% rename from RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift index 95aea9dbe..1427c3f3f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift @@ -17,32 +17,32 @@ import Foundation import SwiftUI -enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { +enum MockCompletionSuggestionScreenState: MockScreenState, CaseIterable { case multipleResults private static var members: [RoomMembersProviderMember]! var screenType: Any.Type { - UserSuggestionList.self + CompletionSuggestionList.self } var screenView: ([Any], AnyView) { - let service = UserSuggestionService(roomMemberProvider: self, commandProvider: self) - let listViewModel = UserSuggestionViewModel(userSuggestionService: service) + let service = CompletionSuggestionService(roomMemberProvider: self, commandProvider: self) + let listViewModel = CompletionSuggestionViewModel(completionSuggestionService: service) - let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in + let viewModel = CompletionSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in service.processTextMessage(textMessage) } return ( [service, listViewModel], - AnyView(UserSuggestionListWithInput(viewModel: viewModel) + AnyView(CompletionSuggestionListWithInput(viewModel: viewModel) .environmentObject(AvatarViewModel.withMockedServices())) ) } } -extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { +extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol { var canMentionRoom: Bool { false } func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) { @@ -61,7 +61,7 @@ extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { } } -extension MockUserSuggestionScreenState: CommandsProviderProtocol { +extension MockCompletionSuggestionScreenState: CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { commands([ CommandsProviderCommand(name: "/ban"), diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift new file mode 100644 index 000000000..01c881970 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift @@ -0,0 +1,77 @@ +// +// 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 +import SwiftUI + +typealias CompletionSuggestionViewModelType = StateStoreViewModel + +class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, CompletionSuggestionViewModelProtocol { + // MARK: - Properties + + // MARK: Private + + private let completionSuggestionService: CompletionSuggestionServiceProtocol + + // MARK: Public + + var sharedContext: CompletionSuggestionViewModelType.Context { + return self.context + } + + var completion: ((CompletionSuggestionViewModelResult) -> Void)? + + // MARK: - Setup + + init(completionSuggestionService: CompletionSuggestionServiceProtocol) { + self.completionSuggestionService = completionSuggestionService + + let items = completionSuggestionService.items.value.map { suggestionItem in + switch suggestionItem { + case .command(let completionSuggestionCommandItem): + return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name) + case .user(let completionSuggestionUserItem): + return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId, + avatar: completionSuggestionUserItem, + displayName: completionSuggestionUserItem.displayName) + } + } + + super.init(initialViewState: CompletionSuggestionViewState(items: items)) + + completionSuggestionService.items.sink { [weak self] items in + self?.state.items = items.map { item in + switch item { + case .command(let completionSuggestionCommandItem): + return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name) + case .user(let completionSuggestionUserItem): + return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId, + avatar: completionSuggestionUserItem, + displayName: completionSuggestionUserItem.displayName) + } + } + }.store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: CompletionSuggestionViewAction) { + switch viewAction { + case .selectedItem(let item): + completion?(.selectedItemWithIdentifier(item.id)) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModelProtocol.swift similarity index 67% rename from RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModelProtocol.swift index 33aa5bb79..d7c51909f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModelProtocol.swift @@ -16,10 +16,10 @@ import Foundation -protocol UserSuggestionViewModelProtocol { - /// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple - /// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController` +protocol CompletionSuggestionViewModelProtocol { + /// Defines a shared context providing the ability to use a single `CompletionSuggestionViewModel` for multiple + /// `CompletionSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController` /// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload the data. - var sharedContext: UserSuggestionViewModelType.Context { get } - var completion: ((UserSuggestionViewModelResult) -> Void)? { get set } + var sharedContext: CompletionSuggestionViewModelType.Context { get } + var completion: ((CompletionSuggestionViewModelResult) -> Void)? { get set } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift similarity index 59% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 1999e6c07..8da2356fd 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -20,40 +20,40 @@ import SwiftUI import UIKit import WysiwygComposer -protocol UserSuggestionCoordinatorDelegate: AnyObject { - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) - func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) +protocol CompletionSuggestionCoordinatorDelegate: AnyObject { + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) + func completionSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinator, textTrigger: String?) + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didUpdateViewHeight height: CGFloat) } -struct UserSuggestionCoordinatorParameters { +struct CompletionSuggestionCoordinatorParameters { let mediaManager: MXMediaManager let room: MXRoom let userID: String } -/// Wrapper around `UserSuggestionViewModelType.Context` to pass it through obj-c. -final class UserSuggestionViewModelContextWrapper: NSObject { - let context: UserSuggestionViewModelType.Context +/// Wrapper around `CompletionSuggestionViewModelType.Context` to pass it through obj-c. +final class CompletionSuggestionViewModelContextWrapper: NSObject { + let context: CompletionSuggestionViewModelType.Context - init(context: UserSuggestionViewModelType.Context) { + init(context: CompletionSuggestionViewModelType.Context) { self.context = context } } -final class UserSuggestionCoordinator: Coordinator, Presentable { +final class CompletionSuggestionCoordinator: Coordinator, Presentable { // MARK: - Properties // MARK: Private - private let parameters: UserSuggestionCoordinatorParameters + private let parameters: CompletionSuggestionCoordinatorParameters - private var userSuggestionHostingController: UIHostingController - private var userSuggestionService: UserSuggestionServiceProtocol - private var userSuggestionViewModel: UserSuggestionViewModelProtocol - private var roomMemberProvider: UserSuggestionCoordinatorRoomMemberProvider - private var commandProvider: UserSuggestionCoordinatorCommandProvider + private var completionSuggestionHostingController: UIHostingController + private var completionSuggestionService: CompletionSuggestionServiceProtocol + private var completionSuggestionViewModel: CompletionSuggestionViewModelProtocol + private var roomMemberProvider: CompletionSuggestionCoordinatorRoomMemberProvider + private var commandProvider: CompletionSuggestionCoordinatorCommandProvider private var cancellables = Set() @@ -63,57 +63,57 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { var childCoordinators: [Coordinator] = [] var completion: (() -> Void)? - weak var delegate: UserSuggestionCoordinatorDelegate? + weak var delegate: CompletionSuggestionCoordinatorDelegate? // MARK: - Setup - init(parameters: UserSuggestionCoordinatorParameters) { + init(parameters: CompletionSuggestionCoordinatorParameters) { self.parameters = parameters - roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID) - commandProvider = UserSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID) - userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider) + roomMemberProvider = CompletionSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID) + commandProvider = CompletionSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID) + completionSuggestionService = CompletionSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider) - let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) - let view = UserSuggestionList(viewModel: viewModel.context) + let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService) + let view = CompletionSuggestionList(viewModel: viewModel.context) .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) - userSuggestionViewModel = viewModel - userSuggestionHostingController = VectorHostingController(rootView: view) + completionSuggestionViewModel = viewModel + completionSuggestionHostingController = VectorHostingController(rootView: view) - userSuggestionViewModel.completion = { [weak self] result in + completionSuggestionViewModel.completion = { [weak self] result in guard let self = self else { return } switch result { case .selectedItemWithIdentifier(let identifier): - if identifier == UserSuggestionID.room { - self.delegate?.userSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.userSuggestionService.currentTextTrigger) + if identifier == CompletionSuggestionUserID.room { + self.delegate?.completionSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.completionSuggestionService.currentTextTrigger) return } if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first { - self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger) + self.delegate?.completionSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.completionSuggestionService.currentTextTrigger) } else if let command = self.commandProvider.commands.filter({ $0 == identifier }).first { - self.delegate?.userSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.userSuggestionService.currentTextTrigger) + self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.completionSuggestionService.currentTextTrigger) } } } - userSuggestionService.items.sink { [weak self] _ in + completionSuggestionService.items.sink { [weak self] _ in guard let self = self else { return } - self.delegate?.userSuggestionCoordinator(self, + self.delegate?.completionSuggestionCoordinator(self, didUpdateViewHeight: self.calculateViewHeight()) }.store(in: &cancellables) } func processTextMessage(_ textMessage: String) { - userSuggestionService.processTextMessage(textMessage) + completionSuggestionService.processTextMessage(textMessage) } func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) { - userSuggestionService.processSuggestionPattern(suggestionPattern) + completionSuggestionService.processSuggestionPattern(suggestionPattern) } // MARK: - Public @@ -121,18 +121,18 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { func start() { } func toPresentable() -> UIViewController { - userSuggestionHostingController + completionSuggestionHostingController } - func sharedContext() -> UserSuggestionViewModelContextWrapper { - UserSuggestionViewModelContextWrapper(context: userSuggestionViewModel.sharedContext) + func sharedContext() -> CompletionSuggestionViewModelContextWrapper { + CompletionSuggestionViewModelContextWrapper(context: completionSuggestionViewModel.sharedContext) } // MARK: - Private private func calculateViewHeight() -> CGFloat { - let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) - let view = UserSuggestionList(viewModel: viewModel.context) + let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService) + let view = CompletionSuggestionList(viewModel: viewModel.context) .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) let controller = VectorHostingController(rootView: view) @@ -156,7 +156,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { } } -private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol { +private class CompletionSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol { private let room: MXRoom private let userID: String @@ -194,7 +194,7 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr self.roomMembers = joinedMembers members(self.roomMembersToProviderMembers(joinedMembers)) } failure: { error in - MXLog.error("[UserSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error) + MXLog.error("[CompletionSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error) } } @@ -203,7 +203,7 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr } } -private class UserSuggestionCoordinatorCommandProvider: CommandsProviderProtocol { +private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderProtocol { private let room: MXRoom private let userID: String diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinatorBridge.swift new file mode 100644 index 000000000..83a9ed94c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinatorBridge.swift @@ -0,0 +1,79 @@ +// +// 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 + +@objc +protocol CompletionSuggestionCoordinatorBridgeDelegate: AnyObject { + func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) + func completionSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinatorBridge, textTrigger: String?) + func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didRequestCommand command: String, textTrigger: String?) + func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat) +} + +@objcMembers +final class CompletionSuggestionCoordinatorBridge: NSObject { + private var _completionSuggestionCoordinator: Any? + fileprivate var completionSuggestionCoordinator: CompletionSuggestionCoordinator { + _completionSuggestionCoordinator as! CompletionSuggestionCoordinator + } + + weak var delegate: CompletionSuggestionCoordinatorBridgeDelegate? + + init(mediaManager: MXMediaManager, room: MXRoom, userID: String) { + let parameters = CompletionSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID) + let completionSuggestionCoordinator = CompletionSuggestionCoordinator(parameters: parameters) + _completionSuggestionCoordinator = completionSuggestionCoordinator + + super.init() + + completionSuggestionCoordinator.delegate = self + } + + func processTextMessage(_ textMessage: String) { + completionSuggestionCoordinator.processTextMessage(textMessage) + } + + func processSuggestionPattern(_ suggestionPatternWrapper: SuggestionPatternWrapper) { + completionSuggestionCoordinator.processSuggestionPattern(suggestionPatternWrapper.suggestionPattern) + } + + func toPresentable() -> UIViewController? { + completionSuggestionCoordinator.toPresentable() + } + + func sharedContext() -> CompletionSuggestionViewModelContextWrapper { + completionSuggestionCoordinator.sharedContext() + } +} + +extension CompletionSuggestionCoordinatorBridge: CompletionSuggestionCoordinatorDelegate { + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) { + delegate?.completionSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger) + } + + func completionSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinator, textTrigger: String?) { + delegate?.completionSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger) + } + + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) { + delegate?.completionSuggestionCoordinatorBridge(self, didRequestCommand: command, textTrigger: textTrigger) + } + + func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didUpdateViewHeight height: CGFloat) { + delegate?.completionSuggestionCoordinatorBridge(self, didUpdateViewHeight: height) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift similarity index 80% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 76d41e700..0353b63d4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -28,7 +28,7 @@ struct CommandsProviderCommand { var name: String } -class UserSuggestionID: NSObject { +class CompletionSuggestionUserID: NSObject { /// A special case added for suggesting `@room` mentions. @objc static let room = "@room" } @@ -42,17 +42,17 @@ protocol CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) } -struct UserSuggestionServiceItem: UserSuggestionItemProtocol { +struct CompletionSuggestionServiceUserItem: CompletionSuggestionUserItemProtocol { let userId: String let displayName: String? let avatarUrl: String? } -struct CommandSuggestionServiceItem: CommandSuggestionItemProtocol { +struct CompletionSuggestionServiceCommandItem: CompletionSuggestionCommandItemProtocol { let name: String } -class UserSuggestionService: UserSuggestionServiceProtocol { +class CompletionSuggestionService: CompletionSuggestionServiceProtocol { // MARK: - Properties // MARK: Private @@ -60,13 +60,13 @@ class UserSuggestionService: UserSuggestionServiceProtocol { private let roomMemberProvider: RoomMembersProviderProtocol private let commandProvider: CommandsProviderProtocol - private var suggestionItems: [SuggestionItem] = [] + private var suggestionItems: [CompletionSuggestionItem] = [] private let currentTextTriggerSubject = CurrentValueSubject(nil) private var cancellables = Set() // MARK: Public - var items = CurrentValueSubject<[SuggestionItem], Never>([]) + var items = CurrentValueSubject<[CompletionSuggestionItem], Never>([]) var currentTextTrigger: String? { currentTextTriggerSubject.value @@ -93,7 +93,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol { } } - // MARK: - UserSuggestionServiceProtocol + // MARK: - CompletionSuggestionServiceProtocol func processTextMessage(_ textMessage: String?) { guard let textMessage = textMessage, @@ -145,14 +145,14 @@ class UserSuggestionService: UserSuggestionServiceProtocol { } self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in - SuggestionItem.user(value: UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl)) + CompletionSuggestionItem.user(value: CompletionSuggestionServiceUserItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl)) } self.items.send(self.suggestionItems.filter { item in - guard case let .user(userSuggestion) = item else { return false } + guard case let .user(completionSuggestionUserItem) = item else { return false } - let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased()) - let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) + let containedInUsername = completionSuggestionUserItem.userId.lowercased().contains(partialName.lowercased()) + let containedInDisplayName = (completionSuggestionUserItem.displayName ?? "").lowercased().contains(partialName.lowercased()) return (containedInUsername || containedInDisplayName) }) @@ -165,7 +165,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol { guard let self else { return } self.suggestionItems = commands.map { command in - SuggestionItem.command(value: CommandSuggestionServiceItem(name: command.name)) + CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(name: command.name)) } self.items.send(self.suggestionItems.filter { item in @@ -184,6 +184,6 @@ extension Array where Element == RoomMembersProviderMember { /// Returns the array with an additional member that represents an `@room` mention. func withRoom(_ canMentionRoom: Bool) -> Self { guard canMentionRoom else { return self } - return self + [RoomMembersProviderMember(userId: UserSuggestionID.room, displayName: "Everyone", avatarUrl: "")] + return self + [RoomMembersProviderMember(userId: CompletionSuggestionUserID.room, displayName: "Everyone", avatarUrl: "")] } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift similarity index 71% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift index 4b5787cff..4586e1294 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift @@ -18,23 +18,23 @@ import Combine import Foundation import WysiwygComposer -protocol UserSuggestionItemProtocol: Avatarable { +protocol CompletionSuggestionUserItemProtocol: Avatarable { var userId: String { get } var displayName: String? { get } var avatarUrl: String? { get } } -protocol CommandSuggestionItemProtocol { +protocol CompletionSuggestionCommandItemProtocol { var name: String { get } } -enum SuggestionItem { - case command(value: CommandSuggestionItemProtocol) - case user(value: UserSuggestionItemProtocol) +enum CompletionSuggestionItem { + case command(value: CompletionSuggestionCommandItemProtocol) + case user(value: CompletionSuggestionUserItemProtocol) } -protocol UserSuggestionServiceProtocol { - var items: CurrentValueSubject<[SuggestionItem], Never> { get } +protocol CompletionSuggestionServiceProtocol { + var items: CurrentValueSubject<[CompletionSuggestionItem], Never> { get } var currentTextTrigger: String? { get } @@ -44,7 +44,7 @@ protocol UserSuggestionServiceProtocol { // MARK: Avatarable -extension UserSuggestionItemProtocol { +extension CompletionSuggestionUserItemProtocol { var mxContentUri: String? { avatarUrl } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/UI/CompletionSuggestionUITests.swift similarity index 79% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/UI/CompletionSuggestionUITests.swift index f44744a9c..5ec9d4b9b 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/UI/CompletionSuggestionUITests.swift @@ -17,9 +17,9 @@ import RiotSwiftUI import XCTest -class UserSuggestionUITests: MockScreenTestCase { - func testUserSuggestionScreen() throws { - app.goToScreenWithIdentifier(MockUserSuggestionScreenState.multipleResults.title) +class CompletionSuggestionUITests: MockScreenTestCase { + func testCompletionSuggestionScreen() throws { + app.goToScreenWithIdentifier(MockCompletionSuggestionScreenState.multipleResults.title) let firstButton = app.buttons["displayNameText-userIdText"].firstMatch XCTAssert(firstButton.waitForExistence(timeout: 10)) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift similarity index 61% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift index 7ae0bfa39..636ba3355 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift @@ -19,51 +19,53 @@ import XCTest @testable import RiotSwiftUI -class UserSuggestionServiceTests: XCTestCase { - var service: UserSuggestionService! +class CompletionSuggestionServiceTests: XCTestCase { + var service: CompletionSuggestionService! var canMentionRoom = false override func setUp() { - service = UserSuggestionService(roomMemberProvider: self, shouldDebounce: false) + service = CompletionSuggestionService(roomMemberProvider: self, + commandProvider: self, + shouldDebounce: false) canMentionRoom = false } func testAlice() { service.processTextMessage("@Al") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") service.processTextMessage("@al") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") service.processTextMessage("@ice") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") service.processTextMessage("@Alice") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") service.processTextMessage("@alice:matrix.org") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") } func testBob() { service.processTextMessage("@ob") - XCTAssertEqual(service.items.value.first?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob") service.processTextMessage("@ob:") - XCTAssertEqual(service.items.value.first?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob") service.processTextMessage("@b:matrix") - XCTAssertEqual(service.items.value.first?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob") } func testBoth() { service.processTextMessage("@:matrix") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") - XCTAssertEqual(service.items.value.last?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") + XCTAssertEqual(service.items.value.last?.asUser?.displayName, "Bob") service.processTextMessage("@.org") - XCTAssertEqual(service.items.value.first?.displayName, "Alice") - XCTAssertEqual(service.items.value.last?.displayName, "Bob") + XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") + XCTAssertEqual(service.items.value.last?.asUser?.displayName, "Bob") } func testEmptyResult() { @@ -117,18 +119,18 @@ class UserSuggestionServiceTests: XCTestCase { } func testRoomWithPower() { - // Given a user without the power to mention a room. + // Given a user with the power to mention a room. canMentionRoom = true - // Given a user without the power to mention a room. + // Given a user with the power to mention a room. service.processTextMessage("@ro") // Then the completion for a room mention should be shown. - XCTAssertEqual(service.items.value.first?.userId, UserSuggestionID.room) + XCTAssertEqual(service.items.value.first?.asUser?.userId, CompletionSuggestionUserID.room) } } -extension UserSuggestionServiceTests: RoomMembersProviderProtocol { +extension CompletionSuggestionServiceTests: RoomMembersProviderProtocol { func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) { let users = [("Alice", "@alice:matrix.org"), ("Bob", "@bob:matrix.org")] @@ -138,3 +140,23 @@ extension UserSuggestionServiceTests: RoomMembersProviderProtocol { }) } } + +extension CompletionSuggestionServiceTests: CommandsProviderProtocol { + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { + let commandList = ["/ban", "/invite", "/join", "/me"] + + commands(commandList.map { command in + CommandsProviderCommand(name: command) + }) + } +} + +extension CompletionSuggestionItem { + var asUser: CompletionSuggestionUserItemProtocol? { + if case let .user(value) = self { return value } else { return nil } + } + + var asCommand: CompletionSuggestionCommandItemProtocol? { + if case let .command(value) = self { return value } else { return nil } + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift similarity index 92% rename from RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift index fe0c21761..02aef8a1f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift @@ -16,7 +16,7 @@ import SwiftUI -struct UserSuggestionList: View { +struct CompletionSuggestionList: View { private enum Constants { static let topPadding: CGFloat = 8.0 static let listItemPadding: CGFloat = 4.0 @@ -43,7 +43,7 @@ struct UserSuggestionList: View { // MARK: Public - @ObservedObject var viewModel: UserSuggestionViewModel.Context + @ObservedObject var viewModel: CompletionSuggestionViewModel.Context var showBackgroundShadow: Bool = true var body: some View { @@ -51,7 +51,7 @@ struct UserSuggestionList: View { EmptyView() } else { ZStack { - UserSuggestionListItem(content: UserSuggestionViewStateItem.user( + CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user( id: "Prototype", avatar: AvatarInput(mxContentUri: "", matrixItemId: "", @@ -79,7 +79,7 @@ struct UserSuggestionList: View { Button { viewModel.send(viewAction: .selectedItem(item)) } label: { - UserSuggestionListItem(content: item) + CompletionSuggestionListItem(content: item) .modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id)) } } @@ -134,8 +134,8 @@ private struct BackgroundView: View { // MARK: - Previews -struct UserSuggestion_Previews: PreviewProvider { - static let stateRenderer = MockUserSuggestionScreenState.stateRenderer +struct CompletionSuggestion_Previews: PreviewProvider { + static let stateRenderer = MockCompletionSuggestionScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup() } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift similarity index 89% rename from RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift index 0175c2abe..c30ec5d89 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift @@ -16,7 +16,7 @@ import SwiftUI -struct UserSuggestionListItem: View { +struct CompletionSuggestionListItem: View { // MARK: - Properties // MARK: Private @@ -25,7 +25,7 @@ struct UserSuggestionListItem: View { // MARK: Public - let content: UserSuggestionViewStateItem + let content: CompletionSuggestionViewStateItem var body: some View { HStack { @@ -59,9 +59,9 @@ struct UserSuggestionListItem: View { // MARK: - Previews -struct UserSuggestionHeader_Previews: PreviewProvider { +struct CompletionSuggestionHeader_Previews: PreviewProvider { static var previews: some View { - UserSuggestionListItem(content: UserSuggestionViewStateItem.user( + CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user( id: "@alice:matrix.org", avatar: MockAvatarInput.example, displayName: "Alice" diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift similarity index 75% rename from RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift rename to RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift index 176be8ec4..0b1dd8e8a 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift @@ -16,24 +16,24 @@ import SwiftUI -struct UserSuggestionListWithInputViewModel { - let listViewModel: UserSuggestionViewModel +struct CompletionSuggestionListWithInputViewModel { + let listViewModel: CompletionSuggestionViewModel let callback: (String) -> Void } -struct UserSuggestionListWithInput: View { +struct CompletionSuggestionListWithInput: View { // MARK: - Properties // MARK: Private // MARK: Public - var viewModel: UserSuggestionListWithInputViewModel + var viewModel: CompletionSuggestionListWithInputViewModel @State private var inputText = "" var body: some View { VStack(spacing: 0.0) { - UserSuggestionList(viewModel: viewModel.listViewModel.context) + CompletionSuggestionList(viewModel: viewModel.listViewModel.context) TextField("Search for user", text: $inputText) .background(Color.white) .onChange(of: inputText, perform: viewModel.callback) @@ -48,8 +48,8 @@ struct UserSuggestionListWithInput: View { // MARK: - Previews -struct UserSuggestionListWithInput_Previews: PreviewProvider { - static let stateRenderer = MockUserSuggestionScreenState.stateRenderer +struct CompletionSuggestionListWithInput_Previews: PreviewProvider { + static let stateRenderer = MockCompletionSuggestionScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup() } diff --git a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift index 8b5327b14..79322b78a 100644 --- a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift @@ -29,7 +29,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let viewModel: ComposerViewModel - let userSuggestionViewModel = MockUserSuggestionViewModel(initialViewState: UserSuggestionViewState(items: [])) + let completionSuggestionViewModel = MockCompletionSuggestionViewModel(initialViewState: CompletionSuggestionViewState(items: [])) let bindings = ComposerBindings(focused: false) switch self { @@ -67,7 +67,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { Spacer() Composer(viewModel: viewModel.context, wysiwygViewModel: wysiwygviewModel, - userSuggestionSharedContext: userSuggestionViewModel.context, + completionSuggestionSharedContext: completionSuggestionViewModel.context, resizeAnimationDuration: 0.1, sendMessageAction: { _ in }, showSendMediaActions: { }) @@ -82,6 +82,4 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { } } -private final class MockUserSuggestionViewModel: UserSuggestionViewModelType { - -} +private final class MockCompletionSuggestionViewModel: CompletionSuggestionViewModelType { } diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index 6f7bab165..33d73ef4a 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -257,11 +257,11 @@ final class SuggestionPatternWrapper: NSObject { } } -final class UserSuggestionViewModelWrapper: NSObject { - let userSuggestionViewModel: UserSuggestionViewModel +final class CompletionSuggestionViewModelWrapper: NSObject { + let completionSuggestionViewModel: CompletionSuggestionViewModel - init(_ userSuggestionViewModel: UserSuggestionViewModel) { - self.userSuggestionViewModel = userSuggestionViewModel + init(_ completionSuggestionViewModel: CompletionSuggestionViewModel) { + self.completionSuggestionViewModel = completionSuggestionViewModel super.init() } } diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index e4317a275..a74b0bb4d 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -23,7 +23,7 @@ struct Composer: View { // MARK: Private @ObservedObject private var viewModel: ComposerViewModelType.Context @ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel - private let userSuggestionSharedContext: UserSuggestionViewModelType.Context + private let completionSuggestionSharedContext: CompletionSuggestionViewModelType.Context private let resizeAnimationDuration: Double private let sendMessageAction: (WysiwygComposerContent) -> Void @@ -223,13 +223,13 @@ struct Composer: View { init( viewModel: ComposerViewModelType.Context, wysiwygViewModel: WysiwygComposerViewModel, - userSuggestionSharedContext: UserSuggestionViewModelType.Context, + completionSuggestionSharedContext: CompletionSuggestionViewModelType.Context, resizeAnimationDuration: Double, sendMessageAction: @escaping (WysiwygComposerContent) -> Void, showSendMediaActions: @escaping () -> Void) { self.viewModel = viewModel self.wysiwygViewModel = wysiwygViewModel - self.userSuggestionSharedContext = userSuggestionSharedContext + self.completionSuggestionSharedContext = completionSuggestionSharedContext self.resizeAnimationDuration = resizeAnimationDuration self.sendMessageAction = sendMessageAction self.showSendMediaActions = showSendMediaActions @@ -256,7 +256,7 @@ struct Composer: View { } } if wysiwygViewModel.maximised { - UserSuggestionList(viewModel: userSuggestionSharedContext, showBackgroundShadow: false) + CompletionSuggestionList(viewModel: completionSuggestionSharedContext, showBackgroundShadow: false) } } .frame(height: composerHeight) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift deleted file mode 100644 index ba1bc75ca..000000000 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// 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 - -@objc -protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject { - func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) - func userSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinatorBridge, textTrigger: String?) - func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestCommand command: String, textTrigger: String?) - func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat) -} - -@objcMembers -final class UserSuggestionCoordinatorBridge: NSObject { - private var _userSuggestionCoordinator: Any? - fileprivate var userSuggestionCoordinator: UserSuggestionCoordinator { - _userSuggestionCoordinator as! UserSuggestionCoordinator - } - - weak var delegate: UserSuggestionCoordinatorBridgeDelegate? - - init(mediaManager: MXMediaManager, room: MXRoom, userID: String) { - let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID) - let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters) - _userSuggestionCoordinator = userSuggestionCoordinator - - super.init() - - userSuggestionCoordinator.delegate = self - } - - func processTextMessage(_ textMessage: String) { - userSuggestionCoordinator.processTextMessage(textMessage) - } - - func processSuggestionPattern(_ suggestionPatternWrapper: SuggestionPatternWrapper) { - userSuggestionCoordinator.processSuggestionPattern(suggestionPatternWrapper.suggestionPattern) - } - - func toPresentable() -> UIViewController? { - userSuggestionCoordinator.toPresentable() - } - - func sharedContext() -> UserSuggestionViewModelContextWrapper { - userSuggestionCoordinator.sharedContext() - } -} - -extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate { - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) { - delegate?.userSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger) - } - - func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) { - delegate?.userSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger) - } - - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) { - delegate?.userSuggestionCoordinatorBridge(self, didRequestCommand: command, textTrigger: textTrigger) - } - - func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) { - delegate?.userSuggestionCoordinatorBridge(self, didUpdateViewHeight: height) - } -} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift deleted file mode 100644 index 68d573bdf..000000000 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// 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 -import SwiftUI - -typealias UserSuggestionViewModelType = StateStoreViewModel - -class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol { - // MARK: - Properties - - // MARK: Private - - private let userSuggestionService: UserSuggestionServiceProtocol - - // MARK: Public - - var sharedContext: UserSuggestionViewModelType.Context { - return self.context - } - - var completion: ((UserSuggestionViewModelResult) -> Void)? - - // MARK: - Setup - - init(userSuggestionService: UserSuggestionServiceProtocol) { - self.userSuggestionService = userSuggestionService - - let items = userSuggestionService.items.value.map { suggestionItem in - switch suggestionItem { - case .command(let commandSuggestionItem): - return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name) - case .user(let userSuggestionItem): - return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId, - avatar: userSuggestionItem, - displayName: userSuggestionItem.displayName) - } - } - - super.init(initialViewState: UserSuggestionViewState(items: items)) - - userSuggestionService.items.sink { [weak self] items in - self?.state.items = items.map { item in - switch item { - case .command(let commandSuggestionItem): - return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name) - case .user(let userSuggestionItem): - return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId, - avatar: userSuggestionItem, - displayName: userSuggestionItem.displayName) - } - } - }.store(in: &cancellables) - } - - // MARK: - Public - - override func process(viewAction: UserSuggestionViewAction) { - switch viewAction { - case .selectedItem(let item): - completion?(.selectedItemWithIdentifier(item.id)) - } - } -} From 4d7a502903c27b92411b9b123faf8b92ebe203de Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 19 Apr 2023 15:32:30 +0200 Subject: [PATCH 066/149] Display additional command content in suggestion list --- .../CompletionSuggestionModels.swift | 4 +-- .../CompletionSuggestionScreenState.swift | 16 +++++++++--- .../CompletionSuggestionViewModel.swift | 12 +++++++-- .../CompletionSuggestionCoordinator.swift | 26 +++++++++++++------ .../Service/CompletionSuggestionService.swift | 8 ++++-- .../CompletionSuggestionServiceProtocol.swift | 2 ++ .../View/CompletionSuggestionListItem.swift | 26 ++++++++++++++----- 7 files changed, 70 insertions(+), 24 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift index 91fc4ffeb..8476834b9 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionModels.swift @@ -25,12 +25,12 @@ enum CompletionSuggestionViewModelResult { } enum CompletionSuggestionViewStateItem: Identifiable { - case command(name: String) + case command(name: String, parametersFormat: String, description: String) case user(id: String, avatar: AvatarInputProtocol?, displayName: String?) var id: String { switch self { - case .command(let name): + case .command(let name, _, _): return name case .user(let id, _, _): return id diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift index 1427c3f3f..81d6e2088 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift @@ -64,10 +64,18 @@ extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol { extension MockCompletionSuggestionScreenState: CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { commands([ - CommandsProviderCommand(name: "/ban"), - CommandsProviderCommand(name: "/invite"), - CommandsProviderCommand(name: "/join"), - CommandsProviderCommand(name: "/me") + CommandsProviderCommand(name: "/ban", + parametersFormat: " [reason]", + description: "Bans user with given id"), + CommandsProviderCommand(name: "/invite", + parametersFormat: "", + description: "Invites user with given id to current room"), + CommandsProviderCommand(name: "/join", + parametersFormat: "", + description: "Joins room with given address"), + CommandsProviderCommand(name: "/me", + parametersFormat: "", + description: "Displays action") ]) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift index 01c881970..0c9c0215c 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift @@ -42,7 +42,11 @@ class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, Completi let items = completionSuggestionService.items.value.map { suggestionItem in switch suggestionItem { case .command(let completionSuggestionCommandItem): - return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name) + return CompletionSuggestionViewStateItem.command( + name: completionSuggestionCommandItem.name, + parametersFormat: completionSuggestionCommandItem.parametersFormat, + description: completionSuggestionCommandItem.description + ) case .user(let completionSuggestionUserItem): return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId, avatar: completionSuggestionUserItem, @@ -56,7 +60,11 @@ class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, Completi self?.state.items = items.map { item in switch item { case .command(let completionSuggestionCommandItem): - return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name) + return CompletionSuggestionViewStateItem.command( + name: completionSuggestionCommandItem.name, + parametersFormat: completionSuggestionCommandItem.parametersFormat, + description: completionSuggestionCommandItem.description + ) case .user(let completionSuggestionUserItem): return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId, avatar: completionSuggestionUserItem, diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 8da2356fd..f2dab2dab 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -95,8 +95,8 @@ final class CompletionSuggestionCoordinator: Coordinator, Presentable { if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first { self.delegate?.completionSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.completionSuggestionService.currentTextTrigger) - } else if let command = self.commandProvider.commands.filter({ $0 == identifier }).first { - self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.completionSuggestionService.currentTextTrigger) + } else if let command = self.commandProvider.commands.filter({ $0.name == identifier }).first { + self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command.name, textTrigger: self.completionSuggestionService.currentTextTrigger) } } } @@ -207,7 +207,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr private let room: MXRoom private let userID: String - var commands: [String] = [] + var commands: [(name: String, parametersFormat: String, description: String)] = [] init(room: MXRoom, userID: String) { self.room = room @@ -221,13 +221,23 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { self.commands = [ - "/ban", - "/invite", - "/join", - "/me" + (name: "/ban", + parametersFormat: " [reason]", + description: "Bans user with given id"), + (name: "/invite", + parametersFormat: "", + description: "Invites user with given id to current room"), + (name: "/join", + parametersFormat: "", + description: "Joins room with given address"), + (name: "/me", + parametersFormat: "", + description: "Displays action") ] // TODO: get real data - commands(self.commands.map { CommandsProviderCommand(name: $0) }) + commands(self.commands.map { CommandsProviderCommand(name: $0.name, + parametersFormat: $0.parametersFormat, + description: $0.description) }) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 0353b63d4..5adf4f3c5 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -25,7 +25,9 @@ struct RoomMembersProviderMember { } struct CommandsProviderCommand { - var name: String + let name: String + let parametersFormat: String + let description: String } class CompletionSuggestionUserID: NSObject { @@ -50,6 +52,8 @@ struct CompletionSuggestionServiceUserItem: CompletionSuggestionUserItemProtocol struct CompletionSuggestionServiceCommandItem: CompletionSuggestionCommandItemProtocol { let name: String + let parametersFormat: String + let description: String } class CompletionSuggestionService: CompletionSuggestionServiceProtocol { @@ -165,7 +169,7 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { guard let self else { return } self.suggestionItems = commands.map { command in - CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(name: command.name)) + CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(name: command.name, parametersFormat: command.parametersFormat, description: command.description)) } self.items.send(self.suggestionItems.filter { item in diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift index 4586e1294..3930c59d1 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionServiceProtocol.swift @@ -26,6 +26,8 @@ protocol CompletionSuggestionUserItemProtocol: Avatarable { protocol CompletionSuggestionCommandItemProtocol { var name: String { get } + var parametersFormat: String { get } + var description: String { get } } enum CompletionSuggestionItem { diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift index c30ec5d89..95f81fb75 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift @@ -30,12 +30,26 @@ struct CompletionSuggestionListItem: View { var body: some View { HStack { switch content { - case .command(let name): - Text(name) - .font(theme.fonts.body) - .foregroundColor(theme.colors.primaryContent) - .accessibility(identifier: "nameText") - .lineLimit(1) + case .command(let name, let parametersFormat, let description): + VStack(alignment: .leading) { + HStack { + Text(name) + .font(theme.fonts.body.bold()) + .foregroundColor(theme.colors.primaryContent) + .accessibility(identifier: "nameText") + .lineLimit(1) + Text(parametersFormat) + .font(theme.fonts.body.italic()) + .foregroundColor(theme.colors.tertiaryContent) + .accessibility(identifier: "parametersFormatText") + .lineLimit(1) + } + Text(description) + .font(theme.fonts.body) + .foregroundColor(theme.colors.tertiaryContent) + .accessibility(identifier: "descriptionText") + .lineLimit(1) + } case .user(let userId, let avatar, let displayName): if let avatar = avatar { AvatarImage(avatarData: avatar, size: .medium) From f6646f9059f28fc336515f0fd25d330e6782271d Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 12:10:03 +0200 Subject: [PATCH 067/149] Rework `MXKSlashCommands` to a more Swift-friendly form and use it in suggestion module --- Riot/Modules/MatrixKit/MatrixKit.h | 2 - .../MatrixKit/Models/Room/MXKRoomDataSource.m | 4 +- .../MatrixKit/Models/Room/MXKSlashCommands.h | 34 ------ .../MatrixKit/Models/Room/MXKSlashCommands.m | 30 ------ .../Models/Room/MXKSlashCommands.swift | 101 ++++++++++++++++++ .../Room/DataSources/RoomDataSource.swift | 2 +- Riot/Modules/Room/MXKRoomViewController.m | 43 ++++---- Riot/Modules/Room/RoomViewController.m | 6 +- .../CompletionSuggestionCoordinator.swift | 77 +++++++++---- .../View/CompletionSuggestionListItem.swift | 1 - 10 files changed, 185 insertions(+), 115 deletions(-) delete mode 100644 Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.h delete mode 100644 Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.m create mode 100644 Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift diff --git a/Riot/Modules/MatrixKit/MatrixKit.h b/Riot/Modules/MatrixKit/MatrixKit.h index 2bb02223b..ce6ea5f1e 100644 --- a/Riot/Modules/MatrixKit/MatrixKit.h +++ b/Riot/Modules/MatrixKit/MatrixKit.h @@ -145,5 +145,3 @@ #import "MXKCountryPickerViewController.h" #import "MXKLanguagePickerViewController.h" - -#import "MXKSlashCommands.h" diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 0998122ae..a69f504cc 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -31,8 +31,6 @@ #import "MXKAppSettings.h" -#import "MXKSlashCommands.h" - #import "GeneratedInterface-Swift.h" const BOOL USE_THREAD_TIMELINE = YES; @@ -316,7 +314,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { _filterMessagesWithURL = NO; - emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", kMXKSlashCmdEmote]; + emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]]; // Set default data and view classes // Cell data diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.h b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.h deleted file mode 100644 index ef9c71783..000000000 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - Copyright 2018 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; - -/** - Slash commands used to perform actions from a room. - */ - -FOUNDATION_EXPORT NSString *const kMXKSlashCmdChangeDisplayName; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdEmote; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdJoinRoom; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdPartRoom; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdInviteUser; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdKickUser; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdBanUser; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdUnbanUser; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdSetUserPowerLevel; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdResetUserPowerLevel; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdChangeRoomTopic; -FOUNDATION_EXPORT NSString *const kMXKSlashCmdDiscardSession; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.m b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.m deleted file mode 100644 index e9d483d9b..000000000 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.m +++ /dev/null @@ -1,30 +0,0 @@ -/* - Copyright 2018 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 "MXKSlashCommands.h" - -NSString *const kMXKSlashCmdChangeDisplayName = @"/nick"; -NSString *const kMXKSlashCmdEmote = @"/me"; -NSString *const kMXKSlashCmdJoinRoom = @"/join"; -NSString *const kMXKSlashCmdPartRoom = @"/part"; -NSString *const kMXKSlashCmdInviteUser = @"/invite"; -NSString *const kMXKSlashCmdKickUser = @"/kick"; -NSString *const kMXKSlashCmdBanUser = @"/ban"; -NSString *const kMXKSlashCmdUnbanUser = @"/unban"; -NSString *const kMXKSlashCmdSetUserPowerLevel = @"/op"; -NSString *const kMXKSlashCmdResetUserPowerLevel = @"/deop"; -NSString *const kMXKSlashCmdChangeRoomTopic = @"/topic"; -NSString *const kMXKSlashCmdDiscardSession = @"/discardsession"; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift new file mode 100644 index 000000000..faae85e94 --- /dev/null +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift @@ -0,0 +1,101 @@ +// +// Copyright 2023 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. +// + +@objc final class MXKSlashCommandsHelper: NSObject { + @objc static func commandNameFor(_ slashCommand: MXKSlashCommand) -> String { + slashCommand.cmd + } + + @objc static func commandUsageFor(_ slashCommand: MXKSlashCommand) -> String { + "Usage: \(slashCommand.cmd) \(slashCommand.parametersFormat)" + } +} + +@objc enum MXKSlashCommand: Int, CaseIterable { + case changeDisplayName + case emote + case joinRoom + case partRoom + case inviteUser + case kickUser + case banUser + case unbanUser + case setUserPowerLevel + case resetUserPowerLevel + case changeRoomTopic + case discardSession + + var cmd: String { + switch self { + case .changeDisplayName: + return "/nick" + case .emote: + return "/me" + case .joinRoom: + return "/join" + case .partRoom: + return "/part" + case .inviteUser: + return "/invite" + case .kickUser: + return "/kick" + case .banUser: + return "/ban" + case .unbanUser: + return "/unban" + case .setUserPowerLevel: + return "/op" + case .resetUserPowerLevel: + return "/deop" + case .changeRoomTopic: + return "/topic" + case .discardSession: + return "/discardsession" + } + } + + // Note: not localized for consistancy, as commands are in english + // also translating these parameters could lead to inconsistency in + // the UI in case of languages with otherlength translation. + var parametersFormat: String { + switch self { + case .changeDisplayName: + return "" + case .emote: + return "" + case .joinRoom: + return "" + case .partRoom: + return "[]" + case .inviteUser: + return "" + case .kickUser: + return " []" + case .banUser: + return " []" + case .unbanUser: + return "" + case .setUserPowerLevel: + return " " + case .resetUserPowerLevel: + return "" + case .changeRoomTopic: + return "" + case .discardSession: + return "" + } + } +} diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.swift b/Riot/Modules/Room/DataSources/RoomDataSource.swift index 281a7a046..89cbabe42 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.swift +++ b/Riot/Modules/Room/DataSources/RoomDataSource.swift @@ -19,7 +19,7 @@ import Foundation extension RoomDataSource { // MARK: - Private Constants private enum Constants { - static let emoteMessageSlashCommandPrefix = String(format: "%@ ", kMXKSlashCmdEmote) + static let emoteMessageSlashCommandPrefix = String(format: "%@ ", MXKSlashCommand.emote.cmd) } // MARK: - NSAttributedString Sending diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index 7c014e018..2e55c4771 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -39,7 +39,6 @@ #import "MXKEncryptionKeysImportView.h" #import "NSBundle+MatrixKit.h" -#import "MXKSlashCommands.h" #import "MXKSwiftHeader.h" #import "MXKPreviewViewController.h" @@ -1284,8 +1283,14 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; // TODO: display an alert with the cmd usage in case of error or unrecognized cmd. NSString *cmdUsage; + + NSString* kMXKSlashCmdChangeDisplayName = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeDisplayName]; + NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom]; + NSString* kMXKSlashCmdPartRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandPartRoom]; + NSString* kMXKSlashCmdChangeRoomTopic = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeRoomTopic]; + - if ([cmd isEqualToString:kMXKSlashCmdEmote]) + if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]]) { // send message as an emote [self sendTextMessage:string]; @@ -1320,7 +1325,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /nick "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeDisplayName]; } } else if ([string hasPrefix:kMXKSlashCmdJoinRoom]) @@ -1355,7 +1360,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /join "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom]; } } else if ([string hasPrefix:kMXKSlashCmdPartRoom]) @@ -1413,7 +1418,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /part []"; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandPartRoom]; } } else if ([string hasPrefix:kMXKSlashCmdChangeRoomTopic]) @@ -1445,10 +1450,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /topic "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeRoomTopic]; } } - else if ([string hasPrefix:kMXKSlashCmdDiscardSession]) + else if ([string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandDiscardSession]]) { [roomDataSource.mxSession.crypto discardOutboundGroupSessionForRoomWithRoomId:roomDataSource.roomId onComplete:^{ MXLogDebug(@"[MXKRoomVC] Manually discarded outbound group session"); @@ -1470,7 +1475,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; userId = nil; } - if ([cmd isEqualToString:kMXKSlashCmdInviteUser]) + if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandInviteUser]]) { if (userId) { @@ -1489,10 +1494,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /invite "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandInviteUser]; } } - else if ([cmd isEqualToString:kMXKSlashCmdKickUser]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandKickUser]]) { if (userId) { @@ -1524,10 +1529,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /kick []"; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandKickUser]; } } - else if ([cmd isEqualToString:kMXKSlashCmdBanUser]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandBanUser]]) { if (userId) { @@ -1559,10 +1564,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /ban []"; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandBanUser]; } } - else if ([cmd isEqualToString:kMXKSlashCmdUnbanUser]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandUnbanUser]]) { if (userId) { @@ -1581,10 +1586,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /unban "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandUnbanUser]; } } - else if ([cmd isEqualToString:kMXKSlashCmdSetUserPowerLevel]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandSetUserPowerLevel]]) { // Retrieve power level NSString *powerLevel = nil; @@ -1617,10 +1622,10 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /op "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandSetUserPowerLevel]; } } - else if ([cmd isEqualToString:kMXKSlashCmdResetUserPowerLevel]) + else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandResetUserPowerLevel]]) { if (userId) { @@ -1639,7 +1644,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; else { // Display cmd usage in text input as placeholder - cmdUsage = @"Usage: /deop "; + cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandResetUserPowerLevel]; } } else diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 38f71f8d1..274e7d437 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1281,6 +1281,8 @@ static CGSize kThreadListBarButtonItemImageSize; - (BOOL)sendAsIRCStyleCommandIfPossible:(NSString*)string { // Override the default behavior for `/join` command in order to open automatically the joined room + + NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom]; if ([string hasPrefix:kMXKSlashCmdJoinRoom]) { @@ -1317,7 +1319,7 @@ static CGSize kThreadListBarButtonItemImageSize; else { // Display cmd usage in text input as placeholder - self.inputToolbarView.placeholder = @"Usage: /join "; + self.inputToolbarView.placeholder = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom]; } return YES; } @@ -5237,7 +5239,7 @@ static CGSize kThreadListBarButtonItemImageSize; if (readyToSend) { BOOL isMessageAHandledCommand = NO; // "/me" command is supported with Pills in RoomDataSource. - if (![attributedTextMessage.string hasPrefix:kMXKSlashCmdEmote]) + if (![attributedTextMessage.string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]]) { // Other commands currently work with identifiers (e.g. ban, invite, op, etc). NSString *message; diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index f2dab2dab..8669e812f 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -95,8 +95,8 @@ final class CompletionSuggestionCoordinator: Coordinator, Presentable { if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first { self.delegate?.completionSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.completionSuggestionService.currentTextTrigger) - } else if let command = self.commandProvider.commands.filter({ $0.name == identifier }).first { - self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command.name, textTrigger: self.completionSuggestionService.currentTextTrigger) + } else if let command = self.commandProvider.commands.filter({ $0.cmd == identifier }).first { + self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command.cmd, textTrigger: self.completionSuggestionService.currentTextTrigger) } } } @@ -207,7 +207,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr private let room: MXRoom private let userID: String - var commands: [(name: String, parametersFormat: String, description: String)] = [] + var commands = MXKSlashCommand.allCases init(room: MXRoom, userID: String) { self.room = room @@ -216,28 +216,59 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr } func updateWithPowerLevels() { - // TODO: filter commands in terms of user power level ? + room.state { [weak self] state in + guard let self, let powerLevels = state?.powerLevels else { return } + + // Note: for now only filter out `/op` and `/deop` (same as Element-Web), + // but we could use power level for ban/invite/etc to filter further. + let adminOnlyCommands: [MXKSlashCommand] = [.setUserPowerLevel, .resetUserPowerLevel] + let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID) + + if RoomPowerLevel(rawValue: userPowerLevel) != .admin { + self.commands = self.commands.filter { + !adminOnlyCommands.contains($0) + } + } + } } func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { - self.commands = [ - (name: "/ban", - parametersFormat: " [reason]", - description: "Bans user with given id"), - (name: "/invite", - parametersFormat: "", - description: "Invites user with given id to current room"), - (name: "/join", - parametersFormat: "", - description: "Joins room with given address"), - (name: "/me", - parametersFormat: "", - description: "Displays action") - ] - - // TODO: get real data - commands(self.commands.map { CommandsProviderCommand(name: $0.name, - parametersFormat: $0.parametersFormat, - description: $0.description) }) + commands(self.commands.map { CommandsProviderCommand( + name: $0.cmd, + parametersFormat: $0.parametersFormat, + description: $0.description + )}) + } +} + +private extension MXKSlashCommand { + // TODO: L10N + var description: String { + switch self { + case .changeDisplayName: + return "Changes your display nickname" + case .emote: + return "Displays action" + case .joinRoom: + return "Joins room with given address" + case .partRoom: + return "Leave room" + case .inviteUser: + return "Invites user with given id to current room" + case .kickUser: + return "Removes user with given id from this room" + case .banUser: + return "Bans user with given id" + case .unbanUser: + return "Unbans user with given id" + case .setUserPowerLevel: + return "Define the power level of a user" + case .resetUserPowerLevel: + return "Deops user with given id" + case .changeRoomTopic: + return "Sets the room topic" + case .discardSession: + return "Forces the current outbound group session in an encrypted room to be discarded" + } } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift index 95f81fb75..4a1616189 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListItem.swift @@ -48,7 +48,6 @@ struct CompletionSuggestionListItem: View { .font(theme.fonts.body) .foregroundColor(theme.colors.tertiaryContent) .accessibility(identifier: "descriptionText") - .lineLimit(1) } case .user(let userId, let avatar, let displayName): if let avatar = avatar { From 57b626593b56cadd440fb11499c71d7c2b0b03f0 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 14:12:55 +0200 Subject: [PATCH 068/149] Display all commands when a single slash is entered --- .../Service/CompletionSuggestionService.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 5adf4f3c5..b16efc137 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -162,21 +162,29 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { }) } case "/": - // TODO: send all commands if only text is "/" partialName.removeFirst() commandProvider.fetchCommands { [weak self] commands in guard let self else { return } self.suggestionItems = commands.map { command in - CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(name: command.name, parametersFormat: command.parametersFormat, description: command.description)) + CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem( + name: command.name, + parametersFormat: command.parametersFormat, + description: command.description + )) } - self.items.send(self.suggestionItems.filter { item in - guard case let .command(commandSuggestion) = item else { return false } + if partialName.isEmpty { + // A single `/` will display all available commands. + self.items.send(self.suggestionItems) + } else { + self.items.send(self.suggestionItems.filter { item in + guard case let .command(commandSuggestion) = item else { return false } - return commandSuggestion.name.lowercased().contains(partialName.lowercased()) - }) + return commandSuggestion.name.lowercased().contains(partialName.lowercased()) + }) + } } default: return From 5e879bf53305298da7a9128307f3c53b2b378591 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 15:43:20 +0200 Subject: [PATCH 069/149] Rework `CompletionSuggestionService` text trigger --- .../Service/CompletionSuggestionService.swift | 75 ++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index b16efc137..86a99370f 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -65,7 +65,7 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { private let commandProvider: CommandsProviderProtocol private var suggestionItems: [CompletionSuggestionItem] = [] - private let currentTextTriggerSubject = CurrentValueSubject(nil) + private let currentTextTriggerSubject = CurrentValueSubject(nil) private var cancellables = Set() // MARK: Public @@ -73,7 +73,7 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { var items = CurrentValueSubject<[CompletionSuggestionItem], Never>([]) var currentTextTrigger: String? { - currentTextTriggerSubject.value + currentTextTriggerSubject.value?.asString() } // MARK: - Setup @@ -88,11 +88,11 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { currentTextTriggerSubject .debounce(for: 0.5, scheduler: RunLoop.main) .removeDuplicates() - .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } + .sink { [weak self] in self?.fetchAndFilterSuggestionsForTextTrigger($0) } .store(in: &cancellables) } else { currentTextTriggerSubject - .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } + .sink { [weak self] in self?.fetchAndFilterSuggestionsForTextTrigger($0) } .store(in: &cancellables) } } @@ -101,16 +101,14 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { func processTextMessage(_ textMessage: String?) { guard let textMessage = textMessage, - textMessage.count > 0, - let lastComponent = textMessage.components(separatedBy: .whitespaces).last, - lastComponent.prefix(while: { $0 == "@" || $0 == "/" }).count == 1 // Partial username should start with one and only one "@" character + let textTrigger = textMessage.currentTextTrigger else { items.send([]) currentTextTriggerSubject.send(nil) return } - currentTextTriggerSubject.send(lastComponent) + currentTextTriggerSubject.send(textTrigger) } func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) { @@ -122,27 +120,23 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { switch suggestionPattern.key { case .at: - currentTextTriggerSubject.send("@" + suggestionPattern.text) + currentTextTriggerSubject.send(TextTrigger(key: .at, text: suggestionPattern.text)) case .hash: // No room suggestion support yet items.send([]) currentTextTriggerSubject.send(nil) case .slash: - currentTextTriggerSubject.send("/" + suggestionPattern.text) + currentTextTriggerSubject.send(TextTrigger(key: .slash, text: suggestionPattern.text)) } } // MARK: - Private - private func fetchAndFilterMembersForTextTrigger(_ textTrigger: String?) { - guard var partialName = textTrigger else { - return - } - - switch partialName.first { - case "@": - partialName.removeFirst() // remove the '@' prefix + private func fetchAndFilterSuggestionsForTextTrigger(_ textTrigger: TextTrigger?) { + guard let textTrigger else { return } + switch textTrigger.key { + case .at: roomMemberProvider.fetchMembers { [weak self] members in guard let self = self else { return @@ -155,15 +149,13 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { self.items.send(self.suggestionItems.filter { item in guard case let .user(completionSuggestionUserItem) = item else { return false } - let containedInUsername = completionSuggestionUserItem.userId.lowercased().contains(partialName.lowercased()) - let containedInDisplayName = (completionSuggestionUserItem.displayName ?? "").lowercased().contains(partialName.lowercased()) + let containedInUsername = completionSuggestionUserItem.userId.lowercased().contains(textTrigger.text.lowercased()) + let containedInDisplayName = (completionSuggestionUserItem.displayName ?? "").lowercased().contains(textTrigger.text.lowercased()) return (containedInUsername || containedInDisplayName) }) } - case "/": - partialName.removeFirst() - + case .slash: commandProvider.fetchCommands { [weak self] commands in guard let self else { return } @@ -175,19 +167,17 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { )) } - if partialName.isEmpty { + if textTrigger.text.isEmpty { // A single `/` will display all available commands. self.items.send(self.suggestionItems) } else { self.items.send(self.suggestionItems.filter { item in guard case let .command(commandSuggestion) = item else { return false } - return commandSuggestion.name.lowercased().contains(partialName.lowercased()) + return commandSuggestion.name.lowercased().contains(textTrigger.text.lowercased()) }) } } - default: - return } } } @@ -199,3 +189,34 @@ extension Array where Element == RoomMembersProviderMember { return self + [RoomMembersProviderMember(userId: CompletionSuggestionUserID.room, displayName: "Everyone", avatarUrl: "")] } } + +private enum SuggestionKey: Character { + case at = "@" + case slash = "/" +} + +private struct TextTrigger: Equatable { + let key: SuggestionKey + let text: String + + func asString() -> String { + return String(key.rawValue) + text + } +} + +private extension String { + // Returns current completion suggestion for a text message, if any. + var currentTextTrigger: TextTrigger? { + let components = self.components(separatedBy: .whitespaces) + guard var lastComponent = components.last, + lastComponent.count > 0, + let suggestionKey = SuggestionKey(rawValue: lastComponent.removeFirst()), + // If a second character exists and is the same as the key it shouldn't trigger. + lastComponent.first != suggestionKey.rawValue, + // Slash commands should be displayed only if there is a single component + !(suggestionKey == .slash && components.count > 1) + else { return nil } + + return TextTrigger(key: suggestionKey, text: lastComponent) + } +} From 49388b95f3a95dcf4dcda33d6acf6cffcb5410e8 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 16:01:17 +0200 Subject: [PATCH 070/149] Re-enable unit tests and fix a few lint warnings --- .../CompletionSuggestionScreenState.swift | 2 +- .../CompletionSuggestionViewModel.swift | 2 +- .../CompletionSuggestionCoordinator.swift | 9 ++------- .../Service/CompletionSuggestionService.swift | 4 ++-- .../CompletionSuggestionServiceTests.swift | 19 ++++++++++++++----- .../View/CompletionSuggestionList.swift | 11 +++-------- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift index 81d6e2088..b78c25575 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift @@ -65,7 +65,7 @@ extension MockCompletionSuggestionScreenState: CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { commands([ CommandsProviderCommand(name: "/ban", - parametersFormat: " [reason]", + parametersFormat: " []", description: "Bans user with given id"), CommandsProviderCommand(name: "/invite", parametersFormat: "", diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift index 0c9c0215c..53d2c6975 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionViewModel.swift @@ -29,7 +29,7 @@ class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, Completi // MARK: Public var sharedContext: CompletionSuggestionViewModelType.Context { - return self.context + context } var completion: ((CompletionSuggestionViewModelResult) -> Void)? diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 8669e812f..102994636 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -103,8 +103,7 @@ final class CompletionSuggestionCoordinator: Coordinator, Presentable { completionSuggestionService.items.sink { [weak self] _ in guard let self = self else { return } - self.delegate?.completionSuggestionCoordinator(self, - didUpdateViewHeight: self.calculateViewHeight()) + self.delegate?.completionSuggestionCoordinator(self, didUpdateViewHeight: self.calculateViewHeight()) }.store(in: &cancellables) } @@ -233,11 +232,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr } func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { - commands(self.commands.map { CommandsProviderCommand( - name: $0.cmd, - parametersFormat: $0.parametersFormat, - description: $0.description - )}) + commands(self.commands.map { CommandsProviderCommand(name: $0.cmd, parametersFormat: $0.parametersFormat, description: $0.description) }) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 86a99370f..09b229ec4 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -200,14 +200,14 @@ private struct TextTrigger: Equatable { let text: String func asString() -> String { - return String(key.rawValue) + text + String(key.rawValue) + text } } private extension String { // Returns current completion suggestion for a text message, if any. var currentTextTrigger: TextTrigger? { - let components = self.components(separatedBy: .whitespaces) + let components = components(separatedBy: .whitespaces) guard var lastComponent = components.last, lastComponent.count > 0, let suggestionKey = SuggestionKey(rawValue: lastComponent.removeFirst()), diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift index 636ba3355..18283bfb3 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift @@ -143,11 +143,20 @@ extension CompletionSuggestionServiceTests: RoomMembersProviderProtocol { extension CompletionSuggestionServiceTests: CommandsProviderProtocol { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { - let commandList = ["/ban", "/invite", "/join", "/me"] - - commands(commandList.map { command in - CommandsProviderCommand(name: command) - }) + commands([ + CommandsProviderCommand(name: "/ban", + parametersFormat: " []", + description: "Bans user with given id"), + CommandsProviderCommand(name: "/invite", + parametersFormat: "", + description: "Invites user with given id to current room"), + CommandsProviderCommand(name: "/join", + parametersFormat: "", + description: "Joins room with given address"), + CommandsProviderCommand(name: "/me", + parametersFormat: "", + description: "Displays action") + ]) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift index 02aef8a1f..cf8e34e02 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionList.swift @@ -30,7 +30,7 @@ struct CompletionSuggestionList: View { to the list items in order to be as close as possible as the `UITableView` display. */ - @available (iOS 16.0, *) + @available(iOS 16.0, *) static let collectionViewPaddingCorrection: CGFloat = -5.0 } @@ -44,19 +44,14 @@ struct CompletionSuggestionList: View { // MARK: Public @ObservedObject var viewModel: CompletionSuggestionViewModel.Context - var showBackgroundShadow: Bool = true + var showBackgroundShadow = true var body: some View { if viewModel.viewState.items.isEmpty { EmptyView() } else { ZStack { - CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user( - id: "Prototype", - avatar: AvatarInput(mxContentUri: "", - matrixItemId: "", - displayName: "Prototype"), - displayName: "Prototype")) + CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user(id: "Prototype", avatar: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Prototype"), displayName: "Prototype")) .background(ViewFrameReader(frame: $prototypeListItemFrame)) .hidden() if showBackgroundShadow { From e6f8565228b36b0ae3b0f0c52aaa451c8c59c821 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 16:43:36 +0200 Subject: [PATCH 071/149] Move room admin condition to be usable in UnitTests and add tests --- .../CompletionSuggestionScreenState.swift | 22 +++- .../CompletionSuggestionCoordinator.swift | 24 ++-- .../Service/CompletionSuggestionService.swift | 11 +- .../CompletionSuggestionServiceTests.swift | 105 +++++++++++++++++- .../CompletionSuggestionListWithInput.swift | 4 +- 5 files changed, 144 insertions(+), 22 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift index b78c25575..5bdd72088 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/CompletionSuggestionScreenState.swift @@ -62,20 +62,34 @@ extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol { } extension MockCompletionSuggestionScreenState: CommandsProviderProtocol { + var isRoomAdmin: Bool { false } + func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { commands([ CommandsProviderCommand(name: "/ban", parametersFormat: " []", - description: "Bans user with given id"), + description: "Bans user with given id", + requiresAdminPowerLevel: false), CommandsProviderCommand(name: "/invite", parametersFormat: "", - description: "Invites user with given id to current room"), + description: "Invites user with given id to current room", + requiresAdminPowerLevel: false), CommandsProviderCommand(name: "/join", parametersFormat: "", - description: "Joins room with given address"), + description: "Joins room with given address", + requiresAdminPowerLevel: false), + CommandsProviderCommand(name: "/op", + parametersFormat: " ", + description: "Define the power level of a user", + requiresAdminPowerLevel: true), + CommandsProviderCommand(name: "/deop", + parametersFormat: "", + description: "Deops user with given id", + requiresAdminPowerLevel: true), CommandsProviderCommand(name: "/me", parametersFormat: "", - description: "Displays action") + description: "Displays action", + requiresAdminPowerLevel: false) ]) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 102994636..6c868ce4c 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -207,6 +207,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr private let userID: String var commands = MXKSlashCommand.allCases + var isRoomAdmin = false init(room: MXRoom, userID: String) { self.room = room @@ -218,21 +219,13 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr room.state { [weak self] state in guard let self, let powerLevels = state?.powerLevels else { return } - // Note: for now only filter out `/op` and `/deop` (same as Element-Web), - // but we could use power level for ban/invite/etc to filter further. - let adminOnlyCommands: [MXKSlashCommand] = [.setUserPowerLevel, .resetUserPowerLevel] let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID) - - if RoomPowerLevel(rawValue: userPowerLevel) != .admin { - self.commands = self.commands.filter { - !adminOnlyCommands.contains($0) - } - } + isRoomAdmin = RoomPowerLevel(rawValue: userPowerLevel) == .admin } } func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { - commands(self.commands.map { CommandsProviderCommand(name: $0.cmd, parametersFormat: $0.parametersFormat, description: $0.description) }) + commands(self.commands.map { CommandsProviderCommand(name: $0.cmd, parametersFormat: $0.parametersFormat, description: $0.description, requiresAdminPowerLevel: $0.requiresAdminPowerLevel) }) } } @@ -266,4 +259,15 @@ private extension MXKSlashCommand { return "Forces the current outbound group session in an encrypted room to be discarded" } } + + // Note: for now only filter out `/op` and `/deop` (same as Element-Web), + // but we could use power level for ban/invite/etc to filter further. + var requiresAdminPowerLevel: Bool { + switch self { + case .setUserPowerLevel, .resetUserPowerLevel: + return true + default: + return false + } + } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift index 09b229ec4..5ded36c2c 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Service/CompletionSuggestionService.swift @@ -28,6 +28,7 @@ struct CommandsProviderCommand { let name: String let parametersFormat: String let description: String + let requiresAdminPowerLevel: Bool } class CompletionSuggestionUserID: NSObject { @@ -41,6 +42,7 @@ protocol RoomMembersProviderProtocol { } protocol CommandsProviderProtocol { + var isRoomAdmin: Bool { get } func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) } @@ -159,7 +161,7 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol { commandProvider.fetchCommands { [weak self] commands in guard let self else { return } - self.suggestionItems = commands.map { command in + self.suggestionItems = commands.filtered(isRoomAdmin: self.commandProvider.isRoomAdmin).map { command in CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem( name: command.name, parametersFormat: command.parametersFormat, @@ -190,6 +192,13 @@ extension Array where Element == RoomMembersProviderMember { } } +extension Array where Element == CommandsProviderCommand { + func filtered(isRoomAdmin: Bool) -> Self { + guard !isRoomAdmin else { return self } + return filter { !$0.requiresAdminPowerLevel } + } +} + private enum SuggestionKey: Character { case at = "@" case slash = "/" diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift index 18283bfb3..90542868d 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Test/Unit/CompletionSuggestionServiceTests.swift @@ -22,14 +22,18 @@ import XCTest class CompletionSuggestionServiceTests: XCTestCase { var service: CompletionSuggestionService! var canMentionRoom = false + var isRoomAdmin = false override func setUp() { service = CompletionSuggestionService(roomMemberProvider: self, commandProvider: self, shouldDebounce: false) canMentionRoom = false + isRoomAdmin = false } - + + // MARK: - User suggestions + func testAlice() { service.processTextMessage("@Al") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice") @@ -128,6 +132,85 @@ class CompletionSuggestionServiceTests: XCTestCase { // Then the completion for a room mention should be shown. XCTAssertEqual(service.items.value.first?.asUser?.userId, CompletionSuggestionUserID.room) } + + // MARK: - Command suggestions + + func testJoin() { + service.processTextMessage("/jo") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join") + + service.processTextMessage("/joi") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join") + + service.processTextMessage("/join") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join") + + service.processTextMessage("/oin") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join") + } + + func testInvite() { + service.processTextMessage("/inv") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/invite") + + service.processTextMessage("/invite") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/invite") + + service.processTextMessage("/vite") + XCTAssertEqual(service.items.value.first?.asCommand?.name, "/invite") + } + + func testMultipleResults() { + service.processTextMessage("/in") + XCTAssertEqual( + service.items.value.compactMap { $0.asCommand?.name }, + ["/invite", "/join"] + ) + } + + func testDoubleSlashDontTrigger() { + service.processTextMessage("//") + XCTAssertTrue(service.items.value.isEmpty) + } + + func testNonLeadingSlashCommandDontTrigger() { + service.processTextMessage("test /joi") + XCTAssertTrue(service.items.value.isEmpty) + } + + func testAdminCommandsAreNotAvailable() { + isRoomAdmin = false + + service.processTextMessage("/op") + XCTAssertTrue(service.items.value.isEmpty) + } + + func testAdminCommandsAreAvailable() { + isRoomAdmin = true + + service.processTextMessage("/op") + XCTAssertEqual(service.items.value.compactMap { $0.asCommand?.name }, ["/op", "/deop"]) + } + + func testDisplayAllCommandsAsStandardUser() { + isRoomAdmin = false + + service.processTextMessage("/") + XCTAssertEqual( + service.items.value.compactMap { $0.asCommand?.name }, + ["/ban", "/invite", "/join", "/me"] + ) + } + + func testDisplayAllCommandsAsAdmin() { + isRoomAdmin = true + + service.processTextMessage("/") + XCTAssertEqual( + service.items.value.compactMap { $0.asCommand?.name }, + ["/ban", "/invite", "/join", "/op", "/deop", "/me"] + ) + } } extension CompletionSuggestionServiceTests: RoomMembersProviderProtocol { @@ -146,16 +229,28 @@ extension CompletionSuggestionServiceTests: CommandsProviderProtocol { commands([ CommandsProviderCommand(name: "/ban", parametersFormat: " []", - description: "Bans user with given id"), + description: "Bans user with given id", + requiresAdminPowerLevel: false), CommandsProviderCommand(name: "/invite", parametersFormat: "", - description: "Invites user with given id to current room"), + description: "Invites user with given id to current room", + requiresAdminPowerLevel: false), CommandsProviderCommand(name: "/join", parametersFormat: "", - description: "Joins room with given address"), + description: "Joins room with given address", + requiresAdminPowerLevel: false), + CommandsProviderCommand(name: "/op", + parametersFormat: " ", + description: "Define the power level of a user", + requiresAdminPowerLevel: true), + CommandsProviderCommand(name: "/deop", + parametersFormat: "", + description: "Deops user with given id", + requiresAdminPowerLevel: true), CommandsProviderCommand(name: "/me", parametersFormat: "", - description: "Displays action") + description: "Displays action", + requiresAdminPowerLevel: false) ]) } } diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift index 0b1dd8e8a..223b4fbc6 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/View/CompletionSuggestionListWithInput.swift @@ -34,13 +34,13 @@ struct CompletionSuggestionListWithInput: View { var body: some View { VStack(spacing: 0.0) { CompletionSuggestionList(viewModel: viewModel.listViewModel.context) - TextField("Search for user", text: $inputText) + TextField("Search for user/command", text: $inputText) .background(Color.white) .onChange(of: inputText, perform: viewModel.callback) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding([.leading, .trailing]) .onAppear { - inputText = "@-" // Make the list show all available mock results + inputText = "@-" // Make the list show all available user mock results } } } From 4910c3ef5c4f04e79dab6a57d945988845a9d9fb Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 16:50:44 +0200 Subject: [PATCH 072/149] Add changelog --- changelog.d/7493.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7493.feature diff --git a/changelog.d/7493.feature b/changelog.d/7493.feature new file mode 100644 index 000000000..075a7f6a2 --- /dev/null +++ b/changelog.d/7493.feature @@ -0,0 +1 @@ +Add composer suggestions for slash commands From 0ff30525616f20a18ce6091a4e17eb2b3fea4dec Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 17:09:02 +0200 Subject: [PATCH 073/149] L10N --- Riot/Assets/en.lproj/Vector.strings | 14 ++++++ Riot/Generated/Strings.swift | 48 +++++++++++++++++++ .../CompletionSuggestionCoordinator.swift | 25 +++++----- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c1099f168..38cded4a3 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -614,6 +614,20 @@ Tap the + to start adding people."; "room_join_group_call" = "Join"; "room_no_privileges_to_create_group_call" = "You need to be an admin or a moderator to start a call."; +// Room commands descriptions +"room_command_change_display_name_description" = "Changes your display nickname"; +"room_command_emote_description" = "Displays action"; +"room_command_join_room_description" = "Joins room with given address"; +"room_command_part_room_description" = "Leave room"; +"room_command_invite_user_description" = "Invites user with given id to current room"; +"room_command_kick_user_description" = "Removes user with given id from this room"; +"room_command_ban_user_description" = "Bans user with given id"; +"room_command_unban_user_description" = "Unbans user with given id"; +"room_command_set_user_power_level_description" = "Define the power level of a user"; +"room_command_reset_user_power_level_description" = "Deops user with given id"; +"room_command_change_room_topic_description" = "Sets the room topic"; +"room_command_discard_session_description" = "Forces the current outbound group session in an encrypted room to be discarded"; + // MARK: Threads "room_thread_title" = "Thread"; "thread_copy_link_to_thread" = "Copy link to thread"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 0cdd03d2d..e48764f1d 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -5211,6 +5211,54 @@ public class VectorL10n: NSObject { public static var roomAvatarViewAccessibilityLabel: String { return VectorL10n.tr("Vector", "room_avatar_view_accessibility_label") } + /// Bans user with given id + public static var roomCommandBanUserDescription: String { + return VectorL10n.tr("Vector", "room_command_ban_user_description") + } + /// Changes your display nickname + public static var roomCommandChangeDisplayNameDescription: String { + return VectorL10n.tr("Vector", "room_command_change_display_name_description") + } + /// Sets the room topic + public static var roomCommandChangeRoomTopicDescription: String { + return VectorL10n.tr("Vector", "room_command_change_room_topic_description") + } + /// Forces the current outbound group session in an encrypted room to be discarded + public static var roomCommandDiscardSessionDescription: String { + return VectorL10n.tr("Vector", "room_command_discard_session_description") + } + /// Displays action + public static var roomCommandEmoteDescription: String { + return VectorL10n.tr("Vector", "room_command_emote_description") + } + /// Invites user with given id to current room + public static var roomCommandInviteUserDescription: String { + return VectorL10n.tr("Vector", "room_command_invite_user_description") + } + /// Joins room with given address + public static var roomCommandJoinRoomDescription: String { + return VectorL10n.tr("Vector", "room_command_join_room_description") + } + /// Removes user with given id from this room + public static var roomCommandKickUserDescription: String { + return VectorL10n.tr("Vector", "room_command_kick_user_description") + } + /// Leave room + public static var roomCommandPartRoomDescription: String { + return VectorL10n.tr("Vector", "room_command_part_room_description") + } + /// Deops user with given id + public static var roomCommandResetUserPowerLevelDescription: String { + return VectorL10n.tr("Vector", "room_command_reset_user_power_level_description") + } + /// Define the power level of a user + public static var roomCommandSetUserPowerLevelDescription: String { + return VectorL10n.tr("Vector", "room_command_set_user_power_level_description") + } + /// Unbans user with given id + public static var roomCommandUnbanUserDescription: String { + return VectorL10n.tr("Vector", "room_command_unban_user_description") + } /// You need permission to manage conference call in this room public static var roomConferenceCallNoPower: String { return VectorL10n.tr("Vector", "room_conference_call_no_power") diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index 6c868ce4c..c02df825f 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -230,33 +230,32 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr } private extension MXKSlashCommand { - // TODO: L10N var description: String { switch self { case .changeDisplayName: - return "Changes your display nickname" + return VectorL10n.roomCommandChangeDisplayNameDescription case .emote: - return "Displays action" + return VectorL10n.roomCommandEmoteDescription case .joinRoom: - return "Joins room with given address" + return VectorL10n.roomCommandJoinRoomDescription case .partRoom: - return "Leave room" + return VectorL10n.roomCommandPartRoomDescription case .inviteUser: - return "Invites user with given id to current room" + return VectorL10n.roomCommandInviteUserDescription case .kickUser: - return "Removes user with given id from this room" + return VectorL10n.roomCommandKickUserDescription case .banUser: - return "Bans user with given id" + return VectorL10n.roomCommandBanUserDescription case .unbanUser: - return "Unbans user with given id" + return VectorL10n.roomCommandUnbanUserDescription case .setUserPowerLevel: - return "Define the power level of a user" + return VectorL10n.roomCommandSetUserPowerLevelDescription case .resetUserPowerLevel: - return "Deops user with given id" + return VectorL10n.roomCommandResetUserPowerLevelDescription case .changeRoomTopic: - return "Sets the room topic" + return VectorL10n.roomCommandChangeRoomTopicDescription case .discardSession: - return "Forces the current outbound group session in an encrypted room to be discarded" + return VectorL10n.roomCommandDiscardSessionDescription } } From 26b2d0b6d847ee2dc8701bbb8dfc2d5b1f7c5c01 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 20 Apr 2023 17:12:09 +0200 Subject: [PATCH 074/149] Fix comment typo --- Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift index faae85e94..7498b5769 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift @@ -67,7 +67,7 @@ } } - // Note: not localized for consistancy, as commands are in english + // Note: not localized for consistency, as commands are in english // also translating these parameters could lead to inconsistency in // the UI in case of languages with otherlength translation. var parametersFormat: String { From bf0edd46570a304b5f9220fa24be83ebe4f09816 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 21 Apr 2023 09:11:35 +0200 Subject: [PATCH 075/149] Fix missing self in closure --- .../Coordinator/CompletionSuggestionCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift index c02df825f..4196da77a 100644 --- a/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/CompletionSuggestion/Coordinator/CompletionSuggestionCoordinator.swift @@ -220,7 +220,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr guard let self, let powerLevels = state?.powerLevels else { return } let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID) - isRoomAdmin = RoomPowerLevel(rawValue: userPowerLevel) == .admin + self.isRoomAdmin = RoomPowerLevel(rawValue: userPowerLevel) == .admin } } From cf941fa636b164c1c0a90d57ec6ff6a99c5543a8 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 21 Apr 2023 09:30:56 +0200 Subject: [PATCH 076/149] Fix `RoomInputToolbarTextView` pills flushing --- .../RoomInputToolbarTextView.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift index 47d981c86..eabd68c9e 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift @@ -23,6 +23,7 @@ class RoomInputToolbarTextView: UITextView { private var heightConstraint: NSLayoutConstraint! + private var pillViews = [UIView]() weak var toolbarDelegate: RoomInputToolbarTextViewDelegate? @@ -51,12 +52,18 @@ class RoomInputToolbarTextView: UITextView { } override var text: String! { + willSet { + flushPills() + } didSet { updateUI() } } override var attributedText: NSAttributedString! { + willSet { + flushPills() + } didSet { updateUI() } @@ -162,3 +169,17 @@ class RoomInputToolbarTextView: UITextView { delegate.onTouchUp(inside: delegate.rightInputToolbarButton) } } + +extension RoomInputToolbarTextView: PillViewFlusher { + func registerPillView(_ pillView: UIView) { + pillViews.append(pillView) + } + + private func flushPills() { + for view in pillViews { + view.alpha = 0.0 + view.removeFromSuperview() + } + pillViews.removeAll() + } +} From 1cd3b97d7eba8db3e9fb0e54a7d78579f300a066 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 21 Apr 2023 13:47:15 +0200 Subject: [PATCH 077/149] Fix typo --- Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift index 7498b5769..54ab1ab3c 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSlashCommands.swift @@ -69,7 +69,7 @@ // Note: not localized for consistency, as commands are in english // also translating these parameters could lead to inconsistency in - // the UI in case of languages with otherlength translation. + // the UI in case of languages with overlength translation. var parametersFormat: String { switch self { case .changeDisplayName: From 3d64f80fc697ae5eae8bdcf4a22b616a2ffbabe7 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 21 Apr 2023 17:13:03 +0200 Subject: [PATCH 078/149] Fix sending command with Pills through RTE --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 +++ .../MXKRoomInputToolbarView.h | 8 ++++++ Riot/Modules/Room/RoomViewController.m | 21 ++++++++++++++++ Riot/Modules/Room/RoomViewController.swift | 2 +- .../WysiwygInputToolbarView.swift | 25 ++++++++++++++++++- 6 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 38cded4a3..1aadc203f 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -627,6 +627,7 @@ Tap the + to start adding people."; "room_command_reset_user_power_level_description" = "Deops user with given id"; "room_command_change_room_topic_description" = "Sets the room topic"; "room_command_discard_session_description" = "Forces the current outbound group session in an encrypted room to be discarded"; +"room_command_error_unknown_command" = "Invalid or unhandled command"; // MARK: Threads "room_thread_title" = "Thread"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index e48764f1d..db052a786 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -5231,6 +5231,10 @@ public class VectorL10n: NSObject { public static var roomCommandEmoteDescription: String { return VectorL10n.tr("Vector", "room_command_emote_description") } + /// Invalid or unhandled command + public static var roomCommandErrorUnknownCommand: String { + return VectorL10n.tr("Vector", "room_command_error_unknown_command") + } /// Invites user with given id to current room public static var roomCommandInviteUserDescription: String { return VectorL10n.tr("Vector", "room_command_invite_user_description") diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h index e366ae239..abd67ec7e 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h @@ -102,6 +102,14 @@ typedef enum : NSUInteger */ - (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendFormattedTextMessage:(NSString *)formattedTextMessage withRawText:(NSString *)rawText; +/** + Tells the delegate that the user wants to send a command. + + @param toolbarView the room input toolbar view. + @param commandText the command to send. + */ +- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendCommand:(NSString *)commandText; + /** Tells the delegate that the user wants to display the send media actions. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 274e7d437..7646a135e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5190,6 +5190,27 @@ static CGSize kThreadListBarButtonItemImageSize; }]; } +- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendCommand:(NSString *)commandText +{ + // Create before sending the message in case of a discussion (direct chat) + MXWeakify(self); + [self createDiscussionIfNeeded:^(BOOL readyToSend) { + MXStrongifyAndReturnIfNil(self); + + if (readyToSend) { + if (![self sendAsIRCStyleCommandIfPossible:commandText]) + { + // Display an error for unknown command + UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil + message:[VectorL10n roomCommandErrorUnknownCommand] + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alert animated:YES completion:nil]; + } + } + }]; +} + - (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)toolbarView { NSMutableArray *actionItems = [NSMutableArray new]; diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index c94111be4..727ca8f80 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -107,7 +107,7 @@ extension RoomViewController { "event_id": eventModified.eventId ]) }) - } else if !self.send(asIRCStyleCommandIfPossible: rawTextMsg) { + } else { roomDataSource.sendFormattedTextMessage(rawTextMsg, html: htmlMsg) { response in switch response { case .success: diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 9bc02c21e..ad488897f 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -338,7 +338,30 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } private func sendWysiwygMessage(content: WysiwygComposerContent) { - delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown) + if content.markdown.prefix(while: { $0 == "/" }).count == 1 { + let commandText: String + if content.markdown.hasPrefix(MXKSlashCommand.emote.cmd) { + // `/me` command works with markdown content + commandText = content.markdown + } else if #available(iOS 15.0, *) { + // Other commands should see pills replaced by matrix identifiers + commandText = PillsFormatter.stringByReplacingPills(in: self.wysiwygViewModel.textView.attributedText, mode: .identifier) + } else { + // Without Pills support, just use the raw text for command + commandText = self.wysiwygViewModel.textView.text + } + + // Fix potential command failures due to trailing characters + // or NBSP that are not properly handled by the command interpreter + let sanitizedCommand = commandText + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: String.nbsp, with: " ") + + delegate?.roomInputToolbarView?(self, sendCommand: sanitizedCommand) + } else { + delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown) + } + if isMaximised { minimise() } From c9a00433d0d37eb5af31d80014333ad167192ad1 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 25 Apr 2023 17:36:06 +0200 Subject: [PATCH 079/149] Fix: Remove the matrix id from the notice display name changed event --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ .../MatrixKit/Utils/EventFormatter/MXKEventFormatter.m | 2 +- changelog.d/7517.change | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7517.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 1aadc203f..679041963 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2984,6 +2984,7 @@ To enable access, tap Settings> Location and select Always"; "notice_avatar_url_changed" = "%@ changed their avatar"; "notice_display_name_set" = "%@ set their display name to %@"; "notice_display_name_changed_from" = "%@ changed their display name from %@ to %@"; +"notice_display_name_changed_to" = "%@ changed their display name to %@"; "notice_display_name_removed" = "%@ removed their display name"; "notice_topic_changed" = "%@ changed the topic to \"%@\"."; "notice_room_name_changed" = "%@ changed the room name to %@."; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index db052a786..90aff5d8f 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3899,6 +3899,10 @@ public class VectorL10n: NSObject { public static func noticeDisplayNameChangedFromByYou(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "notice_display_name_changed_from_by_you", p1, p2) } + /// %@ changed their display name to %@ + public static func noticeDisplayNameChangedTo(_ p1: String, _ p2: String) -> String { + return VectorL10n.tr("Vector", "notice_display_name_changed_to", p1, p2) + } /// %@ removed their display name public static func noticeDisplayNameRemoved(_ p1: String) -> String { return VectorL10n.tr("Vector", "notice_display_name_removed", p1) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index e2a9e4a01..19fae141d 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -571,7 +571,7 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* } else { - displayText = [VectorL10n noticeDisplayNameChangedFrom:event.sender :prevDisplayname :displayname]; + displayText = [VectorL10n noticeDisplayNameChangedTo:prevDisplayname :displayname]; } } } diff --git a/changelog.d/7517.change b/changelog.d/7517.change new file mode 100644 index 000000000..f43662947 --- /dev/null +++ b/changelog.d/7517.change @@ -0,0 +1 @@ +Timeline: Remove the matrix ID displayed when someone has changed its display name. From 782ad8465cfd0ee95ef122a4fc5a7f0697ddc3c5 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 25 Apr 2023 09:48:11 +0200 Subject: [PATCH 080/149] Fix: allow to render a TimelinePoll even if the poll is loading --- .../MatrixSDK/PollHistoryService.swift | 1 + .../Coordinator/TimelinePollCoordinator.swift | 15 +++++++++++--- .../TimelinePoll/TimelinePollModels.swift | 7 +++++++ .../TimelinePollScreenState.swift | 17 ++++++++++++++++ .../TimelinePoll/TimelinePollViewModel.swift | 6 +++++- .../TimelinePollViewModelProtocol.swift | 1 + .../TimelinePoll/View/TimelinePollView.swift | 20 ++++++++++++++++--- changelog.d/7497.bugfix | 1 + 8 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 changelog.d/7497.bugfix diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 7f6d8c5f6..79784c9d8 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -170,6 +170,7 @@ private extension PollHistoryService { do { newContext.pollAggregator = try PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) + newContext.pollAggregator?.reloadPollData() } catch { pollAggregationContexts.removeValue(forKey: eventId) } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 3214fae65..1cbfc148b 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -77,6 +77,8 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } } .store(in: &cancellables) + + pollAggregator.reloadPollData() } // MARK: - Public @@ -109,13 +111,20 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll)) + viewModel.updateWithPollState(.loaded) } - func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { } + func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { + viewModel.updateWithPollState(.loading) + } - func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { } + func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { + viewModel.updateWithPollState(.loaded) + } - func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } + func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { + viewModel.updateWithPollState(.invalidStartEvent) + } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 0ee87c55f..6439157dd 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -37,6 +37,12 @@ enum TimelinePollEventType { case ended } +enum TimelinePollState { + case loading + case loaded + case invalidStartEvent +} + struct TimelinePollAnswerOption: Identifiable { var id: String var text: String @@ -99,6 +105,7 @@ struct TimelinePollViewState: BindableState { } struct TimelinePollViewStateBindings { + var pollState: TimelinePollState var alertInfo: AlertInfo? } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index 8c70b21e3..aea09ade2 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -23,6 +23,9 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { case openUndisclosed case closedUndisclosed case closedPollEnded + case loading + case invalidStartEvent + case withAlert var screenType: Any.Type { TimelinePollDetails.self @@ -47,6 +50,20 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { let viewModel = TimelinePollViewModel(timelinePollDetails: poll) + switch self { + case .loading: + viewModel.updateWithPollState(.loading) + case .invalidStartEvent: + viewModel.updateWithPollState(.invalidStartEvent) + default: + viewModel.updateWithPollState(.loaded) + } + + if self == .withAlert { + viewModel.showAnsweringFailure() + } + + return ([viewModel], AnyView(TimelinePollView(viewModel: viewModel.context))) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index a86862cf4..da98d26a2 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -31,7 +31,7 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - Setup init(timelinePollDetails: TimelinePollDetails) { - super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings())) + super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings(pollState: .loading))) } // MARK: - Public @@ -58,6 +58,10 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro state.poll = pollDetails } + func updateWithPollState(_ pollState: TimelinePollState) { + state.bindings.pollState = pollState + } + func showAnsweringFailure() { state.bindings.alertInfo = AlertInfo(id: .failedSubmittingAnswer, title: VectorL10n.pollTimelineVoteNotRegisteredTitle, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift index 492f7f7a3..f4e0e5a20 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift @@ -21,6 +21,7 @@ protocol TimelinePollViewModelProtocol { var completion: ((TimelinePollViewModelResult) -> Void)? { get set } func updateWithPollDetails(_ pollDetails: TimelinePollDetails) + func updateWithPollState(_ pollState: TimelinePollState) func showAnsweringFailure() func showClosingFailure() } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index 2109a0e8a..fb1af9b2b 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -28,6 +28,23 @@ struct TimelinePollView: View { @ObservedObject var viewModel: TimelinePollViewModel.Context var body: some View { + Group { + switch viewModel.pollState { + case .loading: + TimelinePollMessageView(message: "loading...") + case .loaded: + pollContent + case .invalidStartEvent: + TimelinePollMessageView(message: VectorL10n.pollTimelineReplyEndedPoll) + } + } + .alert(item: $viewModel.alertInfo) { info in + info.alert + } + } + + @ViewBuilder + private var pollContent: some View { let poll = viewModel.viewState.poll VStack(alignment: .leading, spacing: 16.0) { @@ -61,9 +78,6 @@ struct TimelinePollView: View { } .padding([.horizontal, .top], 2.0) .padding([.bottom]) - .alert(item: $viewModel.alertInfo) { info in - info.alert - } } private var totalVotesString: String { diff --git a/changelog.d/7497.bugfix b/changelog.d/7497.bugfix new file mode 100644 index 000000000..a8558b843 --- /dev/null +++ b/changelog.d/7497.bugfix @@ -0,0 +1 @@ +Poll: The timeline sometimes displayed closed polls in the wrong order. From a08fc09d200f8740eb47c9598dbd4ce8d60b22a6 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 12:58:18 +0200 Subject: [PATCH 081/149] Disable accessibility for emojis during verification --- .../Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Riot/Modules/KeyVerification/Common/Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift b/Riot/Modules/KeyVerification/Common/Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift index e609f6b38..df8b3359e 100644 --- a/Riot/Modules/KeyVerification/Common/Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift +++ b/Riot/Modules/KeyVerification/Common/Verify/SAS/Views/VerifyEmojiCollectionViewCell.swift @@ -24,4 +24,9 @@ class VerifyEmojiCollectionViewCell: UICollectionViewCell, Reusable, Themable { func update(theme: Theme) { name.textColor = theme.textPrimaryColor } + + override func awakeFromNib() { + super.awakeFromNib() + emoji.isAccessibilityElement = false + } } From b1ca9f0781c5a4edc68514b705eca1adeefb8d1f Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 13:03:06 +0200 Subject: [PATCH 082/149] Add changelog.d file --- changelog.d/pr-7521.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7521.bugfix diff --git a/changelog.d/pr-7521.bugfix b/changelog.d/pr-7521.bugfix new file mode 100644 index 000000000..3cedf12d4 --- /dev/null +++ b/changelog.d/pr-7521.bugfix @@ -0,0 +1 @@ +Disable accessibility for emojis during session verification. \ No newline at end of file From bbda17fd70ec82f72e2eda86c646445c1d772a72 Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 15:36:29 +0200 Subject: [PATCH 083/149] Fix accessibility in SetPinCoordinatorBridgePresenter --- .../Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift b/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift index c381b76eb..6c4e69d14 100644 --- a/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift +++ b/Riot/Modules/SetPinCode/SetPinCoordinatorBridgePresenter.swift @@ -91,6 +91,10 @@ final class SetPinCoordinatorBridgePresenter: NSObject { } func presentWithMainAppWindow(_ window: UIWindow) { + // Prevents the VoiceOver reading accessible content when the PIN screen is on top + // Calling `makeKeyAndVisible` in `dismissWithMainAppWindow(_:)` restores the visibility state. + window.isHidden = true + let pinCoordinatorWindow = UIWindow(frame: window.bounds) let setPinCoordinator = SetPinCoordinator(session: self.session, viewMode: self.viewMode, pinCodePreferences: .shared) From 6f23d9ac62193070d80fad7cfeb361644daa881e Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 15:46:43 +0200 Subject: [PATCH 084/149] Remove accessibility from placeholder button --- .../EnterPinCodeViewController.storyboard | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.storyboard b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.storyboard index e3f675ca6..ab5ef6482 100644 --- a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.storyboard +++ b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.storyboard @@ -1,26 +1,27 @@ - + - + + - + - + @@ -44,20 +45,20 @@ - + @@ -97,7 +98,7 @@ - + @@ -106,7 +107,7 @@ - + @@ -124,12 +125,12 @@ - + - - - - - - - - - + @@ -312,6 +316,7 @@ + @@ -323,7 +328,6 @@ - @@ -350,5 +354,8 @@ + + + From f2773ea439c322aa34018236b308dd61b0574f2f Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Wed, 26 Apr 2023 15:55:19 +0200 Subject: [PATCH 085/149] Add changelog.d file --- changelog.d/pr-7522.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-7522.bugfix diff --git a/changelog.d/pr-7522.bugfix b/changelog.d/pr-7522.bugfix new file mode 100644 index 000000000..0bd4e5b53 --- /dev/null +++ b/changelog.d/pr-7522.bugfix @@ -0,0 +1 @@ +Fix accessibility when entering the PIN to unlock the app. From 4b4cdbd331aacf01bdd68c6cc268fd9a2288a67b Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Tue, 25 Apr 2023 11:03:51 +0100 Subject: [PATCH 086/149] Update triage for labelled issues Modernise actions from graphql to use new actions. Remove automation for Delight, WTF, FTUE, voice message and message bubble boards. --- .github/workflows/triage-move-labelled.yml | 248 ++------------------- 1 file changed, 21 insertions(+), 227 deletions(-) diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 291360fd2..130326b3d 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -53,23 +53,10 @@ jobs: contains(github.event.issue.labels.*.name, 'O-Frequent')) || contains(github.event.issue.labels.*.name, 'A11y')) steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc0sUA" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/18 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} add_product_issues_to_project: name: X-Needs-Product to Design project board @@ -77,138 +64,10 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'X-Needs-Product') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AAg6N" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - Delight_issues_to_board: - name: Spaces issues to Delight project board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Team: Delight') || - contains(github.event.issue.labels.*.name, 'Z-AppLayout') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc1HvQ" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - move_voice-message_issues: - name: A-Voice Messages to voice message board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'A-Voice Messages') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc2KCw" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - move_message_bubble_issues: - name: A-Message-Bubbles to Message bubble board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc3m-g" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - move_FTUE_issues: - name: Z-FTUE to FTUE board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Z-FTUE') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AAqVx" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - - move_WTF_issues: - name: Z-WTF to WTF board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Z-WTF') - steps: - - uses: octokit/graphql-action@v2.x - with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AArk0" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/28 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ex_plorers: name: Add labelled issues to X-Plorer project @@ -216,23 +75,10 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Element X Feature') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4ALoFY" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/73 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ps_features1: name: Add labelled issues to PS features team 1 @@ -245,23 +91,10 @@ jobs: (contains(github.event.issue.labels.*.name, 'A-Session-Mgmt') && contains(github.event.issue.labels.*.name, 'A-User-Settings')) steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AHJKF" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/56 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ps_features2: name: Add labelled issues to PS features team 2 @@ -270,23 +103,10 @@ jobs: contains(github.event.issue.labels.*.name, 'A-DM-Start') || contains(github.event.issue.labels.*.name, 'A-Broadcast') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AHJKd" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/58 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ps_features3: name: Add labelled issues to PS features team 3 @@ -294,23 +114,10 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4AHJKW" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/57 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} voip: name: Add labelled issues to VoIP project board @@ -318,20 +125,7 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: VoIP') steps: - - uses: octokit/graphql-action@v2.x - id: add_to_project + - uses: actions/add-to-project@main with: - headers: '{"GraphQL-Features": "projects_next_graphql"}' - query: | - mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { - item { - id - } - } - } - projectid: ${{ env.PROJECT_ID }} - contentid: ${{ github.event.issue.node_id }} - env: - PROJECT_ID: "PVT_kwDOAM0swc4ABMIk" - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + project-url: https://github.com/orgs/vector-im/projects/41 + github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} From b563043b1979f2f1b7adbfd29c741d096987e980 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 26 Apr 2023 15:31:07 +0200 Subject: [PATCH 087/149] Fix: TimelinePoll code refactoring --- Riot/Assets/en.lproj/Vector.strings | 2 + Riot/Generated/Strings.swift | 4 + .../MockPollHistoryDetailScreenState.swift | 2 +- .../MatrixSDK/PollHistoryService.swift | 10 +- .../Coordinator/TimelinePollCoordinator.swift | 29 +++-- .../Unit/TimelinePollViewModelTests.swift | 115 +++++++++++------- .../TimelinePoll/TimelinePollModels.swift | 9 +- .../TimelinePollScreenState.swift | 9 +- .../TimelinePoll/TimelinePollViewModel.swift | 51 ++++---- .../TimelinePollViewModelProtocol.swift | 3 +- .../TimelinePoll/View/TimelinePollView.swift | 26 ++-- 11 files changed, 143 insertions(+), 117 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 1aadc203f..41b2b4a54 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2405,6 +2405,8 @@ Tap the + to start adding people."; "poll_timeline_reply_ended_poll" = "Ended poll"; +"poll_timeline_loading" = "Loading..."; + // MARK: - Location sharing "location_sharing_title" = "Location"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index db052a786..cd5a22877 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4923,6 +4923,10 @@ public class VectorL10n: NSObject { public static var pollTimelineEndedText: String { return VectorL10n.tr("Vector", "poll_timeline_ended_text") } + /// Loading... + public static var pollTimelineLoading: String { + return VectorL10n.tr("Vector", "poll_timeline_loading") + } /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index 09a8fb3c7..9c57bdabb 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -48,7 +48,7 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let timelineViewModel = TimelinePollViewModel(timelinePollDetails: poll) + let timelineViewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(poll)) let viewModel = PollHistoryDetailViewModel(poll: poll) return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context, contentPoll: TimelinePollView(viewModel: timelineViewModel.context)))) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 79784c9d8..9425cc2d4 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -170,7 +170,6 @@ private extension PollHistoryService { do { newContext.pollAggregator = try PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self) - newContext.pollAggregator?.reloadPollData() } catch { pollAggregationContexts.removeValue(forKey: eventId) } @@ -210,13 +209,14 @@ extension PollHistoryService: PollAggregatorDelegate { func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - guard let context = pollAggregationContexts[aggregator.poll.id], context.published == false else { + + guard let poll = aggregator.poll, let context = pollAggregationContexts[poll.id], context.published == false else { return } context.published = true - let newPoll: TimelinePollDetails = .init(poll: aggregator.poll, represent: .started) + let newPoll: TimelinePollDetails = .init(poll: poll, represent: .started) if context.isLivePoll { livePollsSubject.send(newPoll) @@ -226,9 +226,9 @@ extension PollHistoryService: PollAggregatorDelegate { } func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - guard let context = pollAggregationContexts[aggregator.poll.id], context.published else { + guard let poll = aggregator.poll, let context = pollAggregationContexts[poll.id], context.published else { return } - updatesSubject.send(.init(poll: aggregator.poll, represent: .started)) + updatesSubject.send(.init(poll: poll, represent: .started)) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 1cbfc148b..e2202524b 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -32,7 +32,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel private let parameters: TimelinePollCoordinatorParameters private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() - private var pollAggregator: PollAggregator + private var pollAggregator: PollAggregator! private(set) var viewModel: TimelinePollViewModelProtocol! private var cancellables = Set() @@ -46,10 +46,9 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel init(parameters: TimelinePollCoordinatorParameters) throws { self.parameters = parameters - try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent) - pollAggregator.delegate = self + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loading) + try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent, delegate: self) - viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll)) viewModel.completion = { [weak self] result in guard let self = self else { return } @@ -77,8 +76,6 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } } .store(in: &cancellables) - - pollAggregator.reloadPollData() } // MARK: - Public @@ -94,11 +91,11 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } func canEndPoll() -> Bool { - pollAggregator.poll.isClosed == false + pollAggregator.poll?.isClosed == false } func canEditPoll() -> Bool { - pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0 + pollAggregator.poll?.isClosed == false && pollAggregator.poll?.totalAnswerCount == 0 } func endPoll() { @@ -110,20 +107,22 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: - PollAggregatorDelegate func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll)) - viewModel.updateWithPollState(.loaded) + if let poll = aggregator.poll { + viewModel.updateWithPollDetailsState(.loaded(buildTimelinePollFrom(poll))) + } } - func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { - viewModel.updateWithPollState(.loading) - } + func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { } func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - viewModel.updateWithPollState(.loaded) + guard let poll = aggregator.poll else { + return + } + viewModel.updateWithPollDetailsState(.loaded(buildTimelinePollFrom(poll))) } func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { - viewModel.updateWithPollState(.invalidStartEvent) + viewModel.updateWithPollDetailsState(.errored) } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index a36a7d092..2fd2b032f 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -41,111 +41,134 @@ class TimelinePollViewModelTests: XCTestCase { hasBeenEdited: false, hasDecryptionError: false) - viewModel = TimelinePollViewModel(timelinePollDetails: timelinePoll) + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(timelinePoll)) context = viewModel.context } func testInitialState() { - XCTAssertEqual(context.viewState.poll.answerOptions.count, 3) - XCTAssertFalse(context.viewState.poll.closed) - XCTAssertEqual(context.viewState.poll.type, .disclosed) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions.count, 3) + XCTAssertEqual(context.viewState.pollState.poll?.closed, false) + XCTAssertEqual(context.viewState.pollState.poll?.type, .disclosed) } func testSingleSelectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testSingleReselectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testMultipleSelectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) } func testMultipleReselectionOnMax1Allowed() { context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) } func testClosedSelection() { - viewModel.state.poll.closed = true + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.closed = true + viewModel.updateWithPollDetailsState(.loaded(poll)) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testSingleSelectionOnMax2Allowed() { - viewModel.state.poll.maxAllowedSelections = 2 + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.maxAllowedSelections = 2 + viewModel.updateWithPollDetailsState(.loaded(poll)) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testSingleReselectionOnMax2Allowed() { - viewModel.state.poll.maxAllowedSelections = 2 + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.maxAllowedSelections = 2 + viewModel.updateWithPollDetailsState(.loaded(poll)) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) } func testMultipleSelectionOnMax2Allowed() { - viewModel.state.poll.maxAllowedSelections = 2 - + guard case var .loaded(poll) = context.viewState.pollState else { + return XCTFail() + } + poll.maxAllowedSelections = 2 + viewModel.updateWithPollDetailsState(.loaded(poll)) + context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) context.send(viewAction: .selectAnswerOptionWithIdentifier("2")) - XCTAssertTrue(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) context.send(viewAction: .selectAnswerOptionWithIdentifier("2")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[1].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, true) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) - XCTAssertFalse(context.viewState.poll.answerOptions[0].selected) - XCTAssertTrue(context.viewState.poll.answerOptions[1].selected) - XCTAssertFalse(context.viewState.poll.answerOptions[2].selected) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[0].selected, false) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[1].selected, true) + XCTAssertEqual(context.viewState.pollState.poll?.answerOptions[2].selected, false) + } +} + +private extension TimelinePollDetailsState { + var poll: TimelinePollDetails? { + switch self { + case .loaded(let poll): + return poll + default: + return nil + } } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 6439157dd..6b2d52c78 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -37,10 +37,10 @@ enum TimelinePollEventType { case ended } -enum TimelinePollState { +enum TimelinePollDetailsState { case loading - case loaded - case invalidStartEvent + case loaded(TimelinePollDetails) + case errored } struct TimelinePollAnswerOption: Identifiable { @@ -100,12 +100,11 @@ struct TimelinePollDetails { extension TimelinePollDetails: Identifiable { } struct TimelinePollViewState: BindableState { - var poll: TimelinePollDetails + var pollState: TimelinePollDetailsState var bindings: TimelinePollViewStateBindings } struct TimelinePollViewStateBindings { - var pollState: TimelinePollState var alertInfo: AlertInfo? } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index aea09ade2..c81d78683 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -48,22 +48,21 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { hasBeenEdited: false, hasDecryptionError: false) - let viewModel = TimelinePollViewModel(timelinePollDetails: poll) + let viewModel: TimelinePollViewModel switch self { case .loading: - viewModel.updateWithPollState(.loading) + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loading) case .invalidStartEvent: - viewModel.updateWithPollState(.invalidStartEvent) + viewModel = TimelinePollViewModel(timelinePollDetailsState: .errored) default: - viewModel.updateWithPollState(.loaded) + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loaded(poll)) } if self == .withAlert { viewModel.showAnsweringFailure() } - return ([viewModel], AnyView(TimelinePollView(viewModel: viewModel.context))) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index da98d26a2..26ac65a68 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -30,8 +30,8 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - Setup - init(timelinePollDetails: TimelinePollDetails) { - super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings(pollState: .loading))) + init(timelinePollDetailsState: TimelinePollDetailsState) { + super.init(initialViewState: TimelinePollViewState(pollState: timelinePollDetailsState, bindings: TimelinePollViewStateBindings())) } // MARK: - Public @@ -40,11 +40,11 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro switch viewAction { // Update local state. An update will be pushed from the coordinator once sent. case .selectAnswerOptionWithIdentifier(let identifier): - guard !state.poll.closed else { + // only if the poll is ready and not closed + guard case let .loaded(poll) = state.pollState, !poll.closed else { return } - - if state.poll.maxAllowedSelections == 1 { + if poll.maxAllowedSelections == 1 { updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion) } else { updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) @@ -54,12 +54,8 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - TimelinePollViewModelProtocol - func updateWithPollDetails(_ pollDetails: TimelinePollDetails) { - state.poll = pollDetails - } - - func updateWithPollState(_ pollState: TimelinePollState) { - state.bindings.pollState = pollState + func updateWithPollDetailsState(_ pollDetailsState: TimelinePollDetailsState) { + state.pollState = pollDetailsState } func showAnsweringFailure() { @@ -77,33 +73,40 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro // MARK: - Private func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { - state.poll.answerOptions.updateEach { answerOption in + guard case var .loaded(poll) = state.pollState else { return } + + var pollAnswerOptions = poll.answerOptions + pollAnswerOptions.updateEach { answerOption in if answerOption.selected { answerOption.selected = false answerOption.count = UInt(max(0, Int(answerOption.count) - 1)) - state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1)) + poll.totalAnswerCount = UInt(max(0, Int(poll.totalAnswerCount) - 1)) } if answerOption.id == selectedAnswerIdentifier { answerOption.selected = true answerOption.count += 1 - state.poll.totalAnswerCount += 1 + poll.totalAnswerCount += 1 } } - + poll.answerOptions = pollAnswerOptions + state.pollState = .loaded(poll) informCoordinatorOfSelectionUpdate(state: state, callback: callback) } func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { - let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true } + guard case .loaded(var poll) = state.pollState else { return } + + let selectedAnswerOptions = poll.answerOptions.filter { $0.selected == true } let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0 - if !isDeselecting, selectedAnswerOptions.count >= state.poll.maxAllowedSelections { + if !isDeselecting, selectedAnswerOptions.count >= poll.maxAllowedSelections { return } - state.poll.answerOptions.updateEach { answerOption in + var pollAnswerOptions = poll.answerOptions + pollAnswerOptions.updateEach { answerOption in if answerOption.id != selectedAnswerIdentifier { return } @@ -111,22 +114,24 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro if answerOption.selected { answerOption.selected = false answerOption.count = UInt(max(0, Int(answerOption.count) - 1)) - state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1)) + poll.totalAnswerCount = UInt(max(0, Int(poll.totalAnswerCount) - 1)) } else { answerOption.selected = true answerOption.count += 1 - state.poll.totalAnswerCount += 1 + poll.totalAnswerCount += 1 } } - + poll.answerOptions = pollAnswerOptions + state.pollState = .loaded(poll) informCoordinatorOfSelectionUpdate(state: state, callback: callback) } func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) { - let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in + guard case .loaded(let poll) = state.pollState else { return } + + let selectedIdentifiers = poll.answerOptions.compactMap { answerOption in answerOption.selected ? answerOption.id : nil } - callback?(.selectedAnswerOptionsWithIdentifiers(selectedIdentifiers)) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift index f4e0e5a20..ade681438 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift @@ -20,8 +20,7 @@ protocol TimelinePollViewModelProtocol { var context: TimelinePollViewModelType.Context { get } var completion: ((TimelinePollViewModelResult) -> Void)? { get set } - func updateWithPollDetails(_ pollDetails: TimelinePollDetails) - func updateWithPollState(_ pollState: TimelinePollState) + func updateWithPollDetailsState(_ pollDetailsState: TimelinePollDetailsState) func showAnsweringFailure() func showClosingFailure() } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index fb1af9b2b..52533288c 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -29,12 +29,12 @@ struct TimelinePollView: View { var body: some View { Group { - switch viewModel.pollState { + switch viewModel.viewState.pollState { case .loading: - TimelinePollMessageView(message: "loading...") - case .loaded: - pollContent - case .invalidStartEvent: + TimelinePollMessageView(message: VectorL10n.pollTimelineLoading) + case .loaded(let poll): + pollContent(poll) + case .errored: TimelinePollMessageView(message: VectorL10n.pollTimelineReplyEndedPoll) } } @@ -44,9 +44,7 @@ struct TimelinePollView: View { } @ViewBuilder - private var pollContent: some View { - let poll = viewModel.viewState.poll - + private func pollContent(_ poll: TimelinePollDetails) -> some View { VStack(alignment: .leading, spacing: 16.0) { if poll.representsPollEndedEvent { Text(VectorL10n.pollTimelineEndedText) @@ -57,7 +55,7 @@ struct TimelinePollView: View { Text(poll.question) .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) + - Text(editedText) + Text(editedText(poll)) .font(theme.fonts.footnote) .foregroundColor(theme.colors.secondaryContent) @@ -71,7 +69,7 @@ struct TimelinePollView: View { .disabled(poll.closed) .fixedSize(horizontal: false, vertical: true) - Text(totalVotesString) + Text(totalVotesString(poll)) .lineLimit(2) .font(theme.fonts.footnote) .foregroundColor(theme.colors.tertiaryContent) @@ -80,9 +78,7 @@ struct TimelinePollView: View { .padding([.bottom]) } - private var totalVotesString: String { - let poll = viewModel.viewState.poll - + private func totalVotesString(_ poll: TimelinePollDetails) -> String { if poll.hasDecryptionError, poll.totalAnswerCount > 0 { return VectorL10n.pollTimelineDecryptionError } @@ -109,8 +105,8 @@ struct TimelinePollView: View { } } - private var editedText: String { - viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" + private func editedText(_ poll: TimelinePollDetails) -> String { + poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" } } From 3bd413f579bf776f2adb28bf8fe6f7601052f6d5 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 27 Apr 2023 14:55:40 +0200 Subject: [PATCH 088/149] Update RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift Co-authored-by: Alfonso Grillo --- .../Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift index 9425cc2d4..c4471844e 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift @@ -209,7 +209,6 @@ extension PollHistoryService: PollAggregatorDelegate { func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - guard let poll = aggregator.poll, let context = pollAggregationContexts[poll.id], context.published == false else { return } From ecc9f50de011442a15cfd544158e9f1e0d7ece70 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 28 Apr 2023 15:47:51 +0200 Subject: [PATCH 089/149] Fix a flickering issue when the timeline datasource is reloaded. --- .../MatrixKit/Models/Room/MXKRoomDataSource.m | 19 +++++++------------ changelog.d/7523.bugfix | 1 + 2 files changed, 8 insertions(+), 12 deletions(-) create mode 100644 changelog.d/7523.bugfix diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index a69f504cc..033aa9361 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -456,11 +456,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } - (void)reset -{ - [self resetNotifying:YES]; -} - -- (void)resetNotifying:(BOOL)notify { if (roomDidFlushDataNotificationObserver) { @@ -556,12 +551,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } _serverSyncEventCount = 0; - - // Notify the delegate to reload its tableview - if (notify && self.delegate) - { - [self.delegate dataSource:self didCellChange:nil]; - } } - (void)reload @@ -575,10 +564,16 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self setState:MXKDataSourceStatePreparing]; - [self resetNotifying:notify]; + [self reset]; // Reload [self didMXSessionStateChange]; + + // Notify the delegate to refresh the tableview + if (notify && self.delegate) + { + [self.delegate dataSource:self didCellChange:nil]; + } } - (void)destroy diff --git a/changelog.d/7523.bugfix b/changelog.d/7523.bugfix new file mode 100644 index 000000000..bc5cf31a7 --- /dev/null +++ b/changelog.d/7523.bugfix @@ -0,0 +1 @@ +Fix a flickering issue when the timeline datasource is reloaded. From e1ecdd65ced71e78d6a0423c09a0124b3392e9eb Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 2 May 2023 11:11:51 +0200 Subject: [PATCH 090/149] Fix application crashing when opening a thread with RTE enabled --- Riot/Modules/Room/RoomViewController.m | 6 ++++++ changelog.d/7530.bugfix | 1 + 2 files changed, 7 insertions(+) create mode 100644 changelog.d/7530.bugfix diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 7646a135e..799f1f718 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1194,6 +1194,12 @@ static CGSize kThreadListBarButtonItemImageSize; - (void)updateRoomInputToolbarViewClassIfNeeded { Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass]; + + // If RTE is enabled, delay the toolbar setup until `completionSuggestionCoordinator` is ready. + if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && _completionSuggestionCoordinator == nil) + { + return; + } BOOL shouldDismissContextualMenu = NO; diff --git a/changelog.d/7530.bugfix b/changelog.d/7530.bugfix new file mode 100644 index 000000000..5733a8d81 --- /dev/null +++ b/changelog.d/7530.bugfix @@ -0,0 +1 @@ +Fix application crashing when opening a thread with RTE enabled From aea756c9c295485f3f6ccd70ffe8080f173ff3b4 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 2 May 2023 11:44:23 +0200 Subject: [PATCH 091/149] Update room input toolbar when `CompletionSuggestionCoordinator` is initialised --- Riot/Modules/Room/RoomViewController.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 799f1f718..a18de9aa1 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1094,6 +1094,8 @@ static CGSize kThreadListBarButtonItemImageSize; _completionSuggestionCoordinator.delegate = self; [self setupCompletionSuggestionViewIfNeeded]; + + [self updateRoomInputToolbarViewClassIfNeeded]; [self updateTopBanners]; } From 48bb7121395a53a98654e0bdd6bf0ca51bb00ce9 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 3 Mar 2023 10:02:57 +0100 Subject: [PATCH 092/149] Secrets recovery: fix an issue preventing the release of SecureBackupSetupCoordinator --- .../Secrets/Recover/SecretsRecoveryCoordinator.swift | 7 ++++--- .../Setup/SecureBackupSetupCoordinator.swift | 10 +++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift index d36ee995e..817414956 100644 --- a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift +++ b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift @@ -121,11 +121,12 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType { private func showSecureBackupSetup(checkKeyBackup: Bool) { let coordinator = SecureBackupSetupCoordinator(session: self.session, checkKeyBackup: checkKeyBackup, navigationRouter: self.navigationRouter, cancellable: self.cancellable) coordinator.delegate = self - coordinator.start() - - self.navigationRouter.push(coordinator.toPresentable(), animated: true, popCompletion: { [weak self] in + // Fix: calling coordinator.start() will update the navigationRouter without a popCompletion + coordinator.start(popCompletion: { [weak self] in self?.remove(childCoordinator: coordinator) }) + // Fix: do not push the presentable from the coordinator to the navigation router as this has already been done by coordinator.start(). + // Also, coordinator.toPresentable() returns a navigation controller, which cannot be pushed into a navigation router. self.add(childCoordinator: coordinator) } } diff --git a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift index 53a03e359..0cb633945 100644 --- a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift +++ b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift @@ -73,15 +73,19 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType { // MARK: - Public methods func start() { + start(popCompletion: nil) + } + + func start(popCompletion: (() -> Void)?) { let rootViewController = self.createIntro() if self.navigationRouter.modules.isEmpty == false { - self.navigationRouter.push(rootViewController, animated: true, popCompletion: nil) + self.navigationRouter.push(rootViewController, animated: true, popCompletion: popCompletion) } else { - self.navigationRouter.setRootModule(rootViewController) + self.navigationRouter.setRootModule(rootViewController, popCompletion: popCompletion) } } - + func toPresentable() -> UIViewController { return self.navigationRouter .toPresentable() From 7b716e4c010bc63fda65815b287eac899b96d339 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 3 Apr 2023 13:50:35 +0200 Subject: [PATCH 093/149] =?UTF-8?q?Fix:=20don=E2=80=99t=20allow=20to=20res?= =?UTF-8?q?et=20secrets=20if=20it=20is=20already=20in=20progress.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift | 2 +- Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift | 1 + .../Secrets/Reset/SecretsResetViewController.swift | 6 ++++++ Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift | 9 ++++++++- Riot/Modules/Secrets/Reset/SecretsResetViewState.swift | 1 + changelog.d/pr-7404.bugfix | 1 + 6 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pr-7404.bugfix diff --git a/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift b/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift index 6c72ebe5d..bd9740ad5 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetCoordinator.swift @@ -94,11 +94,11 @@ extension SecretsResetCoordinator: SecretsResetViewModelCoordinatorDelegate { extension SecretsResetCoordinator: ReauthenticationCoordinatorDelegate { func reauthenticationCoordinatorDidComplete(_ coordinator: ReauthenticationCoordinatorType, withAuthenticationParameters authenticationParameters: [String: Any]?) { - self.secretsResetViewModel.process(viewAction: .authenticationInfoEntered(authenticationParameters ?? [:])) } func reauthenticationCoordinatorDidCancel(_ coordinator: ReauthenticationCoordinatorType) { + self.secretsResetViewModel.process(viewAction: .authenticationCancelled) self.remove(childCoordinator: coordinator) } diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift index aa135b5fe..5b960342a 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewAction.swift @@ -22,6 +22,7 @@ import Foundation enum SecretsResetViewAction { case loadData case reset + case authenticationCancelled case authenticationInfoEntered(_ authInfo: [String: Any]) case cancel } diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift index 6008a1fb0..fccbfb6e3 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewController.swift @@ -132,6 +132,8 @@ final class SecretsResetViewController: UIViewController { self.renderLoading() case .resetDone: self.renderLoaded() + case .resetCancelled: + self.renderCancelled() case .error(let error): self.render(error: error) } @@ -145,6 +147,10 @@ final class SecretsResetViewController: UIViewController { self.activityPresenter.removeCurrentActivityIndicator(animated: true) } + private func renderCancelled() { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + } + private func render(error: Error) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift index 2e8e7604c..62b0c686f 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift @@ -49,6 +49,8 @@ final class SecretsResetViewModel: SecretsResetViewModelType { break case .reset: self.askAuthentication() + case .authenticationCancelled: + self.authenticationCancelled() case .authenticationInfoEntered(let authParameters): self.resetSecrets(with: authParameters) case .cancel: @@ -68,7 +70,6 @@ final class SecretsResetViewModel: SecretsResetViewModelType { } MXLog.debug("[SecretsResetViewModel] resetSecrets") - self.update(viewState: .resetting) crossSigning.setup(withAuthParams: authParameters, success: { [weak self] in guard let self = self else { return @@ -96,7 +97,13 @@ final class SecretsResetViewModel: SecretsResetViewModelType { } private func askAuthentication() { + self.update(viewState: .resetting) + let setupCrossSigningRequest = self.crossSigningService.setupCrossSigningRequest() self.coordinatorDelegate?.secretsResetViewModel(self, needsToAuthenticateWith: setupCrossSigningRequest) } + + private func authenticationCancelled() { + self.update(viewState: .resetCancelled) + } } diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift index b7cb0acb8..128f90b19 100644 --- a/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift +++ b/Riot/Modules/Secrets/Reset/SecretsResetViewState.swift @@ -22,5 +22,6 @@ import Foundation enum SecretsResetViewState { case resetting case resetDone + case resetCancelled case error(Error) } diff --git a/changelog.d/pr-7404.bugfix b/changelog.d/pr-7404.bugfix new file mode 100644 index 000000000..58609a160 --- /dev/null +++ b/changelog.d/pr-7404.bugfix @@ -0,0 +1 @@ +Fix an issue where the Secrets Reset screen would open twice. From 2de038225382205ad264a8c69c499cf04ad605be Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 2 May 2023 15:26:32 +0200 Subject: [PATCH 094/149] Fix the frame of the marker view highlighting an event --- .../MXKRoomBubbleTableViewCell+Riot.m | 48 +++++-------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index 907dd5ff2..9164a61d7 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -256,40 +256,18 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = if (componentIndex < bubbleComponents.count) { - MXKRoomBubbleComponent *component = bubbleComponents[componentIndex]; - - // Define the marker frame - CGFloat markPosY = component.position.y + self.msgTextViewTopConstraint.constant; - - NSInteger mostRecentComponentIndex = bubbleComponents.count - 1; - if ([bubbleData isKindOfClass:RoomBubbleCellData.class]) + CGRect componentFrame = [self componentFrameInContentViewForIndex:componentIndex]; + if (CGRectIsEmpty(componentFrame)) { - mostRecentComponentIndex = ((RoomBubbleCellData*)bubbleData).mostRecentComponentIndex; - } - - // Compute the mark height. - // Use the rest of the cell height by default. - CGFloat markHeight = self.contentView.frame.size.height - markPosY; - if (componentIndex != mostRecentComponentIndex) - { - // There is another component (with display) after this component in the cell. - // Stop the marker height to the top of this component. - for (NSInteger index = componentIndex + 1; index < bubbleComponents.count; index ++) - { - MXKRoomBubbleComponent *nextComponent = bubbleComponents[index]; - - if (nextComponent.attributedTextMessage) - { - markHeight = nextComponent.position.y - component.position.y; - break; - } - } + return; } - UIView *markerView = [[UIView alloc] initWithFrame:CGRectMake(VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X, - markPosY, - VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH, - markHeight)]; + CGRect markerFrame = CGRectMake(VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X, + CGRectGetMinY(componentFrame), + VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH, + CGRectGetHeight(componentFrame)); + + UIView *markerView = [[UIView alloc] initWithFrame:markerFrame]; markerView.backgroundColor = ThemeService.shared.theme.tintColor; [markerView setTranslatesAutoresizingMaskIntoConstraints:NO]; @@ -303,28 +281,28 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = toItem:self.contentView attribute:NSLayoutAttributeLeading multiplier:1.0 - constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X]; + constant:CGRectGetMinX(markerFrame)]; NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:markerView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTop multiplier:1.0 - constant:markPosY]; + constant:CGRectGetMinY(markerFrame)]; NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:markerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 - constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH]; + constant:CGRectGetWidth(markerFrame)]; NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:markerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 - constant:markHeight]; + constant:CGRectGetHeight(markerFrame)]; // Available on iOS 8 and later [NSLayoutConstraint activateConstraints:@[leftConstraint, topConstraint, widthConstraint, heightConstraint]]; From 54fcc55d846b8db28bb7aa2d8b5fae600cada967 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 3 May 2023 11:43:02 +0200 Subject: [PATCH 095/149] Fix: highlighting an event removes the highlighting of the previous event. --- Riot/Modules/Room/RoomViewController.m | 36 +++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index a18de9aa1..592495dda 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -7504,23 +7504,47 @@ static CGSize kThreadListBarButtonItemImageSize; return; } + NSMutableArray *rowsToReload = [[NSMutableArray alloc] init]; + // Get the current hightlighted event because we will need to reload it + NSString *currentHiglightedEventId = self.customizedRoomDataSource.highlightedEventId; + if (currentHiglightedEventId) + { + NSInteger currentHiglightedRow = [self.roomDataSource indexOfCellDataWithEventId:currentHiglightedEventId]; + if (currentHiglightedRow != NSNotFound) + { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:currentHiglightedRow inSection:0]; + if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) + { + [rowsToReload addObject:indexPath]; + } + } + } + self.customizedRoomDataSource.highlightedEventId = eventId; + // Add the new highligted event to the list of rows to reload NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; - if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) + BOOL indexPathIsVisible = [[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]; + if (indexPathIsVisible) { - [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] + [rowsToReload addObject:indexPath]; + } + + // Reload rows + if (rowsToReload.count > 0) + { + [self.bubblesTableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationNone]; - [self.bubblesTableView scrollToRowAtIndexPath:indexPath - atScrollPosition:UITableViewScrollPositionMiddle - animated:YES]; } - else if ([self.bubblesTableView vc_hasIndexPath:indexPath]) + + // Scroll to the newly highlighted row + if (indexPathIsVisible || [self.bubblesTableView vc_hasIndexPath:indexPath]) { [self.bubblesTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; } + if (completion) { completion(); From e4e1d70aeb1997c54997e8d3891b31c563c12a8e Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 3 May 2023 11:49:54 +0200 Subject: [PATCH 096/149] Add changelog file --- changelog.d/7526.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7526.bugfix diff --git a/changelog.d/7526.bugfix b/changelog.d/7526.bugfix new file mode 100644 index 000000000..7adb60cc0 --- /dev/null +++ b/changelog.d/7526.bugfix @@ -0,0 +1 @@ +Fix the position of the marker highlighting an event. From 2b88e5ce68314c25bb3631f6867fa1be34b057f1 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 20 Apr 2023 14:31:26 +0200 Subject: [PATCH 097/149] VoiceBroadcast: Play a sound to notify the user when VB is pause due to an error. --- Riot/Assets/Sounds/vberror.mp3 | Bin 0 -> 9497 bytes .../MatrixKitAssets.bundle/Sounds/vberror.mp3 | Bin 0 -> 9497 bytes .../MatrixKit/MatrixKit-Bridging-Header.h | 1 + .../VoiceBroadcastRecorderService.swift | 35 +++++++++++++++++- changelog.d/7504.change | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 Riot/Assets/Sounds/vberror.mp3 create mode 100644 Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/Sounds/vberror.mp3 create mode 100644 changelog.d/7504.change diff --git a/Riot/Assets/Sounds/vberror.mp3 b/Riot/Assets/Sounds/vberror.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..14c710595b54f5a340c2e67d1b4f8938f9e7b560 GIT binary patch literal 9497 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 URL? { + if let path = Bundle.main.path(forResource: soundName, ofType: "mp3") { + return URL(fileURLWithPath: path) + } else { + return Bundle.mxk_audioURLFromMXKAssetsBundle(withName: soundName) + } + } } diff --git a/changelog.d/7504.change b/changelog.d/7504.change new file mode 100644 index 000000000..2fed9c438 --- /dev/null +++ b/changelog.d/7504.change @@ -0,0 +1 @@ +Add an audio alert when the voice broadcast recording is automatically paused From 2eb07ef5e09ca3379dfe988c39337343be1730fa Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 3 May 2023 14:57:36 +0200 Subject: [PATCH 098/149] Fix partial text messages not being saved for each room with RTE enabled --- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../MXKRoomInputToolbarView.h | 14 ++++++ .../MXKRoomInputToolbarView.m | 5 ++ Riot/Modules/Room/MXKRoomViewController.m | 2 +- Riot/Modules/Room/RoomViewController.m | 9 +++- .../WysiwygInputToolbarView.swift | 49 ++++++++++++++++--- .../MockComposerLinkActionScreenState.swift | 2 +- .../ComposerLinkActionViewModel.swift | 2 +- changelog.d/7535.bugfix | 1 + project.yml | 2 +- 10 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 changelog.d/7535.bugfix diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9870eb6da..bf7d208dd 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "758f226a92d6726ab626c1e78ecd183bdba77016", - "version" : "2.0.0" + "revision" : "ff5e8054da60212051cb0dec244500ca0f441bac", + "version" : "2.1.0" } }, { diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h index abd67ec7e..b18e93690 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h @@ -213,6 +213,15 @@ typedef enum : NSUInteger */ - (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView updateActivityIndicator:(BOOL)isAnimating; +/** + Tells the delegate that the partial content of the composer has changed + and should be stored to allow restoring it later if needed. + + @param toolbarView the room input toolbar view + @param partialAttributedTextMessage the partial content to store + */ +- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView shouldStorePartialContent:(NSAttributedString*)partialAttributedTextMessage; + @end /** @@ -390,6 +399,11 @@ typedef enum : NSUInteger */ @property (nonatomic) NSAttributedString *attributedTextMessage; +/** + Sets the partial text message to apply to the current message composer. + */ +- (void)setPartialContent:(NSAttributedString *)attributedTextMessage; + /** Default font for the message composer. */ diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m index d05cd9f53..b5b15d4b8 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m @@ -1405,4 +1405,9 @@ NSString* MXKFileSizes_description(MXKFileSizes sizes) return NO; } +- (void)setPartialContent:(NSAttributedString *)attributedTextMessage +{ + self.attributedTextMessage = attributedTextMessage; +} + @end diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index 2e55c4771..b1a1bc18b 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -360,7 +360,7 @@ static const CGFloat kCellVisibilityMinimumHeight = 8.0; { // Retrieve the potential message partially typed during last room display. // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early) - inputToolbarView.attributedTextMessage = roomDataSource.partialAttributedTextMessage; + [inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage]; } if (!hasAppearedOnce) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index a18de9aa1..cb3214684 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -693,7 +693,7 @@ static CGSize kThreadListBarButtonItemImageSize; { // Retrieve the potential message partially typed during last room display. // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early) - self.inputToolbarView.attributedTextMessage = self.roomDataSource.partialAttributedTextMessage; + [self.inputToolbarView setPartialContent:self.roomDataSource.partialAttributedTextMessage]; } [self setMaximisedToolbarIsHiddenIfNeeded: NO]; @@ -5293,6 +5293,11 @@ static CGSize kThreadListBarButtonItemImageSize; }]; } +- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView shouldStorePartialContent:(NSAttributedString *)partialAttributedTextMessage +{ + self.roomDataSource.partialAttributedTextMessage = partialAttributedTextMessage; +} + #pragma mark - MXKRoomMemberDetailsViewControllerDelegate - (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString *)matrixId completion:(void (^)(void))completion @@ -6135,7 +6140,7 @@ static CGSize kThreadListBarButtonItemImageSize; if (self.saveProgressTextInput) { // Restore the potential message partially typed before jump to last unread messages. - self.inputToolbarView.attributedTextMessage = roomDataSource.partialAttributedTextMessage; + [self.inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage]; } }; diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index ad488897f..8a5c75618 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -96,11 +96,17 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp // Note: this is only interactive in plain text mode. If RTE is enabled, // APIs from the composer view model should be used. get { - guard !self.textFormattingEnabled else { return nil } + guard !self.textFormattingEnabled else { + MXLog.failure("[WysiwygInputToolbarView] Trying to get attributedTextMessage in RTE mode") + return nil + } return self.wysiwygViewModel.textView.attributedText } set { - guard !self.textFormattingEnabled else { return } + guard !self.textFormattingEnabled else { + MXLog.failure("[WysiwygInputToolbarView] Trying to set attributedTextMessage in RTE mode") + return + } self.wysiwygViewModel.textView.attributedText = newValue } } @@ -174,6 +180,16 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp showKeyboard() } } + + override func setPartialContent(_ attributedTextMessage: NSAttributedString) { + let content: String + if #available(iOS 15.0, *) { + content = PillsFormatter.stringByReplacingPills(in: attributedTextMessage, mode: .markdown) + } else { + content = attributedTextMessage.string + } + self.wysiwygViewModel.setMarkdownContent(content) + } func showKeyboard() { self.viewModel.showKeyboard() @@ -191,7 +207,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } func mention(_ member: MXRoomMember) { - self.wysiwygViewModel.setMention(link: MXTools.permalinkToUser(withUserId: member.userId), + self.wysiwygViewModel.setMention(url: MXTools.permalinkToUser(withUserId: member.userId), name: member.displayname, mentionType: .user) } @@ -281,12 +297,31 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp }, wysiwygViewModel.$plainTextContent - .dropFirst() .removeDuplicates() - .sink { [weak self] value in - guard let self else { return } - self.textMessage = value.string + .dropFirst() + .sink { [weak self] attributed in + // Note: filter out `plainTextMode` being off, as switching to RTE will trigger this + // publisher with empty content. This avoids saving the partial text message + // or trying to compute suggestion from this empty content. + guard let self, wysiwygViewModel.plainTextMode else { return } + self.textMessage = attributed.string self.toolbarViewDelegate?.roomInputToolbarViewDidChangeTextMessage(self) + self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed) + }, + + wysiwygViewModel.$attributedContent + .removeDuplicates(by: { + $0.text == $1.text + }) + .dropFirst() + .sink { [weak self] _ in + // Note: filter out `plainTextMode` being on, as switching to plain text mode will trigger this + // publisher with empty content. This avoids saving the partial text message + // or trying to compute suggestion from this empty content. + guard let self, !self.wysiwygViewModel.plainTextMode else { return } + let markdown = self.wysiwygViewModel.content.markdown + let attributed = NSAttributedString(string: markdown, attributes: [.font: self.defaultFont]) + self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed) } ] diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/MockComposerLinkActionScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/MockComposerLinkActionScreenState.swift index 6bdc5ebc5..335ff3196 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/MockComposerLinkActionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/MockComposerLinkActionScreenState.swift @@ -33,7 +33,7 @@ enum MockComposerLinkActionScreenState: MockScreenState, CaseIterable { case .create: viewModel = .init(from: .create) case .edit: - viewModel = .init(from: .edit(link: "https://element.io")) + viewModel = .init(from: .edit(url: "https://element.io")) } return ( [viewModel], diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift index 367417282..d16dd7212 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/ViewModel/ComposerLinkActionViewModel.swift @@ -36,7 +36,7 @@ final class ComposerLinkActionViewModel: ComposerLinkActionViewModelType, Compos switch linkAction { case let .edit(link): initialViewState = .init( - linkAction: .edit(link: link), + linkAction: .edit(url: link), bindings: .init( text: "", linkUrl: link diff --git a/changelog.d/7535.bugfix b/changelog.d/7535.bugfix new file mode 100644 index 000000000..f21ab863c --- /dev/null +++ b/changelog.d/7535.bugfix @@ -0,0 +1 @@ +Labs: Rich Text Editor: Fix partial text messages not being saved for each room diff --git a/project.yml b/project.yml index 6a207706d..3922de651 100644 --- a/project.yml +++ b/project.yml @@ -56,7 +56,7 @@ packages: branch: 0.0.1 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 2.0.0 + version: 2.1.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 From aa71f4be2c988c57a0bf67584fb4264696beeab2 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 3 May 2023 15:49:58 +0200 Subject: [PATCH 099/149] Fix composer unit tests --- .../Test/Unit/ComposerLinkActionViewModelTests.swift | 6 +++--- .../Room/Composer/Test/Unit/ComposerViewModelTests.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift index 2407eccc4..3fbb8d564 100644 --- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift @@ -54,7 +54,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { func testEditDefaultState() { let link = "element.io" - setUp(with: .edit(link: link)) + setUp(with: .edit(url: link)) XCTAssertEqual(context.viewState.bindings.text, "") XCTAssertEqual(context.viewState.bindings.linkUrl, link) XCTAssertTrue(context.viewState.isSaveButtonDisabled) @@ -83,7 +83,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testRemoveAction() { - setUp(with: .edit(link: "element.io")) + setUp(with: .edit(url: "element.io")) var result: ComposerLinkActionViewModelResult! viewModel.callback = { value in result = value @@ -119,7 +119,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase { } func testSaveActionForEdit() { - setUp(with: .edit(link: "element.io")) + setUp(with: .edit(url: "element.io")) var result: ComposerLinkActionViewModelResult! viewModel.callback = { value in result = value diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift index e4d5b595d..c68cd7783 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift @@ -98,7 +98,7 @@ final class ComposerViewModelTests: XCTestCase { XCTAssertEqual(result, .linkTapped(LinkAction: .createWithText)) context.send(viewAction: .linkTapped(linkAction: .create)) XCTAssertEqual(result, .linkTapped(LinkAction: .create)) - context.send(viewAction: .linkTapped(linkAction: .edit(link: "https://element.io"))) - XCTAssertEqual(result, .linkTapped(LinkAction: .edit(link: "https://element.io"))) + context.send(viewAction: .linkTapped(linkAction: .edit(url: "https://element.io"))) + XCTAssertEqual(result, .linkTapped(LinkAction: .edit(url: "https://element.io"))) } } From ee9fd2c18615a0ed6cb8f28bf330566dc5772c63 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 3 May 2023 17:26:54 +0200 Subject: [PATCH 100/149] Add missing self in closure --- .../Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 8a5c75618..edd951fd6 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -303,7 +303,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp // Note: filter out `plainTextMode` being off, as switching to RTE will trigger this // publisher with empty content. This avoids saving the partial text message // or trying to compute suggestion from this empty content. - guard let self, wysiwygViewModel.plainTextMode else { return } + guard let self, self.wysiwygViewModel.plainTextMode else { return } self.textMessage = attributed.string self.toolbarViewDelegate?.roomInputToolbarViewDidChangeTextMessage(self) self.toolbarViewDelegate?.roomInputToolbarView?(self, shouldStorePartialContent: attributed) From 211cc7e5f13fc0b3d9c91004c2894dbdb206d665 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Wed, 3 May 2023 10:35:55 +0200 Subject: [PATCH 101/149] Add logs to track a problem with the top left avatar disappearing --- Riot/Modules/Common/Avatar/AvatarView.swift | 9 +++++++++ Riot/Modules/Home/AllChats/AllChatsCoordinator.swift | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/Riot/Modules/Common/Avatar/AvatarView.swift b/Riot/Modules/Common/Avatar/AvatarView.swift index a3f46a5aa..a1ead57db 100644 --- a/Riot/Modules/Common/Avatar/AvatarView.swift +++ b/Riot/Modules/Common/Avatar/AvatarView.swift @@ -103,12 +103,17 @@ class AvatarView: UIView, Themable { func updateAvatarImageView(with viewData: AvatarViewDataProtocol) { guard let avatarImageView = self.avatarImageView else { + MXLog.warning("[AvatarView] avatar not updated because avatarImageView is nil.") return } let (defaultAvatarImage, defaultAvatarImageContentMode) = viewData.fallbackImageParameters() ?? (nil, .scaleAspectFill) updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode) + if defaultAvatarImage == nil { + MXLog.warning("[AvatarView] defaultAvatarImage is nil") + } + if let avatarUrl = viewData.avatarUrl { avatarImageView.setImageURI(avatarUrl, withType: nil, @@ -118,6 +123,10 @@ class AvatarView: UIView, Themable { previewImage: defaultAvatarImage, mediaManager: viewData.mediaManager) updateAvatarContentMode(contentMode: .scaleAspectFill) + + if avatarImageView.frame.size.width < 8 || avatarImageView.frame.size.height < 8 { + MXLog.warning("[AvatarView] small avatarImageView frame: \(avatarImageView.frame)") + } } else { updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode) } diff --git a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift index 89c8edc2c..8765adb05 100644 --- a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift +++ b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift @@ -387,7 +387,11 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { } private func updateAvatarButtonItem() { + MXLog.info("[AllChatsCoordinator] updating avatar button item.") if let avatar = userAvatarViewData(from: currentMatrixSession) { + if avatarMenuView == nil { + MXLog.warning("[AllChatsCoordinator] updateAvatarButtonItem: avatarMenuView is nil.") + } avatarMenuView?.fill(with: avatar) avatarMenuButton?.setImage(nil, for: .normal) } else { From b56686d33784b04133d7650cdbfa0d6cfc2ca4a4 Mon Sep 17 00:00:00 2001 From: Frank Rotermund Date: Tue, 9 May 2023 09:02:36 +0200 Subject: [PATCH 102/149] MESSENGER-4697 set app version to 2.7.0 --- Config/AppVersion.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 4fcd0456f..d2e9637b9 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -16,5 +16,5 @@ // // Version -MARKETING_VERSION = 2.6.0 +MARKETING_VERSION = 2.7.0 CURRENT_PROJECT_VERSION = 20220714163152 From 7675da88c4634e91a697bbe2b45781b5aeff79ce Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 9 May 2023 17:05:27 +0100 Subject: [PATCH 103/149] Use the app's language for accessibility. --- Riot/Modules/Application/LegacyAppDelegate.m | 1 + Riot/Modules/Settings/SettingsViewController.m | 1 + changelog.d/pr-7493.bugfix | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog.d/pr-7493.bugfix diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 8678f5ab8..ab17eeac5 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -390,6 +390,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } [NSBundle mxk_setLanguage:language]; [NSBundle mxk_setFallbackLanguage:@"en"]; + UIApplication.sharedApplication.accessibilityLanguage = language; if (BuildSettings.disableRightToLeftLayout) { diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 055841f3f..be87bea3b 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -4158,6 +4158,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> || (language == nil && [NSBundle mxk_language])) { [NSBundle mxk_setLanguage:language]; + UIApplication.sharedApplication.accessibilityLanguage = language; // Store user settings NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults; diff --git a/changelog.d/pr-7493.bugfix b/changelog.d/pr-7493.bugfix new file mode 100644 index 000000000..b486878b5 --- /dev/null +++ b/changelog.d/pr-7493.bugfix @@ -0,0 +1 @@ +Make sure to use the chosen language for the VoiceOver voice too. From 6b0ce2e65af329c712121ce4f4fd8cc58b1bd915 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Tue, 9 May 2023 09:53:42 +0200 Subject: [PATCH 104/149] Feat: add a flag in the build settings to force the user to define a homeserver. --- Config/BuildSettings.swift | 9 +++++++-- .../AuthenticationCoordinator.swift | 14 ++++++++++++-- .../Legacy/AuthenticationViewController.m | 18 ++++++++++++++++-- .../Common/AuthenticationModels.swift | 4 ++++ ...henticationServerSelectionCoordinator.swift | 8 +++++++- changelog.d/pr-7541.change | 1 + 6 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 changelog.d/pr-7541.change diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index e8c129619..1b2571708 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -98,10 +98,15 @@ final class BuildSettings: NSObject { // MARK: - Server configuration - // Default servers proposed on the authentication screen + // Force the user to set a homeserver instead of using the default one + static let forceHomeserverSelection = false + + // Default server proposed on the authentication screen static let serverConfigDefaultHomeserverUrlString = "https://matrix.org" - static let serverConfigDefaultIdentityServerUrlString = "https://vector.im" + // Default identity server + static let serverConfigDefaultIdentityServerUrlString = "https://vector.im" + static let serverConfigSygnalAPIUrlString = "https://matrix.org/_matrix/push/v1/notify" diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index a245147cd..6d5250497 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -130,9 +130,19 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc } let flow: AuthenticationFlow = initialScreen == .login ? .login : .register + + // Use the homeserver defined by a provisioningLink or by the user (if none is set, the default one will be used) + let homeserverAddress = authenticationService.provisioningLink?.homeserverUrl ?? authenticationService.state.homeserver.addressFromUser + + // Check if the user must select a server + if BuildSettings.forceHomeserverSelection, homeserverAddress == nil { + showServerSelectionScreen(for: flow) + return + } + do { - // Start the flow using the default server (or a provisioning link if set). - try await authenticationService.startFlow(flow) + // Start the flow (if homeserverAddress is nil, the default server will be used). + try await authenticationService.startFlow(flow, for: homeserverAddress) } catch { MXLog.error("[AuthenticationCoordinator] start: Failed to start, showing server selection.") showServerSelectionScreen(for: flow) diff --git a/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m b/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m index 3f5cfaf67..c0605d813 100644 --- a/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/Legacy/AuthenticationViewController.m @@ -132,7 +132,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; target:self action:@selector(onButtonPressed:)]; - self.defaultHomeServerUrl = RiotSettings.shared.homeserverUrlString; + if (BuildSettings.forceHomeserverSelection) + { + self.defaultHomeServerUrl = nil; + } + else + { + self.defaultHomeServerUrl = RiotSettings.shared.homeserverUrlString; + } self.defaultIdentityServerUrl = RiotSettings.shared.identityServerUrlString; @@ -1207,7 +1214,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; [self saveCustomServerInputs]; // Restore default configuration - [self setHomeServerTextFieldText:self.defaultHomeServerUrl]; + if (BuildSettings.forceHomeserverSelection) + { + [self setHomeServerTextFieldText:nil]; + } + else + { + [self setHomeServerTextFieldText:self.defaultHomeServerUrl]; + } [self setIdentityServerTextFieldText:self.defaultIdentityServerUrl]; [self.customServersTickButton setImage:AssetImages.selectionUntick.image forState:UIControlStateNormal]; diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift index e59aa0189..34d7adb90 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift @@ -86,6 +86,10 @@ class HomeserverAddress: NSObject { /// - Ensure the address contains a scheme, otherwise make it `https`. /// - Remove any trailing slashes. static func sanitized(_ address: String) -> String { + guard !address.isEmpty else { + // prevent prefixing an empty string with "https:" + return address + } var address = address.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() if !address.contains("://") { diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift index c5d521701..13308c262 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift @@ -57,7 +57,13 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { self.parameters = parameters let homeserver = parameters.authenticationService.state.homeserver - let viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: homeserver.displayableAddress, + let homeserverAddress: String + if BuildSettings.forceHomeserverSelection, homeserver.addressFromUser == nil { + homeserverAddress = "" + } else { + homeserverAddress = homeserver.displayableAddress + } + let viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: homeserverAddress, flow: parameters.authenticationService.state.flow, hasModalPresentation: parameters.hasModalPresentation) let view = AuthenticationServerSelectionScreen(viewModel: viewModel.context) diff --git a/changelog.d/pr-7541.change b/changelog.d/pr-7541.change new file mode 100644 index 000000000..0e0c71fa6 --- /dev/null +++ b/changelog.d/pr-7541.change @@ -0,0 +1 @@ +Add a flag in the build settings to force the user to define a homeserver instead of using the default one. From e499389998aeb5de7cd9790c393bd1a7eaaabd9f Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 9 May 2023 19:12:30 +0100 Subject: [PATCH 105/149] Fix voiceover order of room creation header and message composer. --- .../RoomCreationIntroCell.swift | 5 +++++ .../RoomCreationIntroCellContentView.swift | 2 ++ .../Views/InputToolbar/RoomInputToolbarView.m | 17 +++++++++++++++++ changelog.d/pr-7543.bugfix | 1 + 4 files changed, 25 insertions(+) create mode 100644 changelog.d/pr-7543.bugfix diff --git a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift index 9bccbddf9..33a9c3d2b 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift +++ b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift @@ -164,6 +164,11 @@ class RoomCreationIntroCell: MXKRoomBubbleTableViewCell { roomCellContentView.didTapAddParticipants = { [weak self] in self?.notifyDelegate(with: RoomCreationIntroCell.tapOnAddParticipants) } + + self.accessibilityElements = [roomCellContentView.roomAvatarView as Any, + roomCellContentView.titleLabel as Any, + roomCellContentView.informationLabel as Any, + roomCellContentView.addParticipantsContainerView as Any] } diff --git a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift index 809cf4676..e664c66ab 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift +++ b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift @@ -69,8 +69,10 @@ final class RoomCreationIntroCellContentView: UIView, NibLoadable, Themable { self.addParticipantsButton.layer.masksToBounds = true self.addParticipantsButton.addTarget(self, action: #selector(socialButtonAction(_:)), for: .touchUpInside) + self.addParticipantsButton.accessibilityLabel = VectorL10n.roomIntroCellAddParticipantsAction self.addParticipantsLabel.text = VectorL10n.roomIntroCellAddParticipantsAction + self.addParticipantsLabel.isAccessibilityElement = false self.roomAvatarView.showCameraBadgeOnFallbackImage = true } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 2cead382a..0d058fefc 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -70,6 +70,8 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; _sendMode = RoomInputToolbarViewSendModeSend; self.inputContextViewHeightConstraint.constant = 0; + self.inputContextLabel.isAccessibilityElement = NO; + self.inputContextButton.isAccessibilityElement = NO; [self.rightInputToolbarButton setTitle:nil forState:UIControlStateNormal]; [self.rightInputToolbarButton setTitle:nil forState:UIControlStateHighlighted]; @@ -252,6 +254,10 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; break; } + // Hide the context items from VoiceOver when the context view is "hidden". + self.inputContextLabel.isAccessibilityElement = self.inputContextViewHeightConstraint.constant > 0; + self.inputContextButton.isAccessibilityElement = self.inputContextViewHeightConstraint.constant > 0; + [self.rightInputToolbarButton setImage:buttonImage forState:UIControlStateNormal]; if (self.maxHeight && updatedHeight > self.maxHeight) @@ -477,11 +483,22 @@ static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor], [self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor], [self.mainToolbarView.rightAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.rightAnchor]]]; + + // The voice message toolbar is taller than the input toolbar so the record button is read + // out before the other subviews. Fix this by manually adding the elements in the right order. + self.accessibilityElements = @[self.attachMediaButton, + self.actionsBar, + self.inputContextLabel, + self.inputContextButton, + self.textView, + self.rightInputToolbarButton, + self.voiceMessageToolbarView]; } else { [self.voiceMessageToolbarView removeFromSuperview]; _voiceMessageToolbarView = nil; + self.accessibilityElements = nil; } } @end diff --git a/changelog.d/pr-7543.bugfix b/changelog.d/pr-7543.bugfix new file mode 100644 index 000000000..6a56590cd --- /dev/null +++ b/changelog.d/pr-7543.bugfix @@ -0,0 +1 @@ +Fix voiceover order of room creation header and message composer. From cc1f0eea269e7bd19cc469aab38dbd906cc31711 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 11 May 2023 09:45:04 +0200 Subject: [PATCH 106/149] Fix: apply the changes requested in the PR review --- Config/BuildSettings.swift | 6 +++--- .../Modules/Authentication/AuthenticationCoordinator.swift | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 1b2571708..f58f969c1 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -98,13 +98,13 @@ final class BuildSettings: NSObject { // MARK: - Server configuration - // Force the user to set a homeserver instead of using the default one + /// Force the user to set a homeserver instead of using the default one static let forceHomeserverSelection = false - // Default server proposed on the authentication screen + /// Default server proposed on the authentication screen static let serverConfigDefaultHomeserverUrlString = "https://matrix.org" - // Default identity server + /// Default identity server static let serverConfigDefaultIdentityServerUrlString = "https://vector.im" static let serverConfigSygnalAPIUrlString = "https://matrix.org/_matrix/push/v1/notify" diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 6d5250497..295a591f7 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -131,18 +131,15 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc let flow: AuthenticationFlow = initialScreen == .login ? .login : .register - // Use the homeserver defined by a provisioningLink or by the user (if none is set, the default one will be used) - let homeserverAddress = authenticationService.provisioningLink?.homeserverUrl ?? authenticationService.state.homeserver.addressFromUser - // Check if the user must select a server - if BuildSettings.forceHomeserverSelection, homeserverAddress == nil { + if BuildSettings.forceHomeserverSelection, authenticationService.provisioningLink?.homeserverUrl == nil { showServerSelectionScreen(for: flow) return } do { // Start the flow (if homeserverAddress is nil, the default server will be used). - try await authenticationService.startFlow(flow, for: homeserverAddress) + try await authenticationService.startFlow(flow) } catch { MXLog.error("[AuthenticationCoordinator] start: Failed to start, showing server selection.") showServerSelectionScreen(for: flow) From 0d1e16bae9e88438c044b19430135aefbffc859f Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Thu, 11 May 2023 13:39:05 +0200 Subject: [PATCH 107/149] MESSENGER-4699 update sdk version --- Podfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile b/Podfile index 3da19f891..976f328ff 100644 --- a/Podfile +++ b/Podfile @@ -43,7 +43,7 @@ when String # specific MatrixSDK released version $matrixSDKVersionSpec = $matrixSDKVersion end -$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.6_bwi' } +$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.9_bwi_beta' } # Method to import the MatrixSDK def import_MatrixSDK From e3b1880f49f0ac9435ac430bf3d0fc48bce43e5d Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Thu, 11 May 2023 18:15:35 +0200 Subject: [PATCH 108/149] Fix: text color of the last event description was incorrect. --- .../Categories/NSAttributedString+Theme.swift | 64 +++++++++++++++++++ Riot/Managers/Theme/ThemeService.swift | 2 +- .../Recents/Views/RecentTableViewCell.m | 3 +- Riot/Utils/EventFormatter.m | 20 +++++- Riot/Utils/ThemeColorResolver.swift | 48 ++++++++++++++ changelog.d/pr-7545.bugfix | 1 + 6 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 Riot/Categories/NSAttributedString+Theme.swift create mode 100644 Riot/Utils/ThemeColorResolver.swift create mode 100644 changelog.d/pr-7545.bugfix diff --git a/Riot/Categories/NSAttributedString+Theme.swift b/Riot/Categories/NSAttributedString+Theme.swift new file mode 100644 index 000000000..9a0e01c93 --- /dev/null +++ b/Riot/Categories/NSAttributedString+Theme.swift @@ -0,0 +1,64 @@ +// +// Copyright 2023 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 + +/// Custom NSAttributedString.Key to specify the theme +let themeIdentifierAttributeName = NSAttributedString.Key("ThemeIdentifier") +/// Custom NSAttributedString.Key to specify a theme color by its name +let themeColorNameAttributeName = NSAttributedString.Key("ThemeColorName") + +extension NSAttributedString { + /// Fix foreground color attributes if this attributed string contains the `themeIdentifierAttributeName` and `foregroundColorNameAttributeName` attributes + /// - Returns: a new attributed string with updated colors + @objc func fixForegroundColor() -> NSAttributedString { + let activeTheme = ThemeService.shared().theme + + // Check if a theme is defined for this attributed string + var needUpdate = false + self.vc_enumerateAttribute(themeIdentifierAttributeName) { (themeIdentifier: String, range: NSRange, _) in + needUpdate = themeIdentifier != activeTheme.identifier + } + + guard needUpdate else { + return self + } + + // Build a new attributedString with the proper colors if possible + let mutableAttributedString = NSMutableAttributedString(attributedString: self) + mutableAttributedString.vc_enumerateAttribute(themeColorNameAttributeName) { (colorName: String, range: NSRange, _) in + if let color = ThemeColorResolver.getColorByName(colorName) { + mutableAttributedString.addAttribute(.foregroundColor, value: color, range: range) + } + } + return mutableAttributedString + } +} + +extension NSMutableAttributedString { + /// Adds a theme color name attribute + /// - Parameters: + /// - colorName: color name + /// - range:range for this attribute + @objc func addThemeColorNameAttribute(_ colorName: String, range: NSRange) { + self.addAttribute(themeColorNameAttributeName, value: colorName, range: range) + } + + /// Adds a theme identifier attribute + @objc func addThemeIdentifierAttribute() { + self.addAttribute(themeIdentifierAttributeName, value: ThemeService.shared().theme.identifier, range: .init(location: 0, length: length)) + } +} diff --git a/Riot/Managers/Theme/ThemeService.swift b/Riot/Managers/Theme/ThemeService.swift index 209812111..3ce421d01 100644 --- a/Riot/Managers/Theme/ThemeService.swift +++ b/Riot/Managers/Theme/ThemeService.swift @@ -23,5 +23,5 @@ extension ThemeService { return nil } return ThemeIdentifier(rawValue: themeId) - } + } } diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index a21996333..afd3f5c88 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -81,7 +81,8 @@ // Manage lastEventAttributedTextMessage optional property if (!roomCellData.roomSummary.spaceChildInfo && [roomCellData respondsToSelector:@selector(lastEventAttributedTextMessage)]) { - self.lastEventDescription.attributedText = roomCellData.lastEventAttributedTextMessage; + // Attempt to correct the attributed string colors to match the current theme + self.lastEventDescription.attributedText = [roomCellData.lastEventAttributedTextMessage fixForegroundColor]; } else { diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 278d902a5..d25655a22 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -573,8 +573,13 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent { // Force the default text color for the last message (cancel highlighted message color) NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:summary.lastMessage.attributedText]; - [lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor - range:NSMakeRange(0, lastEventDescription.length)]; + NSRange range = NSMakeRange(0, lastEventDescription.length); + [lastEventDescription addAttribute:NSForegroundColorAttributeName + value:ThemeService.shared.theme.colors.secondaryContent + range:range]; + [lastEventDescription addThemeColorNameAttribute:@"secondaryContent" range:range]; + [lastEventDescription addThemeIdentifierAttribute]; + summary.lastMessage.attributedText = lastEventDescription; } @@ -670,9 +675,11 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent NSAttributedString *attachmentString = nil; UIColor *textColor; + NSString *colorIdentifier; if (isStoppedVoiceBroadcast) { - textColor = ThemeService.shared.theme.textSecondaryColor; + textColor = ThemeService.shared.theme.colors.secondaryContent; + colorIdentifier = @"secondaryContent"; NSString *senderDisplayName; if ([stateEvent.stateKey isEqualToString:session.myUser.userId]) { @@ -688,6 +695,7 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent else { textColor = ThemeService.shared.theme.colors.alert; + colorIdentifier = @"alert"; UIImage *liveImage = AssetImages.voiceBroadcastLive.image; NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; @@ -717,6 +725,12 @@ withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent } [lastMessage addAttribute:NSForegroundColorAttributeName value:textColor range:NSMakeRange(0, lastMessage.length)]; + if (colorIdentifier) + { + [lastMessage addThemeColorNameAttribute:colorIdentifier range:NSMakeRange(0, lastMessage.length)]; + [lastMessage addThemeIdentifierAttribute]; + } + summary.lastMessage.attributedText = lastMessage; return YES; diff --git a/Riot/Utils/ThemeColorResolver.swift b/Riot/Utils/ThemeColorResolver.swift new file mode 100644 index 000000000..c0010c97d --- /dev/null +++ b/Riot/Utils/ThemeColorResolver.swift @@ -0,0 +1,48 @@ +// +// Copyright 2023 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 + +/// Utility struct to get a theme color by its name +struct ThemeColorResolver { + private static var theme: Theme? + private static var colorsTable: [String: UIColor] = [:] + private static let queue = DispatchQueue(label: "io.element.ThemeColorResolver.queue", qos: .userInteractive) + + private static func setTheme(theme: Theme) { + queue.sync { + guard self.theme?.identifier != theme.identifier else { + return + } + self.theme = theme + colorsTable = [:] + let mirror = Mirror(reflecting: theme.colors) + for child in mirror.children { + if let colorName = child.label { + colorsTable[colorName] = child.value as? UIColor + } + } + } + } + + /// Finds a color by its name in the current theme colors + /// - Parameter name: color name + /// - Returns: the corresponding color or nil + static func getColorByName(_ name: String) -> UIColor? { + setTheme(theme: ThemeService.shared().theme) + return colorsTable[name] + } +} diff --git a/changelog.d/pr-7545.bugfix b/changelog.d/pr-7545.bugfix new file mode 100644 index 000000000..a2f30eb67 --- /dev/null +++ b/changelog.d/pr-7545.bugfix @@ -0,0 +1 @@ +Fix: The last event description text color now matches the active theme. From ff6152315458ce455e9b5166c813a80a1a52698e Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 12 May 2023 18:30:20 +0200 Subject: [PATCH 109/149] Disable removing mention/command text trigger with RTE enabled --- Riot/Modules/Room/RoomViewController.m | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index cc108baa3..17253279b 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -8147,6 +8147,14 @@ static CGSize kThreadListBarButtonItemImageSize; - (void)removeTriggerTextFromComposer:(NSString *)textTrigger { RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView; + Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass]; + + // RTE handles removing the text trigger by itself. + if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && RiotSettings.shared.enableWysiwygTextFormatting) + { + return; + } + if (toolbar && textTrigger.length) { NSMutableAttributedString *attributedTextMessage = [[NSMutableAttributedString alloc] initWithAttributedString:toolbar.attributedTextMessage]; [[attributedTextMessage mutableString] replaceOccurrencesOfString:textTrigger From 1d15f31ab9ef382de5a17e2c230a4c4fb72dd2bd Mon Sep 17 00:00:00 2001 From: aringenbach Date: Fri, 12 May 2023 18:34:32 +0200 Subject: [PATCH 110/149] Fix mention pills display in thread list --- .../Views/Cell/ThreadTableViewCell.swift | 8 ++-- .../Views/Cell/ThreadTableViewCell.xib | 37 ++++++++++--------- changelog.d/7322.bugfix | 1 + 3 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 changelog.d/7322.bugfix diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift index b99eec0c3..be4ed85dc 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift @@ -35,7 +35,7 @@ class ThreadTableViewCell: UITableViewCell { @IBOutlet private weak var rootMessageAvatarView: UserAvatarView! @IBOutlet private weak var rootMessageSenderLabel: UILabel! - @IBOutlet private weak var rootMessageContentLabel: UILabel! + @IBOutlet private weak var rootMessageContentTextView: UITextView! @IBOutlet private weak var lastMessageTimeLabel: UILabel! @IBOutlet private weak var summaryView: ThreadSummaryView! @IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView! @@ -61,7 +61,7 @@ class ThreadTableViewCell: UITableViewCell { if let rootMessageText = model.rootMessageText { updateRootMessageContentAttributes(rootMessageText, color: rootMessageColor) } else { - rootMessageContentLabel.attributedText = nil + rootMessageContentTextView.attributedText = nil } lastMessageTimeLabel.text = model.lastMessageTime if let summaryModel = model.summaryModel { @@ -83,7 +83,7 @@ class ThreadTableViewCell: UITableViewCell { mutable.addAttributes([ .foregroundColor: color ], range: NSRange(location: 0, length: mutable.length)) - rootMessageContentLabel.attributedText = mutable + rootMessageContentTextView.attributedText = mutable } } @@ -97,7 +97,7 @@ extension ThreadTableViewCell: Themable { Self.usernameColorGenerator.update(theme: theme) updateRootMessageSenderColor() rootMessageAvatarView.backgroundColor = .clear - if let attributedText = rootMessageContentLabel.attributedText { + if let attributedText = rootMessageContentTextView.attributedText { updateRootMessageContentAttributes(attributedText, color: rootMessageColor) } lastMessageTimeLabel.textColor = theme.colors.secondaryContent diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib index f9c881396..3014cd711 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -11,14 +11,14 @@ - + - + @@ -27,7 +27,7 @@ - + @@ -51,13 +51,13 @@ - - + + + @@ -68,20 +68,20 @@ - + - - + - + + @@ -89,7 +89,7 @@ - + @@ -97,6 +97,9 @@ + + + diff --git a/changelog.d/7322.bugfix b/changelog.d/7322.bugfix new file mode 100644 index 000000000..b13925fa3 --- /dev/null +++ b/changelog.d/7322.bugfix @@ -0,0 +1 @@ +Fix mention pills display in thread list From 8e2761de4ed1dcc3cdde7275d6c4e6f063dbc590 Mon Sep 17 00:00:00 2001 From: Element CI Date: Tue, 16 May 2023 15:46:01 +0300 Subject: [PATCH 111/149] changelog.d: Upgrade MatrixSDK version ([v0.26.10](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.10)). --- 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 f4cbeda08..52ce26306 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,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.26.9' +$matrixSDKVersion = '= 0.26.10' # $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..a52d29522 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixSDK version ([v0.26.10](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.10)). \ No newline at end of file From 41bd49f99ba95e99cf065bf5303cfb851bc443e1 Mon Sep 17 00:00:00 2001 From: Element CI Date: Tue, 16 May 2023 15:46:02 +0300 Subject: [PATCH 112/149] version++ --- CHANGES.md | 31 +++++++++++++++++++++++++++++++ changelog.d/7322.bugfix | 1 - changelog.d/7493.feature | 1 - changelog.d/7497.bugfix | 1 - changelog.d/7504.change | 1 - changelog.d/7517.change | 1 - changelog.d/7523.bugfix | 1 - changelog.d/7526.bugfix | 1 - changelog.d/7530.bugfix | 1 - changelog.d/7535.bugfix | 1 - changelog.d/pr-7404.bugfix | 1 - changelog.d/pr-7493.bugfix | 1 - changelog.d/pr-7508.change | 1 - changelog.d/pr-7512.bugfix | 1 - changelog.d/pr-7521.bugfix | 1 - changelog.d/pr-7522.bugfix | 1 - changelog.d/pr-7541.change | 1 - changelog.d/pr-7543.bugfix | 1 - changelog.d/pr-7545.bugfix | 1 - changelog.d/x-nolink-0.change | 1 - 20 files changed, 31 insertions(+), 19 deletions(-) delete mode 100644 changelog.d/7322.bugfix delete mode 100644 changelog.d/7493.feature delete mode 100644 changelog.d/7497.bugfix delete mode 100644 changelog.d/7504.change delete mode 100644 changelog.d/7517.change delete mode 100644 changelog.d/7523.bugfix delete mode 100644 changelog.d/7526.bugfix delete mode 100644 changelog.d/7530.bugfix delete mode 100644 changelog.d/7535.bugfix delete mode 100644 changelog.d/pr-7404.bugfix delete mode 100644 changelog.d/pr-7493.bugfix delete mode 100644 changelog.d/pr-7508.change delete mode 100644 changelog.d/pr-7512.bugfix delete mode 100644 changelog.d/pr-7521.bugfix delete mode 100644 changelog.d/pr-7522.bugfix delete mode 100644 changelog.d/pr-7541.change delete mode 100644 changelog.d/pr-7543.bugfix delete mode 100644 changelog.d/pr-7545.bugfix delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index 428bd30f7..7e942b526 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,34 @@ +## Changes in 1.10.12 (2023-05-16) + +✨ Features + +- Add composer suggestions for slash commands ([#7493](https://github.com/vector-im/element-ios/issues/7493)) + +🙌 Improvements + +- Crypto: Deprecate MXLegacyCrypto ([#7508](https://github.com/vector-im/element-ios/pull/7508)) +- Add a flag in the build settings to force the user to define a homeserver instead of using the default one. ([#7541](https://github.com/vector-im/element-ios/pull/7541)) +- Upgrade MatrixSDK version ([v0.26.10](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.10)). +- Add an audio alert when the voice broadcast recording is automatically paused ([#7504](https://github.com/vector-im/element-ios/issues/7504)) +- Timeline: Remove the matrix ID displayed when someone has changed its display name. ([#7517](https://github.com/vector-im/element-ios/issues/7517)) + +🐛 Bugfixes + +- Fix an issue where the Secrets Reset screen would open twice. ([#7404](https://github.com/vector-im/element-ios/pull/7404)) +- Make sure to use the chosen language for the VoiceOver voice too. ([#7493](https://github.com/vector-im/element-ios/pull/7493)) +- Fix the position of the send confirmation icon. ([#7512](https://github.com/vector-im/element-ios/pull/7512)) +- Disable accessibility for emojis during session verification. ([#7521](https://github.com/vector-im/element-ios/pull/7521)) +- Fix accessibility when entering the PIN to unlock the app. ([#7522](https://github.com/vector-im/element-ios/pull/7522)) +- Fix voiceover order of room creation header and message composer. ([#7543](https://github.com/vector-im/element-ios/pull/7543)) +- Fix: The last event description text color now matches the active theme. ([#7545](https://github.com/vector-im/element-ios/pull/7545)) +- Fix mention pills display in thread list ([#7322](https://github.com/vector-im/element-ios/issues/7322)) +- Poll: The timeline sometimes displayed closed polls in the wrong order. ([#7497](https://github.com/vector-im/element-ios/issues/7497)) +- Fix a flickering issue when the timeline datasource is reloaded. ([#7523](https://github.com/vector-im/element-ios/issues/7523)) +- Fix the position of the marker highlighting an event. ([#7526](https://github.com/vector-im/element-ios/issues/7526)) +- Fix application crashing when opening a thread with RTE enabled ([#7530](https://github.com/vector-im/element-ios/issues/7530)) +- Labs: Rich Text Editor: Fix partial text messages not being saved for each room ([#7535](https://github.com/vector-im/element-ios/issues/7535)) + + ## Changes in 1.10.11 (2023-04-18) 🙌 Improvements diff --git a/changelog.d/7322.bugfix b/changelog.d/7322.bugfix deleted file mode 100644 index b13925fa3..000000000 --- a/changelog.d/7322.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix mention pills display in thread list diff --git a/changelog.d/7493.feature b/changelog.d/7493.feature deleted file mode 100644 index 075a7f6a2..000000000 --- a/changelog.d/7493.feature +++ /dev/null @@ -1 +0,0 @@ -Add composer suggestions for slash commands diff --git a/changelog.d/7497.bugfix b/changelog.d/7497.bugfix deleted file mode 100644 index a8558b843..000000000 --- a/changelog.d/7497.bugfix +++ /dev/null @@ -1 +0,0 @@ -Poll: The timeline sometimes displayed closed polls in the wrong order. diff --git a/changelog.d/7504.change b/changelog.d/7504.change deleted file mode 100644 index 2fed9c438..000000000 --- a/changelog.d/7504.change +++ /dev/null @@ -1 +0,0 @@ -Add an audio alert when the voice broadcast recording is automatically paused diff --git a/changelog.d/7517.change b/changelog.d/7517.change deleted file mode 100644 index f43662947..000000000 --- a/changelog.d/7517.change +++ /dev/null @@ -1 +0,0 @@ -Timeline: Remove the matrix ID displayed when someone has changed its display name. diff --git a/changelog.d/7523.bugfix b/changelog.d/7523.bugfix deleted file mode 100644 index bc5cf31a7..000000000 --- a/changelog.d/7523.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a flickering issue when the timeline datasource is reloaded. diff --git a/changelog.d/7526.bugfix b/changelog.d/7526.bugfix deleted file mode 100644 index 7adb60cc0..000000000 --- a/changelog.d/7526.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the position of the marker highlighting an event. diff --git a/changelog.d/7530.bugfix b/changelog.d/7530.bugfix deleted file mode 100644 index 5733a8d81..000000000 --- a/changelog.d/7530.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix application crashing when opening a thread with RTE enabled diff --git a/changelog.d/7535.bugfix b/changelog.d/7535.bugfix deleted file mode 100644 index f21ab863c..000000000 --- a/changelog.d/7535.bugfix +++ /dev/null @@ -1 +0,0 @@ -Labs: Rich Text Editor: Fix partial text messages not being saved for each room diff --git a/changelog.d/pr-7404.bugfix b/changelog.d/pr-7404.bugfix deleted file mode 100644 index 58609a160..000000000 --- a/changelog.d/pr-7404.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix an issue where the Secrets Reset screen would open twice. diff --git a/changelog.d/pr-7493.bugfix b/changelog.d/pr-7493.bugfix deleted file mode 100644 index b486878b5..000000000 --- a/changelog.d/pr-7493.bugfix +++ /dev/null @@ -1 +0,0 @@ -Make sure to use the chosen language for the VoiceOver voice too. diff --git a/changelog.d/pr-7508.change b/changelog.d/pr-7508.change deleted file mode 100644 index dbe206b34..000000000 --- a/changelog.d/pr-7508.change +++ /dev/null @@ -1 +0,0 @@ -Crypto: Deprecate MXLegacyCrypto diff --git a/changelog.d/pr-7512.bugfix b/changelog.d/pr-7512.bugfix deleted file mode 100644 index 1c6d3a98d..000000000 --- a/changelog.d/pr-7512.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the position of the send confirmation icon. diff --git a/changelog.d/pr-7521.bugfix b/changelog.d/pr-7521.bugfix deleted file mode 100644 index 3cedf12d4..000000000 --- a/changelog.d/pr-7521.bugfix +++ /dev/null @@ -1 +0,0 @@ -Disable accessibility for emojis during session verification. \ No newline at end of file diff --git a/changelog.d/pr-7522.bugfix b/changelog.d/pr-7522.bugfix deleted file mode 100644 index 0bd4e5b53..000000000 --- a/changelog.d/pr-7522.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix accessibility when entering the PIN to unlock the app. diff --git a/changelog.d/pr-7541.change b/changelog.d/pr-7541.change deleted file mode 100644 index 0e0c71fa6..000000000 --- a/changelog.d/pr-7541.change +++ /dev/null @@ -1 +0,0 @@ -Add a flag in the build settings to force the user to define a homeserver instead of using the default one. diff --git a/changelog.d/pr-7543.bugfix b/changelog.d/pr-7543.bugfix deleted file mode 100644 index 6a56590cd..000000000 --- a/changelog.d/pr-7543.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix voiceover order of room creation header and message composer. diff --git a/changelog.d/pr-7545.bugfix b/changelog.d/pr-7545.bugfix deleted file mode 100644 index a2f30eb67..000000000 --- a/changelog.d/pr-7545.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix: The last event description text color now matches the active theme. diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index a52d29522..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixSDK version ([v0.26.10](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.10)). \ No newline at end of file From 30a924bad5635421e71273c18f7d14f9fb709795 Mon Sep 17 00:00:00 2001 From: Element CI Date: Tue, 16 May 2023 16:26:10 +0300 Subject: [PATCH 113/149] finish version++ --- Podfile.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index f47ccb4f3..eae31a173 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -39,9 +39,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.26.9): - - MatrixSDK/Core (= 0.26.9) - - MatrixSDK/Core (0.26.9): + - MatrixSDK (0.26.10): + - MatrixSDK/Core (= 0.26.10) + - MatrixSDK/Core (0.26.10): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) @@ -49,7 +49,7 @@ PODS: - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.26.9): + - MatrixSDK/JingleCallStack (0.26.10): - JitsiMeetSDKLite (= 7.0.1-lite) - MatrixSDK/Core - MatrixSDKCrypto (0.3.4) @@ -102,8 +102,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.26.9) - - MatrixSDK/JingleCallStack (= 0.26.9) + - MatrixSDK (= 0.26.10) + - MatrixSDK/JingleCallStack (= 0.26.10) - OLMKit - PostHog (~> 2.0.0) - ReadMoreTextView (~> 3.0.1) @@ -187,7 +187,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 2f6222978156818cf4c6ba590762ade601ba72f9 + MatrixSDK: 68e39c246ff8d80c5788d5fc46e93fcbb24703fa MatrixSDKCrypto: ac805c22c24f79f349cdbfa065855c73a4c81b51 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d @@ -208,6 +208,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: a55fb48d3bef5f5e24fcaf8c39d1eae1ed8c1603 +PODFILE CHECKSUM: 4c82d7cddeb9c9b7a7adeaa2cd76d416117cd1a6 COCOAPODS: 1.11.3 From 260c8de58495146462e00193902a0d3ee39acec0 Mon Sep 17 00:00:00 2001 From: Frank Rotermund Date: Thu, 25 May 2023 13:52:30 +0000 Subject: [PATCH 114/149] Feature/4393 poll with visible participant p1 --- Config/BWIBuildSettings.swift | 1 + Podfile | 2 +- Riot/Assets/de.lproj/Bwi.strings | 1 + Riot/Assets/en.lproj/Bwi.strings | 1 + Riot/Generated/BWIStrings.swift | 4 +++ .../Coordinator/PollEditFormCoordinator.swift | 3 +- .../PollEditForm/PollEditFormModels.swift | 4 ++- .../PollEditForm/PollEditFormViewModel.swift | 7 ++-- .../Room/PollEditForm/View/PollEditForm.swift | 5 +++ .../PollEditFormParticipationToggle.swift | 35 +++++++++++++++++++ .../MockPollHistoryDetailScreenState.swift | 1 + .../Service/Mock/MockPollHistoryService.swift | 2 ++ .../Room/PollHistory/View/PollListItem.swift | 3 ++ .../Coordinator/TimelinePollCoordinator.swift | 1 + .../TimelinePoll/TimelinePollModels.swift | 2 ++ .../TimelinePollScreenState.swift | 1 + .../TimelinePoll/TimelinePollViewModel.swift | 2 ++ .../View/TimelinePollAnswerOptionButton.swift | 1 + 18 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormParticipationToggle.swift diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index 0bb015b1c..a25e779e3 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -124,6 +124,7 @@ class BWIBuildSettings: NSObject { var bwiUserLabelParticipantSorting = true var bwiShowClosedPolls = true + var bwiPollShowParticipantsToggle = true var bwiShowThreads = false var bwiShowRoomCreationSectionFooter = false diff --git a/Podfile b/Podfile index 976f328ff..969eed1b1 100644 --- a/Podfile +++ b/Podfile @@ -43,7 +43,7 @@ when String # specific MatrixSDK released version $matrixSDKVersionSpec = $matrixSDKVersion end -$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.9_bwi_beta' } +$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.9_bwi_beta_2' } # Method to import the MatrixSDK def import_MatrixSDK diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index 448710eb4..4b0801314 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -540,6 +540,7 @@ "poll_edit_form_poll_type" = "Umfragetyp"; "poll_edit_form_poll_type_closed" = "Versteckte Umfrage"; "poll_edit_form_poll_type_open" = "Offene Umfrage"; +"poll_edit_form_participant_toggle" = "Anzeigen, wer für welche Option gestimmt hat."; // MARK: - Welcome Experience "welcome_experience_title1" = "Willkommen beim BundesMessenger"; diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index d50de826c..33da52014 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -445,6 +445,7 @@ "poll_edit_form_poll_type" = "Poll type"; "poll_edit_form_poll_type_closed" = "Hidden Poll"; "poll_edit_form_poll_type_open" = "Open poll"; +"poll_edit_form_participant_toggle" = "Show who voted for which option"; // MARK: - Welcome Experience "welcome_experience_title1" = "Welcome to BundesMessenger"; diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index cc09adf87..206257ed6 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -967,6 +967,10 @@ public class BWIL10n: NSObject { public static var pollEditFormCreatePoll: String { return BWIL10n.tr("Bwi", "poll_edit_form_create_poll") } + /// Anzeigen, wer für welche Option gestimmt hat. + public static var pollEditFormParticipantToggle: String { + return BWIL10n.tr("Bwi", "poll_edit_form_participant_toggle") + } /// Umfragetyp public static var pollEditFormPollType: String { return BWIL10n.tr("Bwi", "poll_edit_form_poll_type") diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index dcda5b5ab..f312bd1b3 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -49,7 +49,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .editing, pollDetails: EditFormPollDetails(type: Self.pollKindKeyToDetailsType(pollContent.kind), question: pollContent.question, - answerOptions: pollContent.answerOptions.map(\.text)))) + answerOptions: pollContent.answerOptions.map(\.text), showParticipants: pollContent.showParticipants))) } else { viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default)) @@ -134,6 +134,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { return MXEventContentPollStart(question: details.question, kind: Self.pollDetailsTypeToKindKey(details.type), + showParticipants: details.showParticipants, maxSelections: NSNumber(value: details.maxSelections), answerOptions: options) } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift index 9e2171bf7..f11393b26 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift @@ -27,9 +27,10 @@ struct EditFormPollDetails { let question: String let answerOptions: [String] let maxSelections: UInt = 1 + let showParticipants: Bool static var `default`: EditFormPollDetails { - EditFormPollDetails(type: .disclosed, question: "", answerOptions: ["", ""]) + EditFormPollDetails(type: .disclosed, question: "", answerOptions: ["", ""], showParticipants: false) } } @@ -96,6 +97,7 @@ struct PollEditFormViewStateBindings { var question: PollEditFormQuestion var answerOptions: [PollEditFormAnswerOption] var type: EditFormPollType + var showParticipants: Bool var alertInfo: PollEditFormErrorAlertInfo? } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift index 43ce0dc72..a4848e57c 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift @@ -50,7 +50,8 @@ class PollEditFormViewModel: PollEditFormViewModelType, PollEditFormViewModelPro bindings: PollEditFormViewStateBindings( question: PollEditFormQuestion(text: parameters.pollDetails.question, maxLength: Constants.maxQuestionLength), answerOptions: parameters.pollDetails.answerOptions.map { PollEditFormAnswerOption(text: $0, maxLength: Constants.maxAnswerOptionLength) }, - type: parameters.pollDetails.type + type: parameters.pollDetails.type, + showParticipants: parameters.pollDetails.showParticipants ) ) @@ -100,11 +101,13 @@ class PollEditFormViewModel: PollEditFormViewModelType, PollEditFormViewModelPro // MARK: - Private private func buildPollDetails() -> EditFormPollDetails { + EditFormPollDetails(type: state.bindings.type, question: state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), answerOptions: state.bindings.answerOptions.compactMap { answerOption in let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? nil : text - }) + }, + showParticipants: state.bindings.showParticipants) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index dd6fb6252..240463030 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -34,6 +34,11 @@ struct PollEditForm: View { VStack(alignment: .leading, spacing: 32.0) { PollEditFormTypePicker(selectedType: $viewModel.type) + // bwi (#4483) Adds a boolean shoparticpants to the view models, the event and the view + if BWIBuildSettings.shared.bwiPollShowParticipantsToggle { + PollEditFormParticipationToggle(showParticipants: $viewModel.showParticipants) + } + VStack(alignment: .leading, spacing: 16.0) { Text(VectorL10n.pollEditFormPollQuestionOrTopic) .font(theme.fonts.title3SB) diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormParticipationToggle.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormParticipationToggle.swift new file mode 100644 index 000000000..a0462b35c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormParticipationToggle.swift @@ -0,0 +1,35 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 PollEditFormParticipationToggle: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + @Binding var showParticipants: Bool + + var body: some View { + Toggle(BWIL10n.pollEditFormParticipantToggle, isOn: $showParticipants) + .accessibilityIdentifier("PollEditFormParticipationToggle") + } +} + +struct PollEditFormParticipationToggle_Previews: PreviewProvider { + static var previews: some View { + PollEditFormParticipationToggle(showParticipants: .constant(true)) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index 09a8fb3c7..ea209bc75 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -40,6 +40,7 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { startDate: .init(timeIntervalSinceReferenceDate: 0), totalAnswerCount: 20, type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, + showParticipants: false, eventType: self == .closedPollEnded ? .ended : .started, maxAllowedSelections: 1, hasBeenEdited: false, diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index c98f4e136..715c37d44 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -57,6 +57,7 @@ private extension MockPollHistoryService { startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24), totalAnswerCount: 30, type: .disclosed, + showParticipants: false, eventType: .started, maxAllowedSelections: 1, hasBeenEdited: false, @@ -74,6 +75,7 @@ private extension MockPollHistoryService { startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24), totalAnswerCount: 30, type: .disclosed, + showParticipants: false, eventType: .started, maxAllowedSelections: 1, hasBeenEdited: false, diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 6ee1b0ddf..18ad338a4 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -90,6 +90,7 @@ struct PollListItem_Previews: PreviewProvider { startDate: .init(), totalAnswerCount: 30, type: .disclosed, + showParticipants: false, eventType: .started, maxAllowedSelections: 1, hasBeenEdited: false, @@ -102,6 +103,7 @@ struct PollListItem_Previews: PreviewProvider { startDate: .init(), totalAnswerCount: 30, type: .disclosed, + showParticipants: false, eventType: .started, maxAllowedSelections: 1, hasBeenEdited: false, @@ -117,6 +119,7 @@ struct PollListItem_Previews: PreviewProvider { startDate: .init(), totalAnswerCount: 30, type: .disclosed, + showParticipants: false, eventType: .started, maxAllowedSelections: 1, hasBeenEdited: false, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 3214fae65..f4f3a8a5b 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -144,6 +144,7 @@ extension TimelinePollDetails { startDate: poll.startDate, totalAnswerCount: poll.totalAnswerCount, type: poll.kind.timelinePollType, + showParticipants: poll.showParticipants, eventType: eventType, maxAllowedSelections: poll.maxAllowedSelections, hasBeenEdited: poll.hasBeenEdited, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 0ee87c55f..9d846e190 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -21,6 +21,7 @@ typealias TimelinePollViewModelCallback = (TimelinePollViewModelResult) -> Void enum TimelinePollViewAction { case selectAnswerOptionWithIdentifier(String) + case showParticipants } enum TimelinePollViewModelResult { @@ -69,6 +70,7 @@ struct TimelinePollDetails { var startDate: Date var totalAnswerCount: UInt var type: TimelinePollType + var showParticipants: Bool var eventType: TimelinePollEventType var maxAllowedSelections: UInt var hasBeenEdited: Bool diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index 8c70b21e3..9c010fe5a 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -40,6 +40,7 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { startDate: .init(), totalAnswerCount: 20, type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, + showParticipants: false, eventType: self == .closedPollEnded ? .ended : .started, maxAllowedSelections: 1, hasBeenEdited: false, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index a86862cf4..bbe304565 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -49,6 +49,8 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro } else { updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) } + case .showParticipants: + break } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index dd468b008..e16fee5af 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -161,6 +161,7 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { startDate: .init(), totalAnswerCount: 100, type: type, + showParticipants: false, eventType: .started, maxAllowedSelections: 1, hasBeenEdited: false, From 5370dded66768fd4cac9a860821dc2e99b1e933f Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Thu, 25 May 2023 14:32:54 +0000 Subject: [PATCH 115/149] MESSENGER-1678 add webview navigation policy --- .../AllChats/AllChatsViewController.swift | 11 +++------- .../Controllers/MXKWebViewViewController.m | 14 +++++++++++++ .../Modules/Settings/SettingsViewController.m | 21 +++---------------- Riot/Modules/TabBar/MasterTabBarController.m | 6 ++---- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index eccd71e12..41e6481c4 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -316,15 +316,10 @@ class AllChatsViewController: HomeViewController { alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertInfoButton, style: .default, handler: { [self] action in - if let webViewController = WebViewViewController(url: BWIBuildSettings.shared.applicationPrivacyPolicyWithMatomoSectionUrlString) { - navigationBar = UINavigationController(rootViewController: webViewController) - webViewController.navigationItem.setLeftBarButton(UIBarButtonItem(title: VectorL10n.close, style: .plain, target: self, action: #selector(self.bwiCloseModal)), animated: false) - webViewController.title = VectorL10n.settingsPrivacyPolicy - navigationBar?.presentationController?.delegate = self + if let url = URL(string: BWIBuildSettings.shared.applicationPrivacyPolicyWithMatomoSectionUrlString) { + UIApplication.shared.open(url) + } showMatomoConsentAlertOnCloseModal = true - present(navigationBar ?? webViewController, animated: true, completion: nil) - } - })) alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertCancelButton, diff --git a/Riot/Modules/MatrixKit/Controllers/MXKWebViewViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKWebViewViewController.m index 17e09796b..08189aec0 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKWebViewViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKWebViewViewController.m @@ -350,4 +350,18 @@ NSString *const kMXKWebViewViewControllerJavaScriptEnableLog = } } +#pragma mark - BWI: WebViewLinkPolicy +-(void)webView:(WKWebView *)webview decidePolicyForNavigationAction:(nonnull WKNavigationAction *)navigationAction decisionHandler:(nonnull void (^)(WKNavigationActionPolicy))decisionHandler +{ + if (navigationAction.navigationType == WKNavigationTypeLinkActivated) { + // bwi: clicked links should be opened in system browser + [[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:nil]; + decisionHandler(WKNavigationActionPolicyCancel); + } else { + // bwi: Open url in webview + decisionHandler(WKNavigationActionPolicyAllow); + } +} + + @end diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 37fe44088..7707a1cff 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -3302,21 +3302,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate> } else if (row == ABOUT_COPYRIGHT_INDEX) { - WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BWIBuildSettings.shared.applicationCopyrightUrlString]; - - webViewViewController.title = [BWIL10n settingsCopyright]; - [webViewViewController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; - - [self pushViewController:webViewViewController]; + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:BWIBuildSettings.shared.applicationCopyrightUrlString] options:@{} completionHandler:nil]; } else if (row == ABOUT_ACCEPTABLE_USE_INDEX) { - WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BuildSettings.applicationAcceptableUsePolicyUrlString]; - - webViewViewController.title = [VectorL10n settingsAcceptableUse]; - [webViewViewController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; - - [self pushViewController:webViewViewController]; + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:BuildSettings.applicationAcceptableUsePolicyUrlString] options:@{} completionHandler:nil]; } else if (row == ABOUT_SUPPORT_INDEX) { @@ -3331,12 +3321,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> } else if (row == ABOUT_PRIVACY_INDEX) { - WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BWIBuildSettings.shared.applicationPrivacyPolicyUrlString]; - - webViewViewController.title = [VectorL10n settingsPrivacyPolicy]; - [webViewViewController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; - - [self pushViewController:webViewViewController]; + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:BWIBuildSettings.shared.applicationPrivacyPolicyUrlString] options:@{} completionHandler:nil]; } else if (row == ABOUT_THIRD_PARTY_INDEX) { diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 938107f76..3cdd0bfb9 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -1105,10 +1105,8 @@ message:[BWIL10n bwiAnalyticsAlertBody:AppInfo.current.displayName] preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:BWIL10n.bwiAnalyticsAlertInfoButton style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BWIBuildSettings.shared.applicationPrivacyPolicyUrlString]; - webViewViewController.title = [VectorL10n settingsPrivacyPolicy]; - [self.navigationController pushViewController:webViewViewController animated:YES]; + [alert addAction:[UIAlertAction actionWithTitle:BWIL10n.bwiAnalyticsAlertInfoButton style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:BWIBuildSettings.shared.applicationPrivacyPolicyUrlString] options:@{} completionHandler:nil]; }]]; [alert addAction:[UIAlertAction actionWithTitle:BWIL10n.bwiAnalyticsAlertCancelButton style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { BWIAnalytics.sharedTracker.running = NO; From 338e1946f62baf70ca7c3abe3ded6a0465e674dd Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Wed, 31 May 2023 13:53:21 +0200 Subject: [PATCH 116/149] MESSENGER-4699 update element version string --- Config/BWIBuildSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index a25e779e3..3e35744de 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -490,7 +490,7 @@ class BWIBuildSettings: NSObject { var passwordIndicatorOnLogin = true // MARK: Displays the element base version on the settings screen - var elementBaseVersion = "1.10.9" + var elementBaseVersion = "1.10.11" var showElementBaseVersion = true From 5179eecaa1d44a7a921a7ae92ae0d19c261d790a Mon Sep 17 00:00:00 2001 From: Frank Rotermund Date: Wed, 31 May 2023 14:31:07 +0000 Subject: [PATCH 117/149] Feature/4383 poll participants details --- Config/BWIBuildSettings.swift | 1 + Podfile | 2 +- Riot/Assets/de.lproj/Bwi.strings | 3 + Riot/Assets/en.lproj/Bwi.strings | 3 + Riot/Generated/BWIStrings.swift | 12 ++ Riot/Modules/Room/RoomCoordinator.swift | 6 + .../Coordinator/PollEditFormCoordinator.swift | 2 +- .../MockPollHistoryDetailScreenState.swift | 6 +- .../Service/Mock/MockPollHistoryService.swift | 2 +- .../Room/PollHistory/View/PollListItem.swift | 8 +- .../Coordinator/TimelinePollCoordinator.swift | 29 ++++- .../Coordinator/TimelinePollProvider.swift | 6 +- .../TimelinePoll/TimelinePollModels.swift | 5 +- .../TimelinePollScreenState.swift | 6 +- .../TimelinePoll/TimelinePollViewModel.swift | 2 +- .../View/TimelinePollAnswerOptionButton.swift | 2 +- .../TimelinePoll/View/TimelinePollView.swift | 13 ++ .../PollParticipantDetailsCoordinator.swift | 68 ++++++++++ .../PollParticipantDetailsModels.swift | 96 ++++++++++++++ .../PollParticipantDetailsView.swift | 121 ++++++++++++++++++ .../PollParticipantDetailsViewModel.swift | 66 ++++++++++ ...lParticipantDetailsViewModelProtocol.swift | 22 ++++ 22 files changed, 462 insertions(+), 19 deletions(-) create mode 100644 bwi/PollParticipantDetails/PollParticipantDetailsCoordinator.swift create mode 100644 bwi/PollParticipantDetails/PollParticipantDetailsModels.swift create mode 100644 bwi/PollParticipantDetails/PollParticipantDetailsView.swift create mode 100644 bwi/PollParticipantDetails/PollParticipantDetailsViewModel.swift create mode 100644 bwi/PollParticipantDetails/PollParticipantDetailsViewModelProtocol.swift diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index a25e779e3..d20aea7bd 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -125,6 +125,7 @@ class BWIBuildSettings: NSObject { var bwiShowClosedPolls = true var bwiPollShowParticipantsToggle = true + var bwiPollVisibleVotes = 5 var bwiShowThreads = false var bwiShowRoomCreationSectionFooter = false diff --git a/Podfile b/Podfile index 969eed1b1..f64282478 100644 --- a/Podfile +++ b/Podfile @@ -43,7 +43,7 @@ when String # specific MatrixSDK released version $matrixSDKVersionSpec = $matrixSDKVersion end -$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.9_bwi_beta_2' } +$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.9_bwi_4383' } # Method to import the MatrixSDK def import_MatrixSDK diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index 4b0801314..05bdbf973 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -541,6 +541,9 @@ "poll_edit_form_poll_type_closed" = "Versteckte Umfrage"; "poll_edit_form_poll_type_open" = "Offene Umfrage"; "poll_edit_form_participant_toggle" = "Anzeigen, wer für welche Option gestimmt hat."; +"poll_timeline_show_participants_button" = "Stimmen anzeigen"; +"poll_participant_details_show_more" = "Alle ansehen (%lu weitere)"; +"poll_participant_details_title" = "Umfragedetails"; // MARK: - Welcome Experience "welcome_experience_title1" = "Willkommen beim BundesMessenger"; diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index 33da52014..c885cd0b7 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -446,6 +446,9 @@ "poll_edit_form_poll_type_closed" = "Hidden Poll"; "poll_edit_form_poll_type_open" = "Open poll"; "poll_edit_form_participant_toggle" = "Show who voted for which option"; +"poll_timeline_show_participants_button" = "Show votes"; +"poll_participant_details_show_more" = "Show all (%lu more)"; +"poll_participant_details_title" = "Poll details"; // MARK: - Welcome Experience "welcome_experience_title1" = "Welcome to BundesMessenger"; diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index 206257ed6..d99fc819e 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -983,6 +983,18 @@ public class BWIL10n: NSObject { public static var pollEditFormPollTypeOpen: String { return BWIL10n.tr("Bwi", "poll_edit_form_poll_type_open") } + /// Alle ansehen (%lu weitere) + public static func pollParticipantDetailsShowMore(_ p1: Int) -> String { + return BWIL10n.tr("Bwi", "poll_participant_details_show_more", p1) + } + /// Umfragedetails + public static var pollParticipantDetailsTitle: String { + return BWIL10n.tr("Bwi", "poll_participant_details_title") + } + /// Stimmen anzeigen + public static var pollTimelineShowParticipantsButton: String { + return BWIL10n.tr("Bwi", "poll_timeline_show_participants_button") + } /// Wiederholen public static var retry: String { return BWIL10n.tr("Bwi", "retry") diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index f249b863c..e018bfbe8 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -143,6 +143,12 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { navigationRouter.setRootModule(self, popCompletion: nil) } } + + // FRROT its like sleep() again -> hopefully there is a better solution. When directly calling self.navigation router here its still nil and it needs to be called this early because soon afterwards the pollCells get build + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + TimelinePollProvider.shared.navigationRouter = self.navigationRouter + } + } func start(withEventId eventId: String, completion: (() -> Void)?) { diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index f312bd1b3..3c0db09b2 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -42,7 +42,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { init(parameters: PollEditFormCoordinatorParameters) { self.parameters = parameters - + var viewModel: PollEditFormViewModel if let startEvent = parameters.pollStartEvent, let pollContent = MXEventContentPollStart(fromJSON: startEvent.content) { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index ea209bc75..2fa3ca831 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -29,9 +29,9 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { } var poll: TimelinePollDetails { - let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), - TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), - TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false, voters:[]), + TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true, voters:[]), + TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false, voters:[])] let poll = TimelinePollDetails(id: "id", question: "Question", diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 715c37d44..e97a8d3f7 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -70,7 +70,7 @@ private extension MockPollHistoryService { .map { index in TimelinePollDetails(id: "p\(index)", question: "Do you like the active poll number \(index)?", - answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)], + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true, voters: [])], closed: true, startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24), totalAnswerCount: 30, diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 18ad338a4..96b0278ed 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -85,7 +85,7 @@ struct PollListItem_Previews: PreviewProvider { Group { let pollData1 = TimelinePollDetails(id: UUID().uuidString, question: "Do you like polls?", - answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true, voters:[])], closed: true, startDate: .init(), totalAnswerCount: 30, @@ -98,7 +98,7 @@ struct PollListItem_Previews: PreviewProvider { let pollData2 = TimelinePollDetails(id: UUID().uuidString, question: "Do you like polls?", - answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true, voters:[])], closed: false, startDate: .init(), totalAnswerCount: 30, @@ -112,8 +112,8 @@ struct PollListItem_Previews: PreviewProvider { let pollData3 = TimelinePollDetails(id: UUID().uuidString, question: "Do you like polls?", answerOptions: [ - .init(id: "id1", text: "Yes, of course!", count: 15, winner: true, selected: true), - .init(id: "id2", text: "No, I don't :-(", count: 15, winner: true, selected: true) + .init(id: "id1", text: "Yes, of course!", count: 15, winner: true, selected: true, voters:[]), + .init(id: "id2", text: "No, I don't :-(", count: 15, winner: true, selected: true, voters:[]) ], closed: true, startDate: .init(), diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index f4f3a8a5b..94fe5b28a 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -29,6 +29,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: Private + private let navigationRouter: NavigationRouterType? private let parameters: TimelinePollCoordinatorParameters private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() @@ -43,9 +44,12 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: - Setup - init(parameters: TimelinePollCoordinatorParameters) throws { + // FRROT show participants needs a navigation router as it is a button click that creates a new View + init(parameters: TimelinePollCoordinatorParameters, navigationRouter: NavigationRouterType? = nil) throws { self.parameters = parameters + self.navigationRouter = navigationRouter + try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent) pollAggregator.delegate = self @@ -56,6 +60,9 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel switch result { case .selectedAnswerOptionsWithIdentifiers(let identifiers): self.selectedAnswerIdentifiersSubject.send(identifiers) + case .showParticipants: + self.showParticipantsView() + } } @@ -105,6 +112,23 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } } + func showParticipantsView() { + if let navigationRouter = navigationRouter { + let parameters = PollParticipantDetailsCoordinatorParameters(room: parameters.room, poll: pollAggregator.poll) + let coordinator = PollParticipantDetailsCoordinator(parameters: parameters) + + add(childCoordinator: coordinator) + + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(coordinator, animated: true, popCompletion: nil) + } else { + navigationRouter.setRootModule(coordinator, popCompletion: nil) + } + + coordinator.start() + } + } + // MARK: - PollAggregatorDelegate func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { @@ -134,7 +158,8 @@ extension TimelinePollDetails { text: pollAnswerOption.text, count: pollAnswerOption.count, winner: pollAnswerOption.isWinner, - selected: pollAnswerOption.isCurrentUserSelection) + selected: pollAnswerOption.isCurrentUserSelection, + voters:pollAnswerOption.voters) } self.init(id: poll.id, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index 0c7233298..43db0c9e6 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -31,11 +31,15 @@ class TimelinePollProvider: NSObject { } } } + + var navigationRouter: NavigationRouterType? = nil + var coordinatorsForEventIdentifiers = [String: TimelinePollCoordinator]() /// Create or retrieve the poll timeline coordinator for this event and return /// a view to be displayed in the timeline func buildTimelinePollVCForEvent(_ event: MXEvent) -> UIViewController? { + guard let session = session, let room = session.room(withRoomId: event.roomId) else { return nil } @@ -45,7 +49,7 @@ class TimelinePollProvider: NSObject { } let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollEvent: event) - guard let coordinator = try? TimelinePollCoordinator(parameters: parameters) else { + guard let coordinator = try? TimelinePollCoordinator(parameters: parameters, navigationRouter: navigationRouter ) else { return messageViewController(for: event) } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 9d846e190..aaf3e5d67 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -26,6 +26,7 @@ enum TimelinePollViewAction { enum TimelinePollViewModelResult { case selectedAnswerOptionsWithIdentifiers([String]) + case showParticipants } enum TimelinePollType { @@ -44,13 +45,15 @@ struct TimelinePollAnswerOption: Identifiable { var count: UInt var winner: Bool var selected: Bool + var voters: [MXEvent] - init(id: String, text: String, count: UInt, winner: Bool, selected: Bool) { + init(id: String, text: String, count: UInt, winner: Bool, selected: Bool, voters: [MXEvent]) { self.id = id self.text = text self.count = count self.winner = winner self.selected = selected + self.voters = voters } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index 9c010fe5a..f5910edf8 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -29,9 +29,9 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), - TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), - TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false, voters:[]), + TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true, voters:[]), + TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false, voters:[])] let poll = TimelinePollDetails(id: "id", question: "Question", diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index bbe304565..0a0aa6a6d 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -50,7 +50,7 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) } case .showParticipants: - break + completion?(.showParticipants) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index e16fee5af..958df2099 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -169,6 +169,6 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { } static func buildAnswerOption(text: String = "Test", selected: Bool, winner: Bool = false) -> TimelinePollAnswerOption { - TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected) + TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected, voters:[]) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index 2109a0e8a..358e5752d 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -58,6 +58,19 @@ struct TimelinePollView: View { .lineLimit(2) .font(theme.fonts.footnote) .foregroundColor(theme.colors.tertiaryContent) + + if poll.showParticipants && (poll.type == .undisclosed || poll.closed) { + Button(action: { + viewModel.send(viewAction:.showParticipants) + }) + { + Text(BWIL10n.pollTimelineShowParticipantsButton) + .font(theme.fonts.body) + .bold() + .foregroundColor(theme.colors.accent) + } + } + } .padding([.horizontal, .top], 2.0) .padding([.bottom]) diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsCoordinator.swift b/bwi/PollParticipantDetails/PollParticipantDetailsCoordinator.swift new file mode 100644 index 000000000..efe8aa1d6 --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsCoordinator.swift @@ -0,0 +1,68 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 +import UIKit + +struct PollParticipantDetailsCoordinatorParameters { + let room: MXRoom + let poll: PollProtocol +} + +final class PollParticipantDetailsCoordinator: Coordinator, Presentable { + + // MARK: Private + + private let parameters: PollParticipantDetailsCoordinatorParameters + private let pollParticipantDetailsHostingController: UIViewController + private var pollParticipantDetailsViewModel: PollParticipantDetailsViewModelProtocol + + // MARK: Public + + var childCoordinators: [Coordinator] = [] + + var completion: (() -> Void)? + + // MARK: - Setup + + init(parameters: PollParticipantDetailsCoordinatorParameters) { + self.parameters = parameters + + var viewModel: PollParticipantDetailsViewModel + + viewModel = PollParticipantDetailsViewModel.init(parameters: PollParticipantDetailsViewModelParameters(poll: parameters.poll, room: parameters.room)) + + let view = PollParticipantDetailsView(viewModel: viewModel.context) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.room.mxSession.mediaManager))) + + pollParticipantDetailsViewModel = viewModel + pollParticipantDetailsHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + + func start() { + + } + + // MARK: - Presentable + + func toPresentable() -> UIViewController { + pollParticipantDetailsHostingController + } +} diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsModels.swift b/bwi/PollParticipantDetails/PollParticipantDetailsModels.swift new file mode 100644 index 000000000..2a8f11599 --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsModels.swift @@ -0,0 +1,96 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 + +struct PollParticipantDetailsViewState: BindableState { + var answers: [PollParticipantAnswer] = [] + var poll: PollParticipantPoll +} + +enum PollParticipantDetailsMode { + case someParticipants + case allParticipants +} + +enum PollParticipantDetailsViewAction { + case openAllParticipants(index: Int) + case closeAllParticipants(index: Int) +} + +struct PollParticipantVoter: Identifiable, BindableState { + var id: String { + displayName + } + + var displayName: String + var userAvatarData: AvatarInputProtocol + var formattedVotingTime: String + + static func buildPollParticipantVoter( event: MXEvent, room: MXRoom) -> PollParticipantVoter? { + if let user = room.mxSession.user(withUserId: event.sender) { + let avatarData = AvatarInput(mxContentUri: user.avatarUrl, matrixItemId: event.sender, displayName: user.displayname) + + let votingTime = Date(timeIntervalSince1970: TimeInterval(event.originServerTs / 1000)) + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone.current + dateFormatter.calendar = Calendar.current + dateFormatter.dateFormat = "dd. MMMM, yyyy HH:mm" + let strDate = dateFormatter.string(from: votingTime) + + return PollParticipantVoter(displayName: user.displayname, userAvatarData: avatarData, formattedVotingTime: strDate) + } else { + return nil + } + } +} + +struct PollParticipantAnswer: Identifiable, BindableState { + var id: String { + originalId + } + + var name: String + var votes: Int + var visibleVotes: Int + var votesText: String + var originalId: String + var voters: [PollParticipantVoter] + var expanded: Bool = false + + static func buildPollParticipantAnswer( answerOption: PollAnswerOptionProtocol, parameters: PollParticipantDetailsViewModelParameters) -> PollParticipantAnswer { + var voters: [PollParticipantVoter] = [] + for participantEvent in answerOption.voters { + if let voter = PollParticipantVoter.buildPollParticipantVoter(event: participantEvent, room: parameters.room) { + voters.append(voter) + } + } + + let votesText = answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count)) + return PollParticipantAnswer.init( name: answerOption.text, + votes: Int(answerOption.count), + visibleVotes: min(Int(answerOption.count), BWIBuildSettings.shared.bwiPollVisibleVotes), + votesText: votesText, + originalId: answerOption.id, + voters: voters) + } +} + +struct PollParticipantPoll : BindableState { + var name: String + let voterRows: Int = 2 +} diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsView.swift b/bwi/PollParticipantDetails/PollParticipantDetailsView.swift new file mode 100644 index 000000000..70bc5372f --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsView.swift @@ -0,0 +1,121 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 + +struct PollParticipantDetailsView: View { + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: PollParticipantDetailsViewModel.Context + + var body: some View { + NavigationView { + VStack { + PollParticipantPollHeaderView(poll: viewModel.viewState.poll) + + List { + ForEach(Array(viewModel.viewState.answers.enumerated()), id: \.offset) { index, answer in + SwiftUI.Section(header: PollParticipantSectionHeaderView(answer: answer)) { + if answer.votes > 0 { + VStack { + ForEach(answer.voters.prefix(upTo: answer.visibleVotes)) { voter in + PollParticipantVoterView(voter: voter) + } + + if answer.votes > answer.visibleVotes && !answer.expanded { + Button(action: { onExpandButton(index: index) }) { + Text(BWIL10n.pollParticipantDetailsShowMore(Int(answer.votes-answer.visibleVotes))) + } + } + } + } + } + } + } + .listStyle(.grouped) + } + } + .accentColor(theme.colors.accent) + .navigationViewStyle(StackNavigationViewStyle()) + .navigationTitle(BWIL10n.pollParticipantDetailsTitle) + .navigationBarTitleDisplayMode(.inline) + } + + private func onExpandButton(index: Int) { + viewModel.send(viewAction: .openAllParticipants(index: index)) + } +} + +struct PollParticipantVoterView: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var voter: PollParticipantVoter + + var body: some View { + HStack(alignment: .center, spacing: 10) { + AvatarImage(avatarData: voter.userAvatarData, size: .medium) + .border() + + VStack(alignment: .leading, spacing: 3) { + Text(voter.displayName) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + Text(voter.formattedVotingTime) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + } + } + } +} + +struct PollParticipantPollHeaderView: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var poll: PollParticipantPoll + + var body: some View { + Text(poll.name) + .font(theme.fonts.headline) + .foregroundColor(theme.colors.primaryContent) + } +} + +struct PollParticipantSectionHeaderView: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var answer: PollParticipantAnswer + + var body: some View { + HStack(alignment: .center) { + Text(answer.name) + .font(theme.fonts.headline) + .foregroundColor(theme.colors.primaryContent) + Spacer() + Text(answer.votesText) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .accessibilityIdentifier("PollAnswerOptionCount") + } + } +} diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsViewModel.swift b/bwi/PollParticipantDetails/PollParticipantDetailsViewModel.swift new file mode 100644 index 000000000..e9b56e62f --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsViewModel.swift @@ -0,0 +1,66 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 +import SwiftUI + +struct PollParticipantDetailsViewModelParameters { + let poll: PollProtocol + let room: MXRoom +} + +typealias PollParticipantDetailsViewModelType = StateStoreViewModel + +class PollParticipantDetailsViewModel: PollParticipantDetailsViewModelType, PollParticipantDetailsViewModelProtocol { + + init(parameters: PollParticipantDetailsViewModelParameters) { + var state = PollParticipantDetailsViewState(poll: PollParticipantPoll(name: parameters.poll.text)) + + var answers: [PollParticipantAnswer] = [] + + let room = parameters.room + + for answerOption in parameters.poll.answerOptions { + + let answer = PollParticipantAnswer.buildPollParticipantAnswer(answerOption: answerOption, parameters: parameters) + answers.append(answer) + } + answers.sort { + $0.votes > $1.votes + } + + state.answers = answers + + super.init(initialViewState: state) + } + + // MARK: - Public + + override func process(viewAction: PollParticipantDetailsViewAction) { + switch viewAction { + + case .openAllParticipants(index: let index): + state.answers[index].expanded = true + state.answers[index].visibleVotes = state.answers[index].votes + case .closeAllParticipants(index: let index): + state.answers[index].expanded = false + state.answers[index].visibleVotes = min(state.answers[index].votes, state.poll.voterRows) + } + } +} diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsViewModelProtocol.swift b/bwi/PollParticipantDetails/PollParticipantDetailsViewModelProtocol.swift new file mode 100644 index 000000000..4511d979b --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsViewModelProtocol.swift @@ -0,0 +1,22 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 PollParticipantDetailsViewModelProtocol { + +} From 4fc5e366e3e7f022d75a87e61f4d7ff7edd82f4e Mon Sep 17 00:00:00 2001 From: Arnfried Griesert Date: Mon, 12 Jun 2023 06:08:33 +0200 Subject: [PATCH 118/149] Fixed additional merge issues --- Config/BWIBuildSettings.swift | 3 ++ Riot/Modules/Application/LegacyAppDelegate.m | 19 +++++------- .../AuthenticationCoordinator.swift | 3 +- .../LegacyAuthenticationCoordinator.swift | 3 +- .../EnterPinCodeViewController.swift | 2 ++ .../Modules/Settings/SettingsViewController.m | 1 - .../Coordinator/TimelinePollCoordinator.swift | 30 +++++++++++++++++-- 7 files changed, 42 insertions(+), 19 deletions(-) diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index e527d66bd..04671e235 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -621,4 +621,7 @@ class BWIBuildSettings: NSObject { // MARK: Sessions Manager var enableNewSessionManagerByDefault = false + + // MARK: Voice Broadcast + var enableLabFeatureVoiceBroadcasts = false } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 100c69c31..9891eead1 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2562,6 +2562,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { MXLogDebug(@"[AppDelegate] showLaunchAnimation"); + /* bwi: 4782 removed by nv UIView *launchLoadingView; if (MXSDKOptions.sharedInstance.enableStartupProgress) { @@ -2569,15 +2570,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { launchLoadingView = [BUMLaunchLoadingViewController makeView]; } else { - if (MXSDKOptions.sharedInstance.enableStartupProgress) - { - MXSession *mainSession = self.mxSessions.firstObject; - launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; - } - else - { - launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:nil]; - } + MXSession *mainSession = self.mxSessions.firstObject; + launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; [(LaunchLoadingView *) launchLoadingView updateWithTheme:ThemeService.shared.theme]; } @@ -2586,7 +2580,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [window addSubview:launchLoadingView]; } - + */ + + /* bwi: 4782 - new code from nv MXSession *mainSession = self.mxSessions.firstObject; LaunchLoadingView *launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress]; @@ -2596,8 +2592,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [window addSubview:launchLoadingView]; - launchAnimationContainerView = launchLoadingView; + launchAnimationContainerView = launchLoadingView; + */ [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameStartupLaunchScreen]; } } diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index 0ceed0374..cb10aacfd 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -635,8 +635,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc loadingViewController.modalPresentationStyle = .fullScreen navigationRouter.setRootModule(loadingViewController) } else { - let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil - let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) + let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift index b0999be31..e0c9a51b4 100644 --- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift @@ -121,8 +121,7 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator loadingViewController.modalPresentationStyle = .fullScreen navigationRouter.setRootModule(loadingViewController) } else { - let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil - let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress) + let loadingViewController = LaunchLoadingViewController(startupProgress: session?.startupProgress) loadingViewController.modalPresentationStyle = .fullScreen // Replace the navigation stack with the loading animation diff --git a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift index e9e24480d..6c6c05569 100644 --- a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift +++ b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift @@ -88,7 +88,9 @@ final class EnterPinCodeViewController: UIViewController { inactiveLogoImageView.isHidden = true } // BWI: accessibility description + /* bwi: 4782 digitButtonReset.vc_setupAccessibilityTraitsButton(withTitle: BWIL10n.pinProtectionResetButtonAccessibilityLabel, hint: BWIL10n.pinProtectionResetButtonAccessibilityHint, isEnabled: true) + */ } override func viewWillAppear(_ animated: Bool) { diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 7c43a0dc3..ff086336d 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -755,7 +755,6 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { [sectionLabs addRowWithTag:LABS_ENABLE_LIVE_LOCATION_SHARING]; } - */ [sectionLabs addRowWithTag:LABS_ENABLE_NEW_SESSION_MANAGER]; /* bwi: disabled for our apps [sectionLabs addRowWithTag:LABS_ENABLE_NEW_CLIENT_INFO_FEATURE]; diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index e2202524b..b033661dc 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -29,6 +29,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: Private + private let navigationRouter: NavigationRouterType? private let parameters: TimelinePollCoordinatorParameters private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() @@ -43,10 +44,11 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: - Setup - init(parameters: TimelinePollCoordinatorParameters) throws { + init(parameters: TimelinePollCoordinatorParameters, navigationRouter: NavigationRouterType? = nil) throws { self.parameters = parameters - viewModel = TimelinePollViewModel(timelinePollDetailsState: .loading) + self.navigationRouter = navigationRouter + viewModel = TimelinePollViewModel(timelinePollDetailsState: .loading) try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent, delegate: self) viewModel.completion = { [weak self] result in @@ -55,6 +57,9 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel switch result { case .selectedAnswerOptionsWithIdentifiers(let identifiers): self.selectedAnswerIdentifiersSubject.send(identifiers) + case .showParticipants: + self.showParticipantsView() + } } @@ -104,6 +109,23 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } } + func showParticipantsView() { + if let navigationRouter = navigationRouter, let poll = pollAggregator.poll { + let parameters = PollParticipantDetailsCoordinatorParameters(room: parameters.room, poll: poll) + let coordinator = PollParticipantDetailsCoordinator(parameters: parameters) + + add(childCoordinator: coordinator) + + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(coordinator, animated: true, popCompletion: nil) + } else { + navigationRouter.setRootModule(coordinator, popCompletion: nil) + } + + coordinator.start() + } + } + // MARK: - PollAggregatorDelegate func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { @@ -142,7 +164,8 @@ extension TimelinePollDetails { text: pollAnswerOption.text, count: pollAnswerOption.count, winner: pollAnswerOption.isWinner, - selected: pollAnswerOption.isCurrentUserSelection) + selected: pollAnswerOption.isCurrentUserSelection, + voters:pollAnswerOption.voters) } self.init(id: poll.id, @@ -152,6 +175,7 @@ extension TimelinePollDetails { startDate: poll.startDate, totalAnswerCount: poll.totalAnswerCount, type: poll.kind.timelinePollType, + showParticipants: poll.showParticipants, eventType: eventType, maxAllowedSelections: poll.maxAllowedSelections, hasBeenEdited: poll.hasBeenEdited, From 1a415fd42c03d8ca61eb0f0f0263d59956c623c9 Mon Sep 17 00:00:00 2001 From: Arnfried Griesert Date: Mon, 12 Jun 2023 06:10:31 +0200 Subject: [PATCH 119/149] Removed CryptoSdk lab feature file --- Riot/Experiments/CryptoSDKFeature.swift | 120 ------------------------ 1 file changed, 120 deletions(-) delete mode 100644 Riot/Experiments/CryptoSDKFeature.swift diff --git a/Riot/Experiments/CryptoSDKFeature.swift b/Riot/Experiments/CryptoSDKFeature.swift deleted file mode 100644 index 1e151fe79..000000000 --- a/Riot/Experiments/CryptoSDKFeature.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// Copyright 2023 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 MatrixSDKCrypto - -/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status -/// of `CryptoSDK`, and which uses feature flags to control rollout availability. -/// -/// The implementation uses both remote and local feature flags to control the availability of `CryptoSDK`. -/// Whilst remote is more convenient in that it allows changes to the rollout without new app releases, -/// it is not available to all users because it requires data tracking user consent. Remote therefore -/// represents the safer, albeit limited rollout strategy, whereas the local feature flags allows eventually -/// targetting all users, but each target change requires new app release. -/// -/// Additionally users can manually enable this feature from the settings if they are not already in the -/// feature group. -@objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature { - @objc static let shared = CryptoSDKFeature() - - var isEnabled: Bool { - BWIBuildSettings.shared.useRustEncryption - } - - var needsVerificationUpgrade: Bool { - get { - return RiotSettings.shared.showVerificationUpgradeAlert - } - set { - RiotSettings.shared.showVerificationUpgradeAlert = newValue - } - } - - private static let FeatureName = "ios-crypto-sdk" - private static let FeatureNameV2 = "ios-crypto-sdk-v2" - - private let remoteFeature: RemoteFeaturesClientProtocol - private let localFeature: PhasedRolloutFeature - - init( - remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared, - localTargetPercentage: Double = 1 - ) { - var targetPercentage = 0.0 - if BWIBuildSettings.shared.useRustEncryption { - targetPercentage = 1.0 - } - self.remoteFeature = remoteFeature - self.localFeature = PhasedRolloutFeature( - name: Self.FeatureName, - targetPercentage: targetPercentage - ) - } - - func enable() { - RiotSettings.shared.enableCryptoSDK = true - Analytics.shared.trackCryptoSDKEnabled() - - MXLog.debug("[CryptoSDKFeature] Crypto SDK enabled") - } - - func enableIfAvailable(forUserId userId: String!) { - guard !isEnabled else { - MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is already enabled") - return - } - - guard let userId else { - MXLog.failure("[CryptoSDKFeature] enableIfAvailable: Missing user id") - return - } - - guard isFeatureEnabled(userId: userId) else { - MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is currently not available for this user") - return - } - - MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature has become available for this user and will be enabled") - enable() - } - - @objc func canManuallyEnable(forUserId userId: String!) -> Bool { - guard let userId else { - MXLog.failure("[CryptoSDKFeature] canManuallyEnable: Missing user id") - return false - } - - // User can manually enable only if not already within the automatic feature group - return !isFeatureEnabled(userId: userId) - } - - @objc func reset() { - RiotSettings.shared.enableCryptoSDK = false - MXLog.debug("[CryptoSDKFeature] Crypto SDK disabled") - } - - private func isFeatureEnabled(userId: String) -> Bool { - // This feature includes app version with a bug, and thus will not be rolled out to 100% users - remoteFeature.isFeatureEnabled(Self.FeatureName) - - // Second version of the remote feature with a bugfix and released eventually to 100% users - || remoteFeature.isFeatureEnabled(Self.FeatureNameV2) - - // Local feature - || localFeature.isEnabled(userId: userId) - } -} From c2b4d7081bc04e8b934ae2f44604627d3e2bc455 Mon Sep 17 00:00:00 2001 From: Arnfried Griesert Date: Mon, 12 Jun 2023 06:48:41 +0200 Subject: [PATCH 120/149] Changed sdk version in Podfile2 --- Podfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile b/Podfile index d2b805bb3..f54632804 100644 --- a/Podfile +++ b/Podfile @@ -43,7 +43,7 @@ when String # specific MatrixSDK released version $matrixSDKVersionSpec = $matrixSDKVersion end -$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.9_bwi_4383' } +$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.10_bwi_beta' } # Method to import the MatrixSDK def import_MatrixSDK From 62548a16f2e61e110acd89dfe9426ea6ddb92685 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Mon, 12 Jun 2023 14:14:30 +0200 Subject: [PATCH 121/149] MESSENGER-4748 fix permalink qr code scan --- bwi/QRCode/PermalinkQRCodeScanner.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bwi/QRCode/PermalinkQRCodeScanner.swift b/bwi/QRCode/PermalinkQRCodeScanner.swift index df3945181..ef99bf4f9 100644 --- a/bwi/QRCode/PermalinkQRCodeScanner.swift +++ b/bwi/QRCode/PermalinkQRCodeScanner.swift @@ -71,7 +71,7 @@ struct PermalinkQRCodeScanner: View { if !BWIBuildSettings.shared.clientPermalinkBaseUrl.isEmpty && qrCode.hasPrefix(BWIBuildSettings.shared.clientPermalinkBaseUrl) { presentationMode.wrappedValue.dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - if let url = URL(string: qrCode) { + if let url = NSURLComponents(string: qrCode)?.url { AppDelegate.theDelegate().handleUniversalLinkURL(url) } } From df2d409fc1f96cffa737462eb1da1489de6ae7a9 Mon Sep 17 00:00:00 2001 From: Frank Rotermund Date: Wed, 14 Jun 2023 07:45:18 +0200 Subject: [PATCH 122/149] MESSENGER-4383 fix for undisclosed button visibility and small UI fixes --- .../Modules/Room/TimelinePoll/View/TimelinePollView.swift | 3 ++- bwi/PollParticipantDetails/PollParticipantDetailsView.swift | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index db37651bb..dd69cc961 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -74,7 +74,8 @@ struct TimelinePollView: View { .font(theme.fonts.footnote) .foregroundColor(theme.colors.tertiaryContent) - if poll.showParticipants && (poll.type == .undisclosed || poll.closed) { + if poll.showParticipants && + (poll.type == TimelinePollType.disclosed || poll.closed) { Button(action: { viewModel.send(viewAction:.showParticipants) }) diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsView.swift b/bwi/PollParticipantDetails/PollParticipantDetailsView.swift index 70bc5372f..7b9bb6c4a 100644 --- a/bwi/PollParticipantDetails/PollParticipantDetailsView.swift +++ b/bwi/PollParticipantDetails/PollParticipantDetailsView.swift @@ -33,6 +33,7 @@ struct PollParticipantDetailsView: View { NavigationView { VStack { PollParticipantPollHeaderView(poll: viewModel.viewState.poll) + .padding(.top, 10) List { ForEach(Array(viewModel.viewState.answers.enumerated()), id: \.offset) { index, answer in @@ -46,7 +47,7 @@ struct PollParticipantDetailsView: View { if answer.votes > answer.visibleVotes && !answer.expanded { Button(action: { onExpandButton(index: index) }) { Text(BWIL10n.pollParticipantDetailsShowMore(Int(answer.votes-answer.visibleVotes))) - } + } } } } @@ -96,7 +97,7 @@ struct PollParticipantPollHeaderView: View { var body: some View { Text(poll.name) - .font(theme.fonts.headline) + .font(theme.fonts.title3) .foregroundColor(theme.colors.primaryContent) } } From 208ad9f3d15adf564474c4ce7a510976e9fa0b0f Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Wed, 14 Jun 2023 05:54:29 +0000 Subject: [PATCH 123/149] MESSENGER-4736 text changes --- Riot/Assets/de.lproj/Bwi.strings | 2 +- Riot/Assets/en.lproj/Bwi.strings | 2 +- Riot/Generated/BWIStrings.swift | 2 +- .../RoomCreationIntro/RoomCreationIntroCellContentView.swift | 4 +--- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index 05bdbf973..e282f89a7 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -387,7 +387,7 @@ // Mark: - Room Creation -"room_intro_cell_information_dm_sentence1_part1" = "Das ist der Anfang deiner Direktnachricht mit "; +"room_intro_cell_information_dm_sentence1_part1" = "Dies ist der Beginn deiner Direktnachrichten mit "; "room_avatar_view_accessibility_hint" = "Raumbild ändern"; "room_avatar_view_accessibility_label" = "Profilbild"; "room_details_permalink" = "Link zum Raum kopieren"; diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index c885cd0b7..2fe98d8ad 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -309,7 +309,7 @@ // Mark: - Room Creation -"room_intro_cell_information_dm_sentence1_part1" = "This is the beginning of your conversation with "; +"room_intro_cell_information_dm_sentence1_part1" = "This is the beginning of your direct message history with "; // MARK: - Notification Times diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index d99fc819e..85319c276 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -1083,7 +1083,7 @@ public class BWIL10n: NSObject { public static var roomEventActionRemovePoll: String { return BWIL10n.tr("Bwi", "room_event_action_remove_poll") } - /// Das ist der Anfang deiner Direktnachricht mit + /// Dies ist der Beginn deiner Direktnachrichten mit public static var roomIntroCellInformationDmSentence1Part1: String { return BWIL10n.tr("Bwi", "room_intro_cell_information_dm_sentence1_part1") } diff --git a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift index 83a073cdb..8747c3e92 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift +++ b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift @@ -184,9 +184,7 @@ final class RoomCreationIntroCellContentView: UIView, NibLoadable, Themable { attributedString.append(firstSentencePart2) attributedString.append(firstSentencePart3) - if isDirect { - attributedString.append(NSAttributedString(string: VectorL10n.roomIntroCellInformationDmSentence2, attributes: informationTextDefaultAttributes)) - } else { + if !isDirect { attributedString.append(NSAttributedString(string: VectorL10n.roomIntroCellInformationMultipleDmSentence2, attributes: informationTextDefaultAttributes)) } From 273017eafa4e26e29c9e16f71bcdfd5ac3751dfd Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Wed, 14 Jun 2023 06:14:17 +0000 Subject: [PATCH 124/149] Feature/4772 add accessibility declaration --- Config/BWIBuildSettings.swift | 7 +++ Riot/Assets/de.lproj/Bwi.strings | 3 + Riot/Assets/en.lproj/Bwi.strings | 2 + Riot/Generated/BWIStrings.swift | 4 ++ .../Modules/Settings/SettingsViewController.m | 27 +++++++- .../Login/AuthenticationLoginModels.swift | 6 ++ .../Login/AuthenticationLoginViewModel.swift | 2 + .../AuthenticationLoginCoordinator.swift | 9 +++ .../View/AuthenticationLoginScreen.swift | 20 ++++++ .../AccessibilityDeclarationView.swift | 46 ++++++++++++++ bwi/MarkDown/MarkDownView.swift | 62 +++++++++++++++++++ 11 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 bwi/AccessibilityDeclaration/AccessibilityDeclarationView.swift create mode 100644 bwi/MarkDown/MarkDownView.swift diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index 04671e235..6bef5e637 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -622,6 +622,13 @@ class BWIBuildSettings: NSObject { // MARK: Sessions Manager var enableNewSessionManagerByDefault = false + // MARK: Accessibility declaration + // bwi flag for showing accessibility declaration on login screen and in settings + var bwiShowAccessibilityDeclaration = false + // internal markdown file for accessibility declaration in en and de. + var accessibilityDeclarationFileDe = "" + var accessibilityDeclarationFileEn = "" + // MARK: Voice Broadcast var enableLabFeatureVoiceBroadcasts = false } diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index e282f89a7..40345b21f 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -620,3 +620,6 @@ // MARK: - Voice Over "textfield_reveal_secret" = "Texteingabe anzeigen"; "textfield_hide_secret" = "Texteingabe verbergen"; + +// MARK: - Accessibility declaration +"bwi_accessibility_declaration_button_title" = "Barrierefreiheitserklärung"; diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index 2fe98d8ad..f7ace9edb 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -531,3 +531,5 @@ "textfield_reveal_secret" = "reveal text input"; "textfield_hide_secret" = "hide text input"; +// MARK: - Accessibility declaration +"bwi_accessibility_declaration_button_title" = "Accessibility declaration"; diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index 85319c276..5d67dcb14 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -99,6 +99,10 @@ public class BWIL10n: NSObject { public static var bumAutheticationTitle: String { return BWIL10n.tr("Bwi", "bum_authetication_title") } + /// Barrierefreiheitserklärung + public static var bwiAccessibilityDeclarationButtonTitle: String { + return BWIL10n.tr("Bwi", "bwi_accessibility_declaration_button_title") + } /// Wir brauchen Deine Hilfe, um Fehler im %@ besser analysieren zu können. Dazu würden wir gerne anonymisierte Diagnosedaten erfassen. Es werden keine Daten an Dritte übermittelt. Details findest Du in der Datenschutzerklärung.\n\nFalls Du nicht mehr mithelfen möchtest, kannst Du dies in den Einstellungen jederzeit wieder deaktivieren.\n\nMöchtest du bei der Fehler-Analyse unterstützen? public static func bwiAnalyticsAlertBody(_ p1: String) -> String { return BWIL10n.tr("Bwi", "bwi_analytics_alert_body", p1) diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index ff086336d..404b8a217 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -216,7 +216,8 @@ typedef NS_ENUM(NSUInteger, ABOUT) ABOUT_MARK_ALL_AS_READ_INDEX, ABOUT_CLEAR_CACHE_INDEX, ABOUT_REPORT_BUG_INDEX, - ABOUT_NETIQUETTE_INDEX + ABOUT_NETIQUETTE_INDEX, + ABOUT_ACCESSIBILITY_DECLARATION_INDEX }; typedef NS_ENUM(NSUInteger, LABS_ENABLE) @@ -725,6 +726,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { [sectionAbout addRowWithTag:ABOUT_PRIVACY_INDEX]; } + // bwi 4772 - show accessibility declaration + if (BWIBuildSettings.shared.bwiShowAccessibilityDeclaration) { + [sectionAbout addRowWithTag:ABOUT_ACCESSIBILITY_DECLARATION_INDEX]; + } [sectionAbout addRowWithTag:ABOUT_THIRD_PARTY_INDEX]; sectionAbout.headerTitle = VectorL10n.settingsAbout; @@ -2837,6 +2842,16 @@ ChangePasswordCoordinatorBridgePresenterDelegate> cell = privacyPolicyCell; } + else if (row == ABOUT_ACCESSIBILITY_DECLARATION_INDEX) + { + MXKTableViewCell *accessibilityDeclarationCell = [self getDefaultTableViewCell:tableView]; + + accessibilityDeclarationCell.textLabel.text = [BWIL10n bwiAccessibilityDeclarationButtonTitle]; + + [accessibilityDeclarationCell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; + + cell = accessibilityDeclarationCell; + } else if (row == ABOUT_THIRD_PARTY_INDEX) { MXKTableViewCell *thirdPartyCell = [self getDefaultTableViewCell:tableView]; @@ -3305,6 +3320,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:BWIBuildSettings.shared.applicationPrivacyPolicyUrlString] options:@{} completionHandler:nil]; } + else if (row == ABOUT_ACCESSIBILITY_DECLARATION_INDEX) + { + [self showAccessibilityDeclaration]; + } else if (row == ABOUT_THIRD_PARTY_INDEX) { NSString *htmlFile = [[NSBundle mainBundle] pathForResource:@"third_party_licenses" ofType:@"html" inDirectory:nil]; @@ -4493,6 +4512,12 @@ ChangePasswordCoordinatorBridgePresenterDelegate> UIViewController *developerSettingsViewController = [DeveloperSettingsViewController makeViewControllerWithSession:self.mainSession]; [self pushViewController:developerSettingsViewController]; } + +- (void)showAccessibilityDeclaration +{ + UIViewController *accessibilityDeclarationViewController = [AccessibilityDeclarationViewController makeViewController]; + [self pushViewController:accessibilityDeclarationViewController]; +} - (void)showPersonalStateSettings { diff --git a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift index dac56608d..356bd5fd3 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift @@ -35,6 +35,8 @@ enum AuthenticationLoginViewModelResult: CustomStringConvertible { case qrLogin /// bwi: register info case register + /// bwi #4772: accessibility declaration + case accessibilityDeclaration /// A string representation of the result, ignoring any associated values that could leak PII. var description: String { @@ -55,6 +57,8 @@ enum AuthenticationLoginViewModelResult: CustomStringConvertible { return "qrLogin" case .register: return "register" + case .accessibilityDeclaration: + return "accessibilityDeclaration" } } } @@ -118,6 +122,8 @@ enum AuthenticationLoginViewAction { case qrLogin /// bwi: register info case register + /// bwi #4772: accessibility declaration + case accessibilityDeclaration } enum AuthenticationLoginErrorType: Hashable { diff --git a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift index 64ee83363..07879621c 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift @@ -54,6 +54,8 @@ class AuthenticationLoginViewModel: AuthenticationLoginViewModelType, Authentica Task { await callback?(.qrLogin) } case .register: Task { await callback?(.register) } + case .accessibilityDeclaration: + Task { await callback?(.accessibilityDeclaration) } } } diff --git a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift index f71f6c0c9..df7f688f4 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift @@ -140,6 +140,8 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable { case .register: // bwi: show info alert self.authenticationLoginViewModel.displayInfoAlert(.register) + case .accessibilityDeclaration: + self.showAccessibilityDeclaration() } } } @@ -325,6 +327,13 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable { } } + /// bwi #4772 show accessibility declaration + @MainActor private func showAccessibilityDeclaration() { + MXLog.debug("[AuthenticationLoginCoordinator] showAccessibilityDeclaration") + let accessibilityDeclarationViewController = AccessibilityDeclarationViewController.makeViewController() + navigationRouter.push(accessibilityDeclarationViewController, animated: true, popCompletion: nil) + } + /// Updates the view model to reflect any changes made to the homeserver. @MainActor private func updateViewModel() { let homeserver = authenticationService.state.homeserver diff --git a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift index 662c26383..60e1b2d35 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift @@ -93,6 +93,13 @@ struct AuthenticationLoginScreen: View { .readableFrame() .padding(.horizontal, 16) } + + if BWIBuildSettings.shared.bumLoginFlowLayout && BWIBuildSettings.shared.bwiShowAccessibilityDeclaration { + accessibilityDeclaration + .frame(alignment: .bottom) + .padding(.bottom, 10) + } + if BWIBuildSettings.shared.bumLoginFlowLayout { dataPrivacyForm .frame(alignment: .bottom) @@ -305,6 +312,19 @@ struct AuthenticationLoginScreen: View { return BWIBuildSettings.shared.bwiLoginFlowLayout ? BWIL10n.authUserIdPlaceholder : BWIL10n.authenticationLoginUsername } } + + // bwi: Accessibility declaration + var accessibilityDeclaration: some View { + Button(action: { + viewModel.send(viewAction: .accessibilityDeclaration) + }, label: { + Text(BWIL10n.bwiAccessibilityDeclarationButtonTitle) + .font(theme.fonts.footnote) + .foregroundColor(.blue) + .underline() + }) + .padding([.horizontal], 20) + } } // MARK: - Previews diff --git a/bwi/AccessibilityDeclaration/AccessibilityDeclarationView.swift b/bwi/AccessibilityDeclaration/AccessibilityDeclarationView.swift new file mode 100644 index 000000000..0dd71f2d7 --- /dev/null +++ b/bwi/AccessibilityDeclaration/AccessibilityDeclarationView.swift @@ -0,0 +1,46 @@ +// +/* + * Copyright (c) 2023 BWI GmbH + * + * 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 + +@objcMembers class AccessibilityDeclarationViewController: NSObject { + + @available(iOS 14.0, *) + class func makeViewController() -> UIViewController { + var accessibilityDeclarationFilePath: URL? = nil + if !BWIBuildSettings.shared.accessibilityDeclarationFileDe.isEmpty && Bundle.main.preferredLocalizations[0].elementsEqual("de") { + accessibilityDeclarationFilePath = Bundle.main.url(forResource: BWIBuildSettings.shared.accessibilityDeclarationFileDe, withExtension: "md") + } else if !BWIBuildSettings.shared.accessibilityDeclarationFileEn.isEmpty { + accessibilityDeclarationFilePath = Bundle.main.url(forResource: BWIBuildSettings.shared.accessibilityDeclarationFileEn, withExtension: "md") + } + + if let url = accessibilityDeclarationFilePath { + guard let string = try? String(contentsOf: url) else { + return UIHostingController(rootView: EmptyView()) + } + let vc = UIHostingController(rootView: MarkDownView(markdownString: string)) + vc.title = BWIL10n.bwiAccessibilityDeclarationButtonTitle + vc.view.backgroundColor = ThemeService.shared().theme.backgroundColor + vc.navigationItem.largeTitleDisplayMode = .never + return vc + } else { + return UIHostingController(rootView: EmptyView()) + } + } +} + + diff --git a/bwi/MarkDown/MarkDownView.swift b/bwi/MarkDown/MarkDownView.swift new file mode 100644 index 000000000..7256b24a9 --- /dev/null +++ b/bwi/MarkDown/MarkDownView.swift @@ -0,0 +1,62 @@ +// +/* + * Copyright (c) 2023 BWI GmbH + * + * 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 Down + +struct MarkDownView: View { + var markdownString: String + @State var labelHeight: CGFloat = .zero + + var body: some View { + GeometryReader { geometry in + ScrollView(.vertical) { + UIMarkDownWrapper(markDownString: markdownString, height: $labelHeight) + .frame(width: geometry.size.width - 20) + .frame(minHeight: labelHeight) + .padding(10) + } + .background(Color(ThemeService.shared().theme.backgroundColor)) + } + } +} + +struct UIMarkDownWrapper: UIViewRepresentable { + var markDownString: String + @Binding var height: CGFloat + + func makeUIView(context: Context) -> UILabel { + let label = UILabel(frame: .zero) + label.numberOfLines = 0 + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return label + } + + func updateUIView(_ uiView: UILabel, context: Context) { + let down = Down(markdownString: markDownString) + + guard let attributedString = try? down.toAttributedString() else { return } + let mutableString = NSMutableAttributedString(attributedString: attributedString) + mutableString.addAttributes([.foregroundColor: ThemeService.shared().theme.textPrimaryColor], range: NSRange(location: 0, length: attributedString.length)) + + uiView.attributedText = mutableString + DispatchQueue.main.async { + height = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height + } + } + +} From 10ec6686e0d6f269deccca953a9e7989da6933f4 Mon Sep 17 00:00:00 2001 From: Arnfried Griesert Date: Wed, 14 Jun 2023 06:28:28 +0000 Subject: [PATCH 125/149] MESSENGER-4744 Removed black theme and map already set black theme to dark --- Riot/Managers/Theme/ThemeIdentifier.swift | 2 +- Riot/Managers/Theme/ThemeService.m | 2 +- .../Modules/Settings/SettingsViewController.m | 20 +++++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Riot/Managers/Theme/ThemeIdentifier.swift b/Riot/Managers/Theme/ThemeIdentifier.swift index 76b19e57d..0286e08cd 100644 --- a/Riot/Managers/Theme/ThemeIdentifier.swift +++ b/Riot/Managers/Theme/ThemeIdentifier.swift @@ -28,7 +28,7 @@ enum ThemeIdentifier: String, RawRepresentable { case "dark": self = .dark case "black": - self = .black + self = .dark // bwi: 4744 (map previous set black theme to dark) default: return nil } diff --git a/Riot/Managers/Theme/ThemeService.m b/Riot/Managers/Theme/ThemeService.m index 9d544ddce..a47892fb4 100644 --- a/Riot/Managers/Theme/ThemeService.m +++ b/Riot/Managers/Theme/ThemeService.m @@ -80,7 +80,7 @@ NSString *const kThemeServiceDidChangeThemeNotification = @"kThemeServiceDidChan } else if ([themeId isEqualToString:@"black"]) { - theme = [BlackTheme new]; + theme = [DarkTheme new]; // bwi: 4744 (map previous set black theme to dark) } else { diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index ff086336d..c9c0ec10d 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2482,9 +2482,15 @@ ChangePasswordCoordinatorBridgePresenterDelegate> theme = @"auto"; } + // bwi: 4744 (map previous set black theme to dark) + if([theme isEqualToString:@"black"]) { + theme = @"dark"; + } + theme = [NSString stringWithFormat:@"settings_ui_theme_%@", theme]; + NSString *i18nTheme = NSLocalizedStringFromTable(theme, @"Vector", nil); - + cell.textLabel.textColor = ThemeService.shared.theme.textPrimaryColor; cell.textLabel.text = [VectorL10n settingsUiTheme]; @@ -4348,7 +4354,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { __weak typeof(self) weakSelf = self; - __block UIAlertAction *autoAction, *lightAction, *darkAction, *blackAction; + __block UIAlertAction *autoAction, *lightAction, *darkAction/* bwi: 4744 , *blackAction*/; NSString *themePickerMessage; void (^actionBlock)(UIAlertAction *action) = ^(UIAlertAction * action) { @@ -4370,11 +4376,13 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { newTheme = @"dark"; } + /* bwi: 4744 else if (action == blackAction) { newTheme = @"black"; } - + */ + NSString *theme = RiotSettings.shared.userInterfaceTheme; if (newTheme && ![newTheme isEqualToString:theme]) { @@ -4418,9 +4426,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate> darkAction = [UIAlertAction actionWithTitle:[VectorL10n settingsUiThemeDark] style:UIAlertActionStyleDefault handler:actionBlock]; + /* bwi: 4744 blackAction = [UIAlertAction actionWithTitle:[VectorL10n settingsUiThemeBlack] style:UIAlertActionStyleDefault handler:actionBlock]; + */ UIAlertController *themePicker = [UIAlertController alertControllerWithTitle:[VectorL10n settingsUiThemePickerTitle] @@ -4433,8 +4443,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> } [themePicker addAction:lightAction]; [themePicker addAction:darkAction]; + /* bwi: 4744 [themePicker addAction:blackAction]; - + */ + // Cancel button [themePicker addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel From 1a21b03d6e5c2a98f8b840530422c51d0b5cddf9 Mon Sep 17 00:00:00 2001 From: Frank Rotermund Date: Thu, 15 Jun 2023 07:40:58 +0200 Subject: [PATCH 126/149] MESSENGER-3539 prevent logout and error popup when logging out --- Riot/Assets/de.lproj/Bwi.strings | 1 + Riot/Assets/en.lproj/Bwi.strings | 1 + Riot/Generated/BWIStrings.swift | 4 +++ .../Modules/Settings/SettingsViewController.h | 8 ++++++ .../Modules/Settings/SettingsViewController.m | 27 ++++++++++++++----- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index 40345b21f..87ea882eb 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -501,6 +501,7 @@ "bwi_error_invite_already_in_room" = "%@ ist bereits im Raum."; "bwi_error_invite_banned_in_room" = "%@ ist vom Raum gebannt."; "bwi_error_invite_general" = "%@ konnte nicht eingeladen werden."; +"bwi_error_logout_offline" = "Abmelden ist ohne Internetverbindung nicht möglich."; // MARK: - Matomo diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index f7ace9edb..dad7187c4 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -410,6 +410,7 @@ "bwi_error_invite_already_in_room" = "%@ is already in the room."; "bwi_error_invite_banned_in_room" = "%@ is banned from the room."; "bwi_error_invite_general" = "%@ could not be invited."; +"bwi_error_logout_offline" = "Logout not possible without internet connection."; // MARK: - Matomo diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index 5d67dcb14..c5f4c7907 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -211,6 +211,10 @@ public class BWIL10n: NSObject { public static func bwiErrorInviteGeneral(_ p1: String) -> String { return BWIL10n.tr("Bwi", "bwi_error_invite_general", p1) } + /// Abmelden ist ohne Internetverbindung nicht möglich. + public static var bwiErrorLogoutOffline: String { + return BWIL10n.tr("Bwi", "bwi_error_logout_offline") + } /// Du kannst jetzt aktive und vergangene Umfragen gesammelt in den Raumdetails einsehen (erreichbar unter Raumdetails, im Bereich "Umfrageverlauf"). public static var bwiFeatureBannerAdvertisementText: String { return BWIL10n.tr("Bwi", "bwi_feature_banner_advertisement_text") diff --git a/Riot/Modules/Settings/SettingsViewController.h b/Riot/Modules/Settings/SettingsViewController.h index 3e99593fc..5b5f87b27 100644 --- a/Riot/Modules/Settings/SettingsViewController.h +++ b/Riot/Modules/Settings/SettingsViewController.h @@ -16,6 +16,14 @@ #import "MatrixKit.h" +FOUNDATION_EXPORT NSString *const BWISettingsErrorDomain; + +typedef enum : NSUInteger +{ + BWISettingsOfflineLogoutErrorCode +} +BWISettingsErrorCode; + @interface SettingsViewController : MXKTableViewController + (instancetype)instantiate; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 6bec36352..56a3f6565 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -47,6 +47,8 @@ NSString* const kSettingsViewControllerPhoneBookCountryCellId = @"kSettingsViewControllerPhoneBookCountryCellId"; +NSString *const BWISettingsErrorDomain = @"BWISettingsErrorDomain"; + typedef NS_ENUM(NSUInteger, SECTION_TAG) { SECTION_TAG_SIGN_OUT = 0, @@ -3452,13 +3454,24 @@ ChangePasswordCoordinatorBridgePresenterDelegate> - (void)onSignout:(id)sender { - self.signOutButton = (UIButton*)sender; - - SignOutFlowPresenter *flowPresenter = [[SignOutFlowPresenter alloc] initWithSession:self.mainSession presentingViewController:self]; - flowPresenter.delegate = self; - - [flowPresenter startWithSourceView:self.signOutButton]; - self.signOutFlowPresenter = flowPresenter; + // bwi (#3539) Check connectivity before login out. Logout can have strange effects when occuring without network connection + if ([AppDelegate theDelegate].isOffline) + { + NSError *error = [NSError errorWithDomain:BWISettingsErrorDomain + code:BWISettingsOfflineLogoutErrorCode + userInfo:@{ + NSLocalizedDescriptionKey : BWIL10n.bwiErrorLogoutOffline + }]; + [[AppDelegate theDelegate] showErrorAsAlert:error]; + } else { + self.signOutButton = (UIButton*)sender; + + SignOutFlowPresenter *flowPresenter = [[SignOutFlowPresenter alloc] initWithSession:self.mainSession presentingViewController:self]; + flowPresenter.delegate = self; + + [flowPresenter startWithSourceView:self.signOutButton]; + self.signOutFlowPresenter = flowPresenter; + } } - (void)onRemove3PID:(NSIndexPath*)indexPath From a52c450bbb3e1d5988a5b2724b61c7c3d2354e7d Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Thu, 15 Jun 2023 10:21:50 +0200 Subject: [PATCH 127/149] MESSENGER-4390 fix private room sharing --- .../RoomInfo/RoomInfoList/RoomInfoListViewModel.swift | 2 +- Riot/Modules/Room/RoomViewController.m | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift index 932f7fddd..01b78d41e 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift @@ -98,7 +98,7 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType { } func isInviteOnlyRoom() -> Bool { - return room.summary.membership == .invite + return room.summary.joinRule == MXRoomJoinRule.invite.identifier } func isPermalinkableRoom() -> Bool { diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 96f84bc20..e1562b51c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4132,7 +4132,9 @@ static CGSize kThreadListBarButtonItemImageSize; if (selectedEvent.sentState == MXEventSentStateSent && !selectedEvent.isTimelinePollEvent && // Forwarding of live-location shares still to be implemented - selectedEvent.eventType != MXEventTypeBeaconInfo) + selectedEvent.eventType != MXEventTypeBeaconInfo && + // bwi #4390: remove forwarding action if room is private + ![self.roomDataSource.room.summary.joinRule isEqualToString: kMXRoomJoinRuleInvite]) { [self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] @@ -4184,7 +4186,9 @@ static CGSize kThreadListBarButtonItemImageSize; if (selectedEvent.sentState == MXEventSentStateSent && (attachment.type == MXKAttachmentTypeFile || attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo || - attachment.type == MXKAttachmentTypeVoiceMessage)) { + attachment.type == MXKAttachmentTypeVoiceMessage) && + // bwi #4390: remove forwarding action if room is private + ![self.roomDataSource.room.summary.joinRule isEqualToString: kMXRoomJoinRuleInvite]) { [self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] style:UIAlertActionStyleDefault From 4abaab4061e798e452020aefae8338162afa6060 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Fri, 16 Jun 2023 17:01:12 +0200 Subject: [PATCH 128/149] MESSENGER-4742 fix permalink info when room is not available --- Riot/Assets/de.lproj/Bwi.strings | 2 ++ Riot/Assets/en.lproj/Bwi.strings | 2 ++ Riot/Generated/BWIStrings.swift | 8 ++++++++ Riot/Modules/Application/LegacyAppDelegate.m | 8 +++++++- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index 87ea882eb..c8bc4ea9b 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -597,6 +597,8 @@ // MARK: - Permalink "settings_permalink_prefix_picker_title" = "Permalink Prefix"; +"bwi_error_room_not_available_title" = "Link ungültig"; +"bwi_error_room_not_available_message" = "Der Raum wurde bereits geschlossen, daher kannst Du nicht mehr beitreten."; // MARK: - Notification Settings "settings_notify_me_for" = "Benachrichtige mich für"; diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index dad7187c4..a9fd60ee2 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -507,6 +507,8 @@ // MARK: - Permalink "settings_permalink_prefix_picker_title" = "Permalink Prefix"; +"bwi_error_room_not_available_title" = "Link invalid"; +"bwi_error_room_not_available_message" = "The room has already been closed, you can no longer join."; // MARK: - Notification Settings "settings_notify_me_for" = "Notify me for"; diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index c5f4c7907..c2223a09b 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -215,6 +215,14 @@ public class BWIL10n: NSObject { public static var bwiErrorLogoutOffline: String { return BWIL10n.tr("Bwi", "bwi_error_logout_offline") } + /// Der Raum wurde bereits geschlossen, daher kannst Du nicht mehr beitreten. + public static var bwiErrorRoomNotAvailableMessage: String { + return BWIL10n.tr("Bwi", "bwi_error_room_not_available_message") + } + /// Link ungültig + public static var bwiErrorRoomNotAvailableTitle: String { + return BWIL10n.tr("Bwi", "bwi_error_room_not_available_title") + } /// Du kannst jetzt aktive und vergangene Umfragen gesammelt in den Raumdetails einsehen (erreichbar unter Raumdetails, im Bereich "Umfrageverlauf"). public static var bwiFeatureBannerAdvertisementText: String { return BWIL10n.tr("Bwi", "bwi_feature_banner_advertisement_text") diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 9891eead1..5cd25ca35 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1672,7 +1672,13 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self peekInRoomWithNavigationParameters:roomPreviewNavigationParameters pathParams:pathParams]; } } failure:^(NSError *error) { - [self peekInRoomWithNavigationParameters:roomPreviewNavigationParameters pathParams:pathParams]; + // bwi #4742: check if the room is still available + NSHTTPURLResponse *response = error.userInfo[@"httpResponse"]; + if (response.statusCode == 404) { + [self showAlertWithTitle:BWIL10n.bwiErrorRoomNotAvailableTitle message:BWIL10n.bwiErrorRoomNotAvailableMessage]; + } else { + [self peekInRoomWithNavigationParameters:roomPreviewNavigationParameters pathParams:pathParams]; + } }]; } From 1cb7d01466d296e584126a241b006c0d713c6033 Mon Sep 17 00:00:00 2001 From: Frank Rotermund Date: Mon, 19 Jun 2023 16:10:12 +0200 Subject: [PATCH 129/149] MESSENGER-4710 remove any possibility to self verifiy with old device manager --- Config/BWIBuildSettings.swift | 5 +++-- Riot/Assets/de.lproj/Bwi.strings | 1 + Riot/Assets/en.lproj/Bwi.strings | 1 + Riot/Generated/BWIStrings.swift | 4 ++++ .../KeyVerification/Common/KeyVerificationCoordinator.swift | 2 +- .../UserVerificationSessionStatusViewController.swift | 2 +- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index 6bef5e637..6a1660c9e 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -438,8 +438,9 @@ class BWIBuildSettings: NSObject { var authScreenShowTestServerOptions = true var authScreenShowSocialLoginSection = false - // MARK: - Cross-signing (bwi=true) - var disableSelfUserVerification = false + // MARK: - Self Verification not crosssigning (bwi=true) + var disableSelfUserVerification = true + var disableCrosssigning = false var additionalSelfVerfificationAlert = false var showNoOtherDeviceError = false diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index 87ea882eb..fea2425a9 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -617,6 +617,7 @@ // MARK: - Device Manager "user_session_verified_session_description" = "Du hast deine Sitzung durch Eingabe des Wiederherstellungsschlüssels oder durch die Verifizierung mit einem anderen Gerät bestätigt. Dies bedeutet, dass du alle Schlüssel zum Entschlüsseln deiner Nachrichten hast und anderen bestätigst, dieser Sitzung zu vertrauen."; "user_session_button_view_all" = "Alle anzeigen (%d)"; +"user_verification_session_details_verify_action_current_user" = "Sitzung verifizieren"; // MARK: - Voice Over "textfield_reveal_secret" = "Texteingabe anzeigen"; diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index dad7187c4..6ea7a9af0 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -527,6 +527,7 @@ // MARK: - Device Manager "user_session_verified_session_description" = "You have confirmed your session by entering the recovery key or verifying with another device. This means that you have all the keys to decrypt your messages and are confirming to others to trust this session."; "user_session_button_view_all" = "View all (%d)"; +"user_verification_session_details_verify_action_current_user" = "Verify session"; // MARK: - Voice Over "textfield_reveal_secret" = "reveal text input"; diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index c5f4c7907..9adb25f9b 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -1711,6 +1711,10 @@ public class BWIL10n: NSObject { public static var userSessionVerifiedSessionDescription: String { return BWIL10n.tr("Bwi", "user_session_verified_session_description") } + /// Sitzung verifizieren + public static var userVerificationSessionDetailsVerifyActionCurrentUser: String { + return BWIL10n.tr("Bwi", "user_verification_session_details_verify_action_current_user") + } /// Ansehen public static var view: String { return BWIL10n.tr("Bwi", "view") diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift index 0b7622a54..9220ae9a1 100644 --- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift +++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift @@ -137,7 +137,7 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType { case .incomingSASTransaction(let incomingSASTransaction): rootCoordinator = self.createDataLoadingScreenCoordinator(otherUserId: incomingSASTransaction.otherUserId, otherDeviceId: incomingSASTransaction.otherDeviceId) case .completeSecurity(let isNewSignIn): - if BWIBuildSettings.shared.disableSelfUserVerification { + if BWIBuildSettings.shared.disableCrosssigning { let coordinator = self.createSecretsRecoveryCoordinator(with: .passphraseOrKey) rootCoordinator = coordinator } else { diff --git a/Riot/Modules/KeyVerification/User/SessionStatus/UserVerificationSessionStatusViewController.swift b/Riot/Modules/KeyVerification/User/SessionStatus/UserVerificationSessionStatusViewController.swift index 0331b504d..42f382ef5 100644 --- a/Riot/Modules/KeyVerification/User/SessionStatus/UserVerificationSessionStatusViewController.swift +++ b/Riot/Modules/KeyVerification/User/SessionStatus/UserVerificationSessionStatusViewController.swift @@ -175,7 +175,7 @@ final class UserVerificationSessionStatusViewController: UIViewController { if viewData.isCurrentUser { unstrustedInformationText = VectorL10n.userVerificationSessionDetailsAdditionalInformationUntrustedCurrentUser - verifyButtonTitle = VectorL10n.userVerificationSessionDetailsVerifyActionCurrentUser + verifyButtonTitle = BWIL10n.userVerificationSessionDetailsVerifyActionCurrentUser } else { unstrustedInformationText = VectorL10n.userVerificationSessionDetailsAdditionalInformationUntrustedOtherUser verifyButtonTitle = VectorL10n.userVerificationSessionDetailsVerifyActionOtherUser From 30bc66acf5c150f97b086f73d5d1d0e093de6094 Mon Sep 17 00:00:00 2001 From: Frank Rotermund Date: Tue, 20 Jun 2023 11:14:01 +0200 Subject: [PATCH 130/149] MESSENGER-4573 reset room filter on logout --- Riot/Modules/Application/LegacyAppDelegate.m | 2 ++ .../Home/AllChats/AllChatsLayoutSettingsManager.swift | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 9891eead1..0ecf9e2cf 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2259,6 +2259,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [BWIBuildSettings.shared reset]; [BWIAnalytics.sharedTracker resetUserdefaults]; + // bwi #4573 reset chatsfilter on logout + [AllChatsLayoutSettingsManager.shared reset]; } completion (YES); } diff --git a/Riot/Modules/Home/AllChats/AllChatsLayoutSettingsManager.swift b/Riot/Modules/Home/AllChats/AllChatsLayoutSettingsManager.swift index c6ff7e76d..27c04a2f8 100644 --- a/Riot/Modules/Home/AllChats/AllChatsLayoutSettingsManager.swift +++ b/Riot/Modules/Home/AllChats/AllChatsLayoutSettingsManager.swift @@ -51,6 +51,11 @@ final class AllChatsLayoutSettingsManager: NSObject { // MARK: - Public + // bwi #4573 reset filters for logout + @objc func reset() { + activeFilters = .all + } + var activeFilters: AllChatsLayoutFilterType { get { guard let value = RiotSettings.defaults.object(forKey: Constants.activeFiltersKey) as? NSNumber else { From 04e333302bfaafbbc338a539f6b4764f75f6a804 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Tue, 20 Jun 2023 12:19:28 +0200 Subject: [PATCH 131/149] MESSENGER-4743 fix removing room avatar --- .../Views/RoomInfoBasicView.swift | 9 ++- .../Settings/RoomSettingsViewController.m | 55 ++++++++++++++++++- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift index ca6c1c85c..6f548f9ca 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift @@ -87,10 +87,11 @@ class RoomInfoBasicView: UIView { } } - if avatarImageView.image == nil { - if let avatarUrl = viewData.avatarUrl { + // bwi: update room avatar + if let avatarUrl = viewData.avatarUrl { + if !avatarUrl.isEmpty { avatarImageView.enableInMemoryCache = true - + avatarImageView.setImageURI(avatarUrl, withType: nil, andImageOrientation: .up, @@ -101,6 +102,8 @@ class RoomInfoBasicView: UIView { } else { avatarImageView.image = avatarImage } + } else { + avatarImageView.image = avatarImage } badgeImageView.image = viewData.encryptionImage diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index baccdff0c..0bdc6c2cc 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -132,6 +132,8 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti { // The updated user data NSMutableDictionary *updatedItemsDict; + // bwi #4743: remove room avatar + BOOL shouldRemoveRoomAvatarImage; // The current table items UITextField* nameTextField; @@ -1342,7 +1344,7 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti [self dismissFirstResponder]; // Check whether some changes have been done - if (updatedItemsDict.count) + if (updatedItemsDict.count|| shouldRemoveRoomAvatarImage) { [self promptUserToSaveChanges]; } @@ -1408,7 +1410,7 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti - (IBAction)onSave:(id)sender { - if (updatedItemsDict.count) + if (updatedItemsDict.count || shouldRemoveRoomAvatarImage) { [self startActivityIndicator]; @@ -1417,6 +1419,42 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti // check if there is some updates related to room state if (mxRoomState) { + // bwi #4743: remove room avatar + if (shouldRemoveRoomAvatarImage) { + shouldRemoveRoomAvatarImage = false; + [updatedItemsDict removeObjectForKey:kRoomSettingsAvatarKey]; + pendingOperation = [mxRoom setAvatar:@"" success:^{ + if (weakSelf) + { + typeof(self) self = weakSelf; + + self->pendingOperation = nil; + + [self onSave:nil]; + } + } failure:^(NSError *error) { + MXLogDebug(@"[RoomSettingsViewController] reset image failed"); + + if (weakSelf) + { + typeof(self) self = weakSelf; + + self->pendingOperation = nil; + + dispatch_async(dispatch_get_main_queue(), ^{ + + NSString* message = error.localizedDescription; + if (!message.length) + { + message = [VectorL10n roomDetailsFailToUpdateAvatar]; + } + [self onSaveFailed:message withKeys:@[kRoomSettingsAvatarKey]]; + + }); + } + }]; + } + if (updatedItemsDict[kRoomSettingsAvatarKey]) { // Retrieve the current picture and make sure its orientation is up @@ -2226,6 +2264,11 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti { roomPhotoCell.mxkImageView.image = (UIImage*) updatedItemsDict[kRoomSettingsAvatarKey]; } + else if (shouldRemoveRoomAvatarImage) { + // bwi #4743: remove room avatar + NSString *roomName = mxRoom.summary.displayName; + roomPhotoCell.mxkImageView.image = [AvatarGenerator generateAvatarForMatrixItem:mxRoom.roomId withDisplayName:roomName]; + } else { // bwi if the room is a personal notes room use a local image @@ -3254,6 +3297,8 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti if (!mxRoom.isDirect || BWIBuildSettings.shared.showUnrelatedRoomSettingsForDirectMessages) { SingleImagePickerPresenter *singleImagePickerPresenter = [[SingleImagePickerPresenter alloc] initWithSession:self.mainSession]; singleImagePickerPresenter.delegate = self; + // bwi #4743: remove room avatar + singleImagePickerPresenter.allowsRemoveImage = (updatedItemsDict[kRoomSettingsAvatarKey] != nil || (mxRoom.summary.avatar != nil && ![mxRoom.summary.avatar isEqual: @""])); UIView *sourceView; @@ -3679,6 +3724,12 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti // This method should never be called here because room settings should not show the remove image option. But nevertheless we do a nice cleanup also for this delegate call [presenter dismissWithAnimated:YES completion:nil]; self.imagePickerPresenter = nil; + + // bwi #4743: the room avatar should be removable + [updatedItemsDict removeObjectForKey:kRoomSettingsAvatarKey]; + shouldRemoveRoomAvatarImage = true; + [self getNavigationItem].rightBarButtonItem.enabled = YES; + [self refreshRoomSettings]; } #pragma mark - TableViewSectionsDelegate From 08b159b6755690a0ae136ba46fb7ca9f9decc526 Mon Sep 17 00:00:00 2001 From: Arnfried Griesert Date: Tue, 20 Jun 2023 11:32:09 +0000 Subject: [PATCH 132/149] MESSENGER-4769 color changes part 2 --- Config/BWIBuildSettings.swift | 3 + .../BuM-Beta/BWIBuildSettings+BuM-Beta.swift | 1 + Podfile.lock | 66 +++++++++++++++---- .../Views/RecentsInvitesTableViewCell.swift | 2 +- .../AllChatsFilterOptionListView.swift | 9 ++- .../AllChats/AllChatsViewController.swift | 36 +++++++++- .../People/Views/InviteRecentTableViewCell.m | 5 ++ .../RoomCreationIntroCellContentView.swift | 15 +++-- .../Room/DirectoryRoomTableViewCell.swift | 2 +- .../View/AuthenticationTermsToggleStyle.swift | 2 +- .../Common/Util/RoundedBorderTextField.swift | 7 +- .../Onboarding/Common/OnboardingIcon.swift | 2 +- .../View/FormPickerItem.swift | 2 + .../Room/PollEditForm/View/PollEditForm.swift | 9 ++- bwi/QRCode/MyQRCodeView.swift | 2 +- 15 files changed, 135 insertions(+), 28 deletions(-) diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index 6bef5e637..80b4f3ea7 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -619,6 +619,9 @@ class BWIBuildSettings: NSObject { // MARK: Rust Encryption var useRustEncryption = false + // MARK: Color Theme + var useNewBumColors = false + // MARK: Sessions Manager var enableNewSessionManagerByDefault = false diff --git a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift index 67dc93639..f8e3f4934 100644 --- a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift +++ b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift @@ -20,6 +20,7 @@ import Foundation extension BWIBuildSettings { func overrideTargetSpecificSettings() { + useNewBumColors = true secondaryAppName = "BundesMessenger" settingsScreenShowLabSettings = true authScreenShowRegister = true diff --git a/Podfile.lock b/Podfile.lock index eae31a173..d88d17f49 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -20,6 +20,24 @@ PODS: - Down (0.11.0) - DSBottomSheet (0.3.0) - DSWaveformImage (6.1.1) + - DTCoreText (1.6.26): + - DTCoreText/Core (= 1.6.26) + - DTFoundation/Core (~> 1.7.5) + - DTFoundation/DTAnimatedGIF (~> 1.7.5) + - DTFoundation/DTHTMLParser (~> 1.7.5) + - DTFoundation/UIKit (~> 1.7.5) + - DTCoreText/Core (1.6.26): + - DTFoundation/Core (~> 1.7.5) + - DTFoundation/DTAnimatedGIF (~> 1.7.5) + - DTFoundation/DTHTMLParser (~> 1.7.5) + - DTFoundation/UIKit (~> 1.7.5) + - DTFoundation/Core (1.7.18) + - DTFoundation/DTAnimatedGIF (1.7.18) + - DTFoundation/DTHTMLParser (1.7.18): + - DTFoundation/Core + - DTFoundation/UIKit (1.7.18): + - DTFoundation/Core + - DTTJailbreakDetection (0.4.0) - FLEX (4.5.0) - FlowCommoniOS (1.12.2) - GBDeviceInfo (7.1.0): @@ -39,6 +57,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) + - MatomoTracker (7.5.2): + - MatomoTracker/Core (= 7.5.2) + - MatomoTracker/Core (7.5.2) - MatrixSDK (0.26.10): - MatrixSDK/Core (= 0.26.10) - MatrixSDK/Core (0.26.10): @@ -95,6 +116,8 @@ DEPENDENCIES: - Down (~> 0.11.0) - DSBottomSheet (~> 0.3) - DSWaveformImage (~> 6.1.1) + - DTCoreText (= 1.6.26) + - DTTJailbreakDetection (~> 0.4.0) - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - GBDeviceInfo (~> 7.1.0) @@ -102,8 +125,9 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.26.10) - - MatrixSDK/JingleCallStack (= 0.26.10) + - MatomoTracker (~> 7.5.2) + - MatrixSDK (from `https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk`, tag `v0.26.10_bwi_beta`) + - MatrixSDK/JingleCallStack (from `https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk`, tag `v0.26.10_bwi_beta`) - OLMKit - PostHog (~> 2.0.0) - ReadMoreTextView (~> 3.0.1) @@ -122,8 +146,20 @@ DEPENDENCIES: - ZXingObjC (~> 3.6.5) SPEC REPOS: - trunk: + https://github.com/CocoaPods/Specs.git: - AFNetworking + - DTCoreText + - DTFoundation + - DTTJailbreakDetection + - GZIP + - JitsiMeetSDKLite + - JitsiWebRTC + - libbase58 + - MatomoTracker + - MatrixSDKCrypto + - Realm + - SwiftyBeaver + trunk: - BlueCryptor - BlueECC - BlueRSA @@ -133,23 +169,16 @@ SPEC REPOS: - FLEX - FlowCommoniOS - GBDeviceInfo - - GZIP - Introspect - - JitsiMeetSDKLite - - JitsiWebRTC - KeychainAccess - KituraContracts - KTCenterFlowLayout - - libbase58 - libPhoneNumber-iOS - LoggerAPI - Logging - - MatrixSDK - - MatrixSDKCrypto - OLMKit - PostHog - ReadMoreTextView - - Realm - Reusable - Sentry - SideMenu @@ -158,13 +187,22 @@ SPEC REPOS: - SwiftGen - SwiftJWT - SwiftLint - - SwiftyBeaver - UICollectionViewLeftAlignedLayout - UICollectionViewRightAlignedLayout - WeakDictionary - zxcvbn-ios - ZXingObjC +EXTERNAL SOURCES: + MatrixSDK: + :git: https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk + :tag: v0.26.10_bwi_beta + +CHECKOUT OPTIONS: + MatrixSDK: + :git: https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk + :tag: v0.26.10_bwi_beta + SPEC CHECKSUMS: AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58 BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 @@ -173,6 +211,9 @@ SPEC CHECKSUMS: Down: b6ba1bc985c9d2f4e15e3b293d2207766fa12612 DSBottomSheet: ca0ac37eb5af2dd54663f86b84382ed90a59be2a DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce + DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce + DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 + DTTJailbreakDetection: 5e356c5badc17995f65a83ed9483f787a0057b71 FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 GBDeviceInfo: 5d62fa85bdcce3ed288d83c28789adf1173e4376 @@ -187,6 +228,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b + MatomoTracker: 1d98ddc58322fd9d65e1a6886b8e41363047bd13 MatrixSDK: 68e39c246ff8d80c5788d5fc46e93fcbb24703fa MatrixSDKCrypto: ac805c22c24f79f349cdbfa065855c73a4c81b51 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 @@ -208,6 +250,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 4c82d7cddeb9c9b7a7adeaa2cd76d416117cd1a6 +PODFILE CHECKSUM: 6bd4e9aff1b435c22f06ad6a4497af49acca8a27 COCOAPODS: 1.11.3 diff --git a/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.swift b/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.swift index dea15153e..41de727aa 100644 --- a/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.swift +++ b/Riot/Modules/Common/Recents/Views/RecentsInvitesTableViewCell.swift @@ -63,7 +63,7 @@ class RecentsInvitesTableViewCell: UITableViewCell, NibReusable, Themable { badgeLabel.textColor = theme.colors.background badgeLabel.font = theme.fonts.footnoteSB - titleLabel.textColor = theme.colors.accent + titleLabel.textColor = BWIBuildSettings.shared.useNewBumColors ? theme.tintColor : theme.colors.accent // bwi: 4769 } // MARK: - Private diff --git a/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift b/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift index 43a3e3398..93758b77b 100644 --- a/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift +++ b/Riot/Modules/Common/SectionHeaders/AllChatsFilterOptionListView.swift @@ -47,7 +47,7 @@ class AllChatsFilterOptionListView: UIView, Themable { // MARK: - Private - private let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + private let backgroundView = UIView() // bwi: 4769 private let separator = UIView() private let tabListView = TabListView() @@ -90,6 +90,10 @@ class AllChatsFilterOptionListView: UIView, Themable { // MARK: - Themable func update(theme: Theme) { + + // bwi: 4769 + backgroundView.backgroundColor = ThemeService.shared().theme.backgroundColor + backgroundColor = theme.colors.background.withAlphaComponent(0.7) tabListView.itemFont = theme.fonts.calloutSB @@ -102,6 +106,9 @@ class AllChatsFilterOptionListView: UIView, Themable { // MARK: - Private private func setupView() { + + // bwi: 4769 + backgroundView.backgroundColor = ThemeService.shared().theme.backgroundColor vc_addSubViewMatchingParent(backgroundView) addSubview(separator) diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index bc083457c..80ec752a5 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -138,6 +138,7 @@ class AllChatsViewController: HomeViewController { // bwi: 4179 toolbar.tintColor = ThemeService.shared().theme.tintColor + toolbar.barTintColor = ThemeService.shared().theme.backgroundColor updateUI() @@ -150,12 +151,22 @@ class AllChatsViewController: HomeViewController { NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.updateBadgeButton), name: MXSpaceNotificationCounter.didUpdateNotificationCount, object: nil) + + // bwi: 4769 + self.registerThemeServiceDidChangeThemeNotification() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - self.toolbar.tintColor = theme.colors.accent + // bwi: 4769 + if BWIBuildSettings.shared.useNewBumColors { + self.toolbar.tintColor = theme.tintColor + self.toolbar.barTintColor = theme.backgroundColor + } else { + self.toolbar.tintColor = theme.colors.accent + } + if self.navigationItem.searchController == nil { self.navigationItem.searchController = searchController } @@ -205,6 +216,17 @@ class AllChatsViewController: HomeViewController { } } + // bwi: 4769 + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + // bwi: 4769 + @objc private func themeDidChange() { + self.update(with: ThemeService.shared().theme) + } + + // MARK: - Public func switchSpace(withId spaceId: String?) { @@ -539,7 +561,16 @@ class AllChatsViewController: HomeViewController { } private func update(with theme: Theme) { - self.navigationController?.toolbar?.tintColor = theme.colors.accent + // bwi: 4769 + if BWIBuildSettings.shared.useNewBumColors { + toolbar.tintColor = ThemeService.shared().theme.tintColor + toolbar.barTintColor = ThemeService.shared().theme.backgroundColor + + UIToolbar.appearance().tintColor = ThemeService.shared().theme.tintColor + UIToolbar.appearance().barTintColor = ThemeService.shared().theme.backgroundColor + } else { + self.navigationController?.toolbar?.tintColor = theme.colors.accent + } } // MARK: - Private @@ -623,7 +654,6 @@ class AllChatsViewController: HomeViewController { // bwi: 4179 var allChatsEditButton = UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu) - allChatsEditButton.tintColor = ThemeService.shared().theme.tintColor if BWIBuildSettings.shared.enableSpaces { self.toolbar.items = [ diff --git a/Riot/Modules/People/Views/InviteRecentTableViewCell.m b/Riot/Modules/People/Views/InviteRecentTableViewCell.m index ea42b74a4..9e07c6ec1 100644 --- a/Riot/Modules/People/Views/InviteRecentTableViewCell.m +++ b/Riot/Modules/People/Views/InviteRecentTableViewCell.m @@ -65,6 +65,11 @@ NSString *const kInviteRecentTableViewCellRoomKey = @"kInviteRecentTableViewCell self.rightButton.backgroundColor = ThemeService.shared.theme.tintColor; self.rightButton.titleLabel.font = ThemeService.shared.theme.fonts.body; + + // bwi: 4769 + if(BWIBuildSettings.shared.useNewBumColors) { + [self.rightButton setTitleColor:ThemeService.shared.theme.backgroundColor forState:UIControlStateNormal]; + } } - (void)prepareForReuse diff --git a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift index 8747c3e92..92ed4cf40 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift +++ b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCellContentView.swift @@ -163,10 +163,17 @@ final class RoomCreationIntroCellContentView: UIView, NibLoadable, Themable { if let topic = topic, topic.isEmpty == false { attributedString.append(NSAttributedString(string: VectorL10n.roomIntroCellInformationRoomWithTopicSentence2(topic), attributes: informationTextDefaultAttributes)) } else { - let secondSentencePart1 = NSAttributedString(string: VectorL10n.roomIntroCellInformationRoomWithoutTopicSentence2Part1, attributes: [.foregroundColor: self.theme.tintColor]) - let secondSentencePart2 = NSAttributedString(string: VectorL10n.roomIntroCellInformationRoomWithoutTopicSentence2Part2, attributes: informationTextDefaultAttributes) - attributedString.append(secondSentencePart1) - attributedString.append(secondSentencePart2) + if BWIBuildSettings.shared.useNewBumColors { + let secondSentencePart1 = NSAttributedString(string: VectorL10n.roomIntroCellInformationRoomWithoutTopicSentence2Part1, attributes: [.foregroundColor: self.theme.tintColor, .font: self.theme.fonts.bodySB]) // bwi: 4769 + let secondSentencePart2 = NSAttributedString(string: VectorL10n.roomIntroCellInformationRoomWithoutTopicSentence2Part2, attributes: informationTextDefaultAttributes) + attributedString.append(secondSentencePart1) + attributedString.append(secondSentencePart2) + } else { + let secondSentencePart1 = NSAttributedString(string: VectorL10n.roomIntroCellInformationRoomWithoutTopicSentence2Part1, attributes: [.foregroundColor: self.theme.tintColor]) + let secondSentencePart2 = NSAttributedString(string: VectorL10n.roomIntroCellInformationRoomWithoutTopicSentence2Part2, attributes: informationTextDefaultAttributes) + attributedString.append(secondSentencePart1) + attributedString.append(secondSentencePart2) + } } return attributedString diff --git a/Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCell.swift b/Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCell.swift index 77f689688..e185a1429 100644 --- a/Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCell.swift +++ b/Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCell.swift @@ -118,7 +118,7 @@ extension DirectoryRoomTableViewCell: Themable { joinButton.layer.borderColor = theme.textSecondaryColor.cgColor } else { joinButton.backgroundColor = theme.tintColor - joinButton.tintColor = .white + joinButton.tintColor = BWIBuildSettings.shared.useNewBumColors ? theme.backgroundColor : .white // bwi: 4769 joinButton.layer.borderWidth = 0.0 joinButton.layer.borderColor = nil } diff --git a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift index f5e94ce84..491cd630e 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift @@ -24,7 +24,7 @@ struct AuthenticationTermsToggleStyle: ToggleStyle { Button { configuration.isOn.toggle() } label: { Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle") .font(.title3.weight(.regular)) - .foregroundColor(theme.colors.accent) + .foregroundColor(BWIBuildSettings.shared.useNewBumColors ? Color(ThemeService.shared().theme.tintColor) : theme.colors.accent) // bwi: 4769 } .buttonStyle(.plain) } diff --git a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift index 6fc4381f4..ef395aa99 100644 --- a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift +++ b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift @@ -98,7 +98,12 @@ struct RoundedBorderTextField: View { /// The text field's border color. private var borderColor: Color { if isEditing { - return BWIBuildSettings.shared.bwiEnableBuMUI ? Color(hex: 0x108194) : theme.colors.accent + // bwi: 4769 + if BWIBuildSettings.shared.useNewBumColors { + return Color(ThemeService.shared().theme.tintColor) + } else { + return Color(hex: 0x108194) + } } else if footerText != nil, isError { return theme.colors.alert } else { diff --git a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingIcon.swift b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingIcon.swift index 4993ce002..c974e2f9a 100644 --- a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingIcon.swift +++ b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingIcon.swift @@ -25,7 +25,7 @@ struct OnboardingIconImage: View { Image(image.name) .resizable() .renderingMode(.template) - .foregroundColor(theme.colors.accent) + .foregroundColor(BWIBuildSettings.shared.useNewBumColors ? Color(ThemeService.shared().theme.tintColor) : theme.colors.accent) .frame(width: OnboardingMetrics.iconSize, height: OnboardingMetrics.iconSize) .background(Circle().foregroundColor(.white).padding(2)) .accessibilityHidden(true) diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift index f194c352a..0d90a2d0e 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift @@ -36,12 +36,14 @@ struct FormPickerItem: View { if let subtitle = subtitle, !subtitle.isEmpty && BWIBuildSettings.shared.notificationSettingsLikeAndroidAndWeb { VStack(alignment: .leading, spacing: 4) { Text(title) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) Text(subtitle) .font(.footnote) .foregroundColor(.secondary) } } else { Text(title) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) } Spacer() if selected { diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index 240463030..8a3c8a190 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -69,12 +69,16 @@ struct PollEditForm: View { } } - Button(VectorL10n.pollEditFormAddOption) { + // bwi: 4769 + Button { withAnimation(.easeInOut(duration: 0.2)) { viewModel.send(viewAction: .addAnswerOption) } + } label: { + Text(VectorL10n.pollEditFormAddOption) + .foregroundColor(Color(ThemeService.shared().theme.tintColor)) + .font(theme.fonts.bodySB) } - .foregroundColor(Color(ThemeService.shared().theme.tintColor)) .disabled(!viewModel.viewState.addAnswerOptionButtonEnabled) Spacer() @@ -89,6 +93,7 @@ struct PollEditForm: View { } .padding(.vertical, 24.0) .padding(.horizontal, 16.0) + .background(Color(ThemeService.shared().theme.backgroundColor)) // bwi: 4769 .activityIndicator(show: viewModel.viewState.showLoadingIndicator) .alert(item: $viewModel.alertInfo) { info in Alert(title: Text(info.title), diff --git a/bwi/QRCode/MyQRCodeView.swift b/bwi/QRCode/MyQRCodeView.swift index f2588af49..5c32bcd9c 100644 --- a/bwi/QRCode/MyQRCodeView.swift +++ b/bwi/QRCode/MyQRCodeView.swift @@ -96,7 +96,7 @@ struct MyQRCodeView: View { var footer: some View { Text(BWIL10n.showMyQrScreenMessage(AppInfo.current.displayName)) .font(.system(size: 15)) - .foregroundColor(Color(ThemeService.shared().theme.colors.primaryContent)) + .foregroundColor(BWIBuildSettings.shared.useNewBumColors ? Color(ThemeService.shared().theme.tintColor) : Color(ThemeService.shared().theme.colors.primaryContent)) // bwi: 4769 .multilineTextAlignment(.center) } From 5c7998e2409325a572fd577d5c5f55477e2bde05 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Wed, 21 Jun 2023 06:24:46 +0000 Subject: [PATCH 133/149] Feature/4795 feature tracking matomo --- Riot/Modules/Room/RoomViewController.h | 1 + Riot/Modules/Room/RoomViewController.m | 18 +++- .../Shared/ForwardingShareItemSender.swift | 12 ++- .../Coordinator/PollEditFormCoordinator.swift | 25 +++++ bwi/MatomoAnalytics/BWIAnalytics.swift | 11 +++ bwi/MatomoAnalytics/BWIAnalyticsHelper.swift | 94 +++++++++++++++++++ .../PerformanceProfile.swift | 28 +----- 7 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 bwi/MatomoAnalytics/BWIAnalyticsHelper.swift diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 3a4ae5f81..1244e838c 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -36,6 +36,7 @@ @class VoiceBroadcastService; @class ComposerLinkActionBridgePresenter; @class PerformanceProfile; +@class BWIAnalyticsHelper; NS_ASSUME_NONNULL_BEGIN diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index e1562b51c..f6c3cd596 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -8179,6 +8179,7 @@ static CGSize kThreadListBarButtonItemImageSize; { [self.roomDataSource sendVoiceMessage:url additionalContentParams:nil mimeType:nil duration:duration samples:samples success:^(NSString *eventId) { MXLogDebug(@"Success with event id %@", eventId); + [self trackVoiceMessage: duration]; completion(YES); } failure:^(NSError *error) { MXLogError(@"Failed sending voice message"); @@ -8210,17 +8211,24 @@ static CGSize kThreadListBarButtonItemImageSize; if( [profile isLogable] ) { [self.roomDataSource.room members:^(MXRoomMembers *roomMembers) { NSUInteger noOfUsers = roomMembers.joinedMembers.count; - NSUInteger noOfDevices = 0; - for (MXRoomMember* member in roomMembers.joinedMembers) { - noOfDevices += [self.mainSession.crypto devicesForUser:member.userId].count; - } - [profile log2AnalyticsWithUsers:noOfUsers devices:noOfDevices]; + [BWIAnalyticsHelper getRoomDeviceCountWithRoom:self.roomDataSource.room completion:^(NSInteger deviceCount) { + [profile log2AnalyticsWithUsers:noOfUsers devices:deviceCount]; + }]; } failure:^(NSError *error) { }]; } } +// Bwi #4795: voice message +- (void) trackVoiceMessage:(NSInteger)duration { + [BWIAnalyticsHelper getRoomDeviceCountWithRoom:self.roomDataSource.room completion:^(NSInteger deviceCount) { + NSString *deviceCountString = [BWIAnalyticsHelper dimensionForDeviceCount: deviceCount]; + NSNumber *durationInSeconds = [NSNumber numberWithInteger:(duration / 1000)]; + [BWIAnalytics.sharedTracker trackEventWithDimensionWithCategory:@"Feature" action:@"SendVoiceMessage" dimension:deviceCountString value:durationInSeconds name:nil]; + }]; +} + #pragma mark - BWI Emoji History - (void) bwiAddedEmoji:(NSString*)emoji { diff --git a/RiotShareExtension/Shared/ForwardingShareItemSender.swift b/RiotShareExtension/Shared/ForwardingShareItemSender.swift index 3909812db..e87fa8c3f 100644 --- a/RiotShareExtension/Shared/ForwardingShareItemSender.swift +++ b/RiotShareExtension/Shared/ForwardingShareItemSender.swift @@ -53,10 +53,10 @@ class ForwardingShareItemSender: NSObject, ShareItemSenderProtocol { var localEcho: MXEvent? room.sendMessage(withContent: event.content, threadId: nil, localEcho: &localEcho) { result in switch result { + case .success(_): + self.trackForwardMessage(room: room) case .failure(let innerError): errors.append(innerError) - default: - break } dispatchGroup.leave() @@ -72,4 +72,12 @@ class ForwardingShareItemSender: NSObject, ShareItemSenderProtocol { success() } } + + func trackForwardMessage(room: MXRoom) { + BWIAnalyticsHelper.getRoomDeviceCount(room: room) { deviceCount in + let deviceCountString = BWIAnalyticsHelper.dimensionForDeviceCount(deviceCount) + let messageType = BWIAnalyticsHelper.getForwardingType(event: self.event) + BWIAnalytics.sharedTracker.trackEventWithDimension(category: "Feature", action: "ForwardMessage", dimension: deviceCountString, value: nil, name: messageType) + } + } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index 3c0db09b2..c3f44b40a 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -79,6 +79,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { guard let self = self else { return } self.pollEditFormViewModel.stopLoading() + self.log2Analytics(details: details, room: self.parameters.room) self.completion?() } failure: { [weak self] error in guard let self = self else { return } @@ -154,4 +155,28 @@ final class PollEditFormCoordinator: Coordinator, Presentable { return mapping[key] ?? EditFormPollType.disclosed } + + + // MARK: Bwi tracking + private func log2Analytics(details: EditFormPollDetails, room: MXRoom) { + BWIAnalyticsHelper.getRoomDeviceCount(room: room) { deviceCount in + var eventName: String + switch details.type { + case .undisclosed: + if details.showParticipants { + eventName = "undisclosed_show_participants" + } else { + eventName = "undisclosed" + } + case .disclosed: + if details.showParticipants { + eventName = "disclosed_show_participants" + } else { + eventName = "disclosed" + } + } + + BWIAnalytics.sharedTracker.trackEventWithDimension(category: "Feature", action: "SendPoll", dimension: BWIAnalyticsHelper.dimensionForDeviceCount(deviceCount), value: NSNumber(value: details.answerOptions.count), name: eventName) + } + } } diff --git a/bwi/MatomoAnalytics/BWIAnalytics.swift b/bwi/MatomoAnalytics/BWIAnalytics.swift index 5b82d975e..4a4e1c69c 100644 --- a/bwi/MatomoAnalytics/BWIAnalytics.swift +++ b/bwi/MatomoAnalytics/BWIAnalytics.swift @@ -195,6 +195,17 @@ import MatomoTracker } } } + + func trackEventWithDimension(category: String, action: String, dimension: String, value: NSNumber?, name: String?) { + if fastRunning { + // bwi: Analytics use custom config + if let dimensionIndex = analyticsConfig.selectedSendMessageDimensionIndex() { + matomo?.setDimension(dimension, forIndex: dimensionIndex) + matomo?.track(eventWithCategory: category, action: action, name: name, number: value, url:nil) // name optional unwrap? + matomo?.remove(dimensionAtIndex: dimensionIndex) + } + } + } } extension BWIAnalytics : MXAnalyticsDelegate { diff --git a/bwi/MatomoAnalytics/BWIAnalyticsHelper.swift b/bwi/MatomoAnalytics/BWIAnalyticsHelper.swift new file mode 100644 index 000000000..62f4f7f96 --- /dev/null +++ b/bwi/MatomoAnalytics/BWIAnalyticsHelper.swift @@ -0,0 +1,94 @@ +// +/* + * Copyright (c) 2023 BWI GmbH + * + * 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 class BWIAnalyticsHelper: NSObject { + + @objc static func getRoomDeviceCount(room: MXRoom, completion: @escaping(Int) -> ()) { + room.members { roomMembers in + var noOfDevices = 0 + for member in roomMembers?.joinedMembers ?? [MXRoomMember]() { + noOfDevices += room.mxSession.crypto.devices(forUser: member.userId).count + } + completion(noOfDevices) + } lazyLoadedMembers: { _ in + completion(0) + } failure: { error in + MXLog.error("[RoomAnalyticsHelper] Failed loading room", context: error) + completion(0) + } + } + + @objc static func getForwardingType(event: MXEvent) -> String? { + guard let messageType: MXMessageType = event.messageType else { + return nil + } + switch messageType { + case .text: + return "text" + case .image: + return "image" + case .video: + return "video" + case .audio: + if (event.content["org.matrix.msc2516.voice"] != nil) || (event.content["org.matrix.msc3245.voice"] != nil) { + return "voice_message" + } else { + return "audio" + } + case .emote: + return nil + case .notice: + return nil + case .location: + return "location" + case .file: + return "file" + case .custom(_): + return nil + } + } + + // MARK: custom dimensions + + @objc static func dimensionForDeviceCount(_ deviceCount: Int) -> String { + if deviceCount <= 0 { + return "Undefiniert" + } + if deviceCount <= 10 { + return "1 bis 10" + } + if deviceCount <= 100 { + return "11 bis 100" + } + if deviceCount <= 200 { + return "101 bis 200" + } + if deviceCount <= 500 { + return "201 bis 500" + } + if deviceCount <= 1000 { + return "501 bis 1000" + } + if deviceCount <= 2500 { + return "1001 bis 2500" + } else { + return "mehr als 2500" + } + } +} diff --git a/bwi/PerformanceProfiles/PerformanceProfile.swift b/bwi/PerformanceProfiles/PerformanceProfile.swift index b48aefe52..98b9b6d86 100644 --- a/bwi/PerformanceProfiles/PerformanceProfile.swift +++ b/bwi/PerformanceProfiles/PerformanceProfile.swift @@ -53,33 +53,7 @@ import Foundation func log2Analytics(users: Int, devices: Int) { if isLogable() { - BWIAnalytics.sharedTracker.trackSlowMessage(dimension: dimensionForDeviceCount(devices), value: Int(timeInterval*1000)) - } - } - - func dimensionForDeviceCount(_ deviceCount: Int) -> String { - if deviceCount <= 0 { - return "Undefiniert" - } - if deviceCount <= 10 { - return "1 bis 10" - } - if deviceCount <= 100 { - return "11 bis 100" - } - if deviceCount <= 200 { - return "101 bis 200" - } - if deviceCount <= 500 { - return "201 bis 500" - } - if deviceCount <= 1000 { - return "501 bis 1000" - } - if deviceCount <= 2500 { - return "1001 bis 2500" - } else { - return "mehr als 2500" + BWIAnalytics.sharedTracker.trackSlowMessage(dimension: BWIAnalyticsHelper.dimensionForDeviceCount(devices), value: Int(timeInterval*1000)) } } } From 26a3639d018a706051522085e069e38e0aebbf9e Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Thu, 22 Jun 2023 10:33:41 +0200 Subject: [PATCH 134/149] MESSENGER-4390 fix message forwarding and disable permalinks for messages in private rooms --- Riot/Modules/Room/RoomViewController.m | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index f6c3cd596..ef5c038cb 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4132,9 +4132,7 @@ static CGSize kThreadListBarButtonItemImageSize; if (selectedEvent.sentState == MXEventSentStateSent && !selectedEvent.isTimelinePollEvent && // Forwarding of live-location shares still to be implemented - selectedEvent.eventType != MXEventTypeBeaconInfo && - // bwi #4390: remove forwarding action if room is private - ![self.roomDataSource.room.summary.joinRule isEqualToString: kMXRoomJoinRuleInvite]) + selectedEvent.eventType != MXEventTypeBeaconInfo) { [self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] @@ -4186,9 +4184,7 @@ static CGSize kThreadListBarButtonItemImageSize; if (selectedEvent.sentState == MXEventSentStateSent && (attachment.type == MXKAttachmentTypeFile || attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo || - attachment.type == MXKAttachmentTypeVoiceMessage) && - // bwi #4390: remove forwarding action if room is private - ![self.roomDataSource.room.summary.joinRule isEqualToString: kMXRoomJoinRuleInvite]) { + attachment.type == MXKAttachmentTypeVoiceMessage)) { [self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] style:UIAlertActionStyleDefault @@ -4346,7 +4342,9 @@ static CGSize kThreadListBarButtonItemImageSize; } } - if (BWIBuildSettings.shared.messageDetailsAllowPermalink && !(self.roomDataSource.room.isDirect || self.roomDataSource.room.isPersonalNotesRoom || self.roomDataSource.room.summary.membership == MXMembershipInvite)) + if (BWIBuildSettings.shared.messageDetailsAllowPermalink && !(self.roomDataSource.room.isDirect || self.roomDataSource.room.isPersonalNotesRoom || + // bwi #4390: remove permalink action if room is private + [self.roomDataSource.room.summary.joinRule isEqualToString: kMXRoomJoinRuleInvite])) { [self.eventMenuBuilder addItemWithType:EventMenuItemTypePermalink action:[UIAlertAction actionWithTitle:[BWIL10n roomEventActionPermalink] From 45cb26e9149a0c8a30800b8ce18999c38535e45d Mon Sep 17 00:00:00 2001 From: Frank Rotermund Date: Thu, 22 Jun 2023 09:48:24 +0000 Subject: [PATCH 135/149] MESSENGER-4682 imprint from wellknown (and fix lab feature list) --- Config/BWIBuildSettings.swift | 3 ++ .../BuM-Beta/BWIBuildSettings+BuM-Beta.swift | 3 ++ Riot/Assets/de.lproj/Bwi.strings | 1 + Riot/Assets/en.lproj/Bwi.strings | 1 + Riot/Generated/BWIStrings.swift | 4 ++ .../Modules/Settings/SettingsViewController.m | 45 ++++++++++++++----- bwi/Wellknown/Wellknown+Bwi.swift | 14 ++++++ bwi/Wellknown/WellknownBWI.swift | 2 + 8 files changed, 61 insertions(+), 12 deletions(-) diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index 78bbe4c24..e383af59c 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -635,4 +635,7 @@ class BWIBuildSettings: NSObject { // MARK: Voice Broadcast var enableLabFeatureVoiceBroadcasts = false + + // MARK: WYSIWYG + var enableLabFeatureWYSIWYG = false } diff --git a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift index f8e3f4934..a7e52d29f 100644 --- a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift +++ b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift @@ -33,6 +33,9 @@ extension BWIBuildSettings { bwiLocationShareButtonVisible = false bwiLoginFlowLayout = false useRustEncryption = true + + enableLabFeatureVoiceBroadcasts = false + enableLabFeatureWYSIWYG = true } } diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index 548c289dd..f227619be 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -205,6 +205,7 @@ "settings_password_has_no_lowercase_letter" = "Das Passwort muss mindestens einen Kleinbuchstaben enthalten"; "settings_about" = "Erweitert"; "bwi_settings_ignored_users_text" = "Ignorierte Nutzer"; +"settings_imprint" = "Impressum"; // MARK: - Room Details diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index ee72e210f..3d567b52b 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -161,6 +161,7 @@ "settings_password_has_no_lowercase_letter" = "The password must include at least one lowercase letter"; "settings_deactivate_my_account" = "Deactivate my account"; "settings_enable_inapp_notifications" = "Enable In-App notifications"; +"settings_imprint" = "Imprint"; // MARK: - Room Details diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index 1698faedb..0c7105e8c 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -1523,6 +1523,10 @@ public class BWIL10n: NSObject { public static var settingsGroupMessages: String { return BWIL10n.tr("Bwi", "settings_group_messages") } + /// Impressum + public static var settingsImprint: String { + return BWIL10n.tr("Bwi", "settings_imprint") + } /// Mentions and Keywords public static var settingsMentionsAndKeywords: String { return BWIL10n.tr("Bwi", "settings_mentions_and_keywords") diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 56a3f6565..a2dc84900 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -210,6 +210,7 @@ typedef NS_ENUM(NSUInteger, ABOUT) ABOUT_TERM_CONDITIONS_INDEX, ABOUT_ACCEPTABLE_USE_INDEX, ABOUT_PRIVACY_INDEX, + ABOUT_IMPRINT_INDEX, ABOUT_THIRD_PARTY_INDEX, ABOUT_SUPPORT_INDEX, ABOUT_SHOW_NSFW_ROOMS_INDEX, @@ -219,7 +220,7 @@ typedef NS_ENUM(NSUInteger, ABOUT) ABOUT_CLEAR_CACHE_INDEX, ABOUT_REPORT_BUG_INDEX, ABOUT_NETIQUETTE_INDEX, - ABOUT_ACCESSIBILITY_DECLARATION_INDEX + ABOUT_ACCESSIBILITY_DECLARATION_INDEX, }; typedef NS_ENUM(NSUInteger, LABS_ENABLE) @@ -733,6 +734,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [sectionAbout addRowWithTag:ABOUT_ACCESSIBILITY_DECLARATION_INDEX]; } [sectionAbout addRowWithTag:ABOUT_THIRD_PARTY_INDEX]; + // bwi #4682 - Show Imprint + if ([self.mainSession.homeserverWellknown imprintURL]) { + [sectionAbout addRowWithTag:ABOUT_IMPRINT_INDEX]; + } sectionAbout.headerTitle = VectorL10n.settingsAbout; [sectionAbout addRowWithTag:ABOUT_MARK_ALL_AS_READ_INDEX]; @@ -755,21 +760,23 @@ ChangePasswordCoordinatorBridgePresenterDelegate> if (BWIBuildSettings.shared.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; - [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; - [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; - [sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS]; - if (BWIBuildSettings.shared.locationSharingEnabled) - { - [sectionLabs addRowWithTag:LABS_ENABLE_LIVE_LOCATION_SHARING]; - } + [sectionLabs addRowWithTag:LABS_ENABLE_NEW_SESSION_MANAGER]; /* bwi: disabled for our apps - [sectionLabs addRowWithTag:LABS_ENABLE_NEW_CLIENT_INFO_FEATURE]; + [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; + [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; + [sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS]; + [sectionLabs addRowWithTag:LABS_ENABLE_NEW_CLIENT_INFO_FEATURE]; + [sectionLabs addRowWithTag:LABS_ENABLE_LIVE_LOCATION_SHARING]; */ - if (@available(iOS 15.0, *)) - { - [sectionLabs addRowWithTag:LABS_ENABLE_WYSIWYG_COMPOSER]; + + if (BWIBuildSettings.shared.enableLabFeatureWYSIWYG) { + if (@available(iOS 15.0, *)) + { + [sectionLabs addRowWithTag:LABS_ENABLE_WYSIWYG_COMPOSER]; + } } + // bwi: disabled for our apps if (BWIBuildSettings.shared.enableLabFeatureVoiceBroadcasts) { @@ -2697,6 +2704,16 @@ ChangePasswordCoordinatorBridgePresenterDelegate> cell = thirdPartyCell; } + else if (row == ABOUT_IMPRINT_INDEX) + { + MXKTableViewCell *imprintCell = [self getDefaultTableViewCell:tableView]; + + imprintCell.textLabel.text = [BWIL10n settingsImprint]; + + [imprintCell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; + + cell = imprintCell; + } else if (row == ABOUT_SHOW_NSFW_ROOMS_INDEX) { MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; @@ -3343,6 +3360,10 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [self pushViewController:webViewViewController]; } + else if (row == ABOUT_IMPRINT_INDEX) + { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[self.mainSession.homeserverWellknown imprintURL]] options:@{} completionHandler:nil]; + } } else if (section == SECTION_TAG_USER_SETTINGS) { diff --git a/bwi/Wellknown/Wellknown+Bwi.swift b/bwi/Wellknown/Wellknown+Bwi.swift index 7040fc475..ae290aada 100644 --- a/bwi/Wellknown/Wellknown+Bwi.swift +++ b/bwi/Wellknown/Wellknown+Bwi.swift @@ -98,4 +98,18 @@ public extension MXWellKnown { return nil } } + + @objc func imprintURL() -> String? { + do { + guard let bwiDict = self.jsonDictionary()["de.bwi"] as? [String : Any] else { + return nil + } + + let bwi = try WellknownBWI(dict: bwiDict) + return bwi.imprintURL + } + catch { + return nil + } + } } diff --git a/bwi/Wellknown/WellknownBWI.swift b/bwi/Wellknown/WellknownBWI.swift index 6cfc4bc92..6b166a20a 100644 --- a/bwi/Wellknown/WellknownBWI.swift +++ b/bwi/Wellknown/WellknownBWI.swift @@ -19,6 +19,7 @@ import Foundation struct WellknownBWI { let dataPrivacyURL: String? + let imprintURL: String? init(dict: [String: Any]) throws { let jsonData = try JSONSerialization.data(withJSONObject: dict, options: []) @@ -30,5 +31,6 @@ struct WellknownBWI { extension WellknownBWI: Decodable { enum CodingKeys: String, CodingKey { case dataPrivacyURL = "data_privacy_url" + case imprintURL = "imprint_url" } } From c31a33a33b28f02a1b6a08ca49cfbcd786804971 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Fri, 23 Jun 2023 10:48:24 +0200 Subject: [PATCH 136/149] activate lab setting voice broadcast and enable session manager by default --- Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift | 3 ++- Riot/Modules/Settings/SettingsViewController.m | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift index a7e52d29f..732aefc93 100644 --- a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift +++ b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift @@ -34,7 +34,8 @@ extension BWIBuildSettings { bwiLoginFlowLayout = false useRustEncryption = true - enableLabFeatureVoiceBroadcasts = false + enableLabFeatureVoiceBroadcasts = true + enableNewSessionManagerByDefault = true enableLabFeatureWYSIWYG = true } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index a2dc84900..751ab69c0 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -760,8 +760,9 @@ ChangePasswordCoordinatorBridgePresenterDelegate> if (BWIBuildSettings.shared.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; - - [sectionLabs addRowWithTag:LABS_ENABLE_NEW_SESSION_MANAGER]; + if (!BWIBuildSettings.shared.enableNewSessionManagerByDefault) { + [sectionLabs addRowWithTag:LABS_ENABLE_NEW_SESSION_MANAGER]; + } /* bwi: disabled for our apps [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; From 14f4d77710d13a2fca552917e09e918431afc88d Mon Sep 17 00:00:00 2001 From: Juergen Wischer Date: Mon, 26 Jun 2023 08:37:46 +0000 Subject: [PATCH 137/149] MESSENGER-4897 new hashes --- Config/BWIBuildSettings.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index e383af59c..17450cf24 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -187,7 +187,16 @@ class BWIBuildSettings: NSObject { "4d5b6dcf02396274be58a69c4bbeba975b529f6b19c504fc99a37892ee1cf0b5", "0d157119821bd9d76ac4f24c7f44f56e6bb5b766a6d5ee7dad6634420e79271a", "e3573fe09d518cce80cececedf80f8e0020cbc150f22db8b64827bff2e27abd9", - "b76a62ccd8ea70d01c3a35ec3839e49ed2c83c8e3276f40a1b2c2cdf7cd77d01" + "b76a62ccd8ea70d01c3a35ec3839e49ed2c83c8e3276f40a1b2c2cdf7cd77d01", + "4a610a4d5fd3d8a1e1fd5669abdf8e0c5f7f5ff0c6b559e0f360cfa092ecb115", + "32752f6d21f3005587941415cd64892ee28c19e6e01ed307edf9ddf4f6a91583", + "704c6eaa107b13ef0694eb7ddd043bb6f595b53670a2e0c3c16e199947a9e013", + "6921f031357cf63fb8538d9a1d1973efae95899907fdbf05a05082b6d1a6d0fb", + "9f960fc663f5eaae67eecff75b137dea130b3ab1cf889c45fc74c688a48aea30", + "160c35279484a027031b131583f3f203b1166306bab214355b00cf28502bce11", + "d5a7298dde23aa0269c4cbd3b1a543e6ede94ce78fc20e4bfb888eb6057b5c52", + "00136d830dd2acd5047efcf8429e939ef7ef97a84bef1930df86aace3f855265", + "64cbbeea37237814445b65c941d010b9d5d024e4c584a476864b00c7c9909bce" ] // use a different badge color if the user was mentioned in a room From f33d85c1431ea21a6adf198977a2209cb917af0d Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Mon, 26 Jun 2023 12:19:39 +0200 Subject: [PATCH 138/149] MESSENGER-4795 add matomo event names --- Riot/Modules/Application/LegacyAppDelegate.m | 6 +++--- Riot/Modules/Room/RoomViewController.m | 2 +- bwi/MatomoAnalytics/BWIAnalytics.swift | 8 +++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index fb3bffe7c..3c998d90e 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -791,7 +791,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [application keyWindow].accessibilityIgnoresInvertColors = YES; [BWIAnalytics.sharedTracker firstCall]; - [BWIAnalytics.sharedTracker trackEvent:@"Session" action:@"PinLogin"]; + [BWIAnalytics.sharedTracker trackEventWithCategory:@"Session" action:@"PinLogin" name:@"pin_login_default" number:nil]; self.isPinUnlocked = true; @@ -2253,7 +2253,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [topVC startActivityIndicator]; } - [BWIAnalytics.sharedTracker trackEvent:@"Session" action:@"Logout"]; + [BWIAnalytics.sharedTracker trackEventWithCategory:@"Session" action:@"Logout" name:@"logout_default" number:nil]; [BWIAnalytics.sharedTracker dispatchAll]; [self logoutSendingRequestServer:YES completion:^(BOOL isLoggedOut) { @@ -2336,7 +2336,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [topVC startActivityIndicator]; } - [BWIAnalytics.sharedTracker trackEvent:@"Session" action:@"Logout"]; + [BWIAnalytics.sharedTracker trackEventWithCategory:@"Session" action:@"Logout" name:@"logout_default" number:nil]; [BWIAnalytics.sharedTracker dispatchAll]; [self logoutSendingRequestServer:YES completion:^(BOOL isLoggedOut) { diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index f6c3cd596..41eadac9e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -8225,7 +8225,7 @@ static CGSize kThreadListBarButtonItemImageSize; [BWIAnalyticsHelper getRoomDeviceCountWithRoom:self.roomDataSource.room completion:^(NSInteger deviceCount) { NSString *deviceCountString = [BWIAnalyticsHelper dimensionForDeviceCount: deviceCount]; NSNumber *durationInSeconds = [NSNumber numberWithInteger:(duration / 1000)]; - [BWIAnalytics.sharedTracker trackEventWithDimensionWithCategory:@"Feature" action:@"SendVoiceMessage" dimension:deviceCountString value:durationInSeconds name:nil]; + [BWIAnalytics.sharedTracker trackEventWithDimensionWithCategory:@"Feature" action:@"SendVoiceMessage" dimension:deviceCountString value:durationInSeconds name:@"send_voice_message_default"]; }]; } diff --git a/bwi/MatomoAnalytics/BWIAnalytics.swift b/bwi/MatomoAnalytics/BWIAnalytics.swift index 4a4e1c69c..a4b43efd6 100644 --- a/bwi/MatomoAnalytics/BWIAnalytics.swift +++ b/bwi/MatomoAnalytics/BWIAnalytics.swift @@ -167,6 +167,12 @@ import MatomoTracker } } + func trackEvent(category: String, action: String, name: String, number: NSNumber?) { + if fastRunning { + matomo?.track(eventWithCategory: category, action: action, name: name, number: number, url: nil) + } + } + func trackBwiDuration(_ duration: TimeInterval, _ category: String, _ name: String) { if fastRunning { matomo?.track(eventWithCategory: "Metrics", action: category, name: name, number: NSNumber(value: duration), url:nil) @@ -190,7 +196,7 @@ import MatomoTracker // bwi: Analytics use custom config if let dimensionIndex = analyticsConfig.selectedSendMessageDimensionIndex() { matomo?.setDimension(dimension, forIndex: dimensionIndex) - matomo?.track(eventWithCategory: "Performance", action: "SendMessage", name: nil, number: NSNumber(value: value), url:nil) + matomo?.track(eventWithCategory: "Performance", action: "SendMessage", name: "send_message_default", number: NSNumber(value: value), url:nil) matomo?.remove(dimensionAtIndex: dimensionIndex) } } From 56da60de836733c40e89c9042bc2cd4f00811e4f Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Mon, 26 Jun 2023 12:22:44 +0000 Subject: [PATCH 139/149] Feature/4753 bum test app --- .../BuM-Open/AppIdentifiers-bum-open.xcconfig | 39 +++++++ .../BuM-Open/BWIBuildSettings+BuM-Open.swift | 34 ++++++ Config/copyOpenConfig.sh | 20 ++++ Podfile | 20 ++++ Riot/target-bum-beta.yml | 1 + Riot/target-bum-open.yml | 107 ++++++++++++++++++ Riot/target-messenger.yml | 1 + Riot/target.yml | 1 + project.yml | 1 + 9 files changed, 224 insertions(+) create mode 100644 Config/BuM-Open/AppIdentifiers-bum-open.xcconfig create mode 100644 Config/BuM-Open/BWIBuildSettings+BuM-Open.swift create mode 100755 Config/copyOpenConfig.sh create mode 100644 Riot/target-bum-open.yml diff --git a/Config/BuM-Open/AppIdentifiers-bum-open.xcconfig b/Config/BuM-Open/AppIdentifiers-bum-open.xcconfig new file mode 100644 index 000000000..cecebe704 --- /dev/null +++ b/Config/BuM-Open/AppIdentifiers-bum-open.xcconfig @@ -0,0 +1,39 @@ +// +// Copyright 2021 Vector Creations 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. +// + + +// App identity +BUNDLE_DISPLAY_NAME = BuM-Open +BASE_BUNDLE_IDENTIFIER = de.bwi.messenger-open +APPLICATION_GROUP_IDENTIFIER = group.de.messenger-open +APPLICATION_SCHEME = element + +// Team +DEVELOPMENT_TEAM = Q111Q11QQ1 + + +// Provisioning profiles +RIOT_PROVISIONING_PROFILE_SPECIFIER = Vector App Store +RIOT_PROVISIONING_PROFILE = 7579fa6f-9887-415e-90fc-2c7acd8812e6 + +NSE_PROVISIONING_PROFILE_SPECIFIER = "Vector NSE: App Store" +NSE_PROVISIONING_PROFILE = e73107b2-1bfe-4615-be3e-39fd4dcb2af0 + +SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER = "Vector Share Extension: App Store" +SHARE_EXTENSION_PROVISIONING_PROFILE = 8c797ca0-0440-49bd-be8d-11d761152995 + +SIRI_INTENTS_PROVISIONING_PROFILE_SPECIFIER = "Vector Siri Intents: App Store" +SIRI_INTENTS_PROVISIONING_PROFILE = 1690e81a-5ad3-4d99-b578-02693579be71 diff --git a/Config/BuM-Open/BWIBuildSettings+BuM-Open.swift b/Config/BuM-Open/BWIBuildSettings+BuM-Open.swift new file mode 100644 index 000000000..823147272 --- /dev/null +++ b/Config/BuM-Open/BWIBuildSettings+BuM-Open.swift @@ -0,0 +1,34 @@ +// +/* + * Copyright (c) 2023 BWI GmbH + * + * 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 BWIBuildSettings { + + func overrideTargetSpecificSettings() { + secondaryAppName = "BundesMessenger" + locationSharingEnabled = false + bwiLocationShareButtonVisible = false + bwiLoginFlowLayout = false + authScreenShowTestServerOptions = false + + enableNewSessionManagerByDefault = true + + bwiEnableLoginProtection = false + } + +} diff --git a/Config/copyOpenConfig.sh b/Config/copyOpenConfig.sh new file mode 100755 index 000000000..392f83a60 --- /dev/null +++ b/Config/copyOpenConfig.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# setConfig.sh +# +# Copyright (c) 2023 BWI GmbH +# +# 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. +# + +cp -vf ../Config/BuM-Open/AppIdentifiers-bum-open.xcconfig ../Config/AppIdentifiers.xcconfig diff --git a/Podfile b/Podfile index f54632804..c2f8d59bc 100644 --- a/Podfile +++ b/Podfile @@ -155,6 +155,26 @@ abstract_target 'RiotPods' do pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] end + + target "BuM-Open" do + import_MatrixSDK + import_MatrixKit_pods + + import_SwiftUI_pods + + pod 'UICollectionViewLeftAlignedLayout', '~> 1.0.2' + pod 'UICollectionViewRightAlignedLayout', '~> 0.0.3' + pod 'KTCenterFlowLayout', '~> 1.3.1' + pod 'FlowCommoniOS', '~> 1.12.0' + pod 'DTTJailbreakDetection', '~> 0.4.0' + pod 'ReadMoreTextView', '~> 3.0.1' + pod 'SwiftBase32', '~> 0.9.0' + pod 'SwiftJWT', '~> 3.6.200' + pod 'SideMenu', '~> 6.5' + pod 'DSWaveformImage', '~> 6.1.1' + + pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] + end target "RiotSwiftUI" do import_SwiftUI_pods diff --git a/Riot/target-bum-beta.yml b/Riot/target-bum-beta.yml index 1351d3fa0..3a39d7c17 100644 --- a/Riot/target-bum-beta.yml +++ b/Riot/target-bum-beta.yml @@ -80,6 +80,7 @@ targets: excludes: - "AppIdentifiers.xcconfig" - "BuM" + - "BuM-Open" - "*.sh" - path: . excludes: diff --git a/Riot/target-bum-open.yml b/Riot/target-bum-open.yml new file mode 100644 index 000000000..60bbc951b --- /dev/null +++ b/Riot/target-bum-open.yml @@ -0,0 +1,107 @@ +name: BuM-Open + +schemes: + BuM-Open: + analyze: + config: Debug + archive: + config: Release + build: + targets: + BuM-Open: + - running + - testing + - profiling + - analyzing + - archiving + profile: + config: Release + run: + config: Debug + disableMainThreadChecker: true + test: + config: Debug + disableMainThreadChecker: true + gatherCoverageData: true + language: "de" + region: "DE" + environmentVariables: + username: + defaultpin: + defaultpassphrase: + defaultpassword: + targets: + - RiotTests + +targets: + BuM-Open: + type: application + platform: iOS + + dependencies: + - target: RiotNSE + - target: DesignKit + - target: CommonKit + - package: AnalyticsEvents + - package: Mapbox + - package: OrderedCollections + - package: SwiftOGG + - package: Lottie + - package: WysiwygComposer + - package: DeviceKit + + configFiles: + Debug: Debug.xcconfig + Release: Release.xcconfig + + preBuildScripts: + - name: ⚠️ SwiftLint + runOnlyWhenInstalling: false + shell: /bin/sh + script: "${PODS_ROOT}/SwiftLint/swiftlint\n" + - name: 🛠 SwiftGen + runOnlyWhenInstalling: false + shell: /bin/sh + script: "${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config Tools/SwiftGen/swiftgen-config.yml\n" + + sources: + - path: ../RiotSwiftUI/Modules + # Riot will provide it's own LocaleProviderType so exclude. + # Riot will provide it's own LocaleProviderType so exclude. + excludes: + - "Common/Locale/LocaleProvider.swift" + - "**/Test/**" + - path: ../Tools + excludes: + - "Logs" + - "Release" + - "Templates/*.sh" + - path: ../Config + excludes: + - "BuM-Beta" + - "BuM" + - "AppIdentifiers.xcconfig" + - "*.sh" + - path: . + excludes: + - "Modules/Room/EmojiPicker/Data/EmojiMart/EmojiJSONStore.swift" + - "Modules/Analytics/Test/Unit/BWIAnalyticsTests.swift" + - "**/*.strings" # Exclude all strings files + - path: ../bwi + excludes: + - "Tests" + - "ServerURLs/serverurls.json" + - path: ../RiotShareExtension/Shared + - path: Modules/MatrixKit + excludes: + - "**/*.md" # excludes all files with the .md extension + # Add separately localizable files + # Once a language has enough translations (>80%), it must be declared here + - path: Assets/en.lproj/InfoPlist.strings + - path: Assets/en.lproj/Localizable.strings + - path: Assets/en.lproj/Vector.strings + - path: Assets/en.lproj/Bwi.strings + - path: Assets/de.lproj/InfoPlist.strings + - path: Assets/de.lproj/Localizable.strings + - path: Assets/de.lproj/Vector.strings + - path: Assets/de.lproj/Bwi.strings diff --git a/Riot/target-messenger.yml b/Riot/target-messenger.yml index 8b53a33c5..654c4a78c 100644 --- a/Riot/target-messenger.yml +++ b/Riot/target-messenger.yml @@ -79,6 +79,7 @@ targets: - path: ../Config excludes: - "BuM-Beta" + - "BuM-Open" - "*.sh" - path: . excludes: diff --git a/Riot/target.yml b/Riot/target.yml index 48f4a6ebf..cfbe335a3 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -85,6 +85,7 @@ targets: - "beta" - "bwi" - "BuM-Beta" + - "BuM-Open" - "*.sh" - path: . excludes: diff --git a/project.yml b/project.yml index 1067da832..76fbbb359 100644 --- a/project.yml +++ b/project.yml @@ -30,6 +30,7 @@ include: - path: Riot/target.yml - path: Riot/target-messenger.yml - path: Riot/target-bum-beta.yml + - path: Riot/target-bum-open.yml - path: RiotTests/target.yml - path: RiotShareExtension/target.yml - path: SiriIntents/target.yml From 982879c47ece87ffc707d5ace277928a98f7faee Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Wed, 28 Jun 2023 05:59:03 +0000 Subject: [PATCH 140/149] Feature/4889 deactivate rust for prod --- Config/CommonConfiguration.swift | 23 +- Config/Configurable.swift | 3 + Podfile | 2 +- Riot/Assets/en.lproj/Vector.strings | 3 + .../MXBugReportRestClient+Riot.swift | 1 + Riot/Experiments/CryptoSDKFeature.swift | 120 +++++++++ Riot/Generated/Strings.swift | 12 + Riot/Modules/Analytics/Analytics.swift | 2 +- .../Analytics/SentryMonitoringClient.swift | 3 + Riot/Modules/Application/LegacyAppDelegate.m | 232 +++++++++++++++++- .../AuthenticationCoordinator.swift | 6 + .../LegacyAuthenticationCoordinator.swift | 6 + .../SessionVerificationListener.swift | 15 +- Riot/Modules/Call/CallViewController.m | 30 ++- .../AllChats/AllChatsViewController.swift | 3 +- .../LaunchLoading/LaunchLoadingView.swift | 3 + .../MatrixKit/Models/Account/MXKAccount.m | 10 +- Riot/Modules/Room/RoomViewController.m | 17 +- .../RoomKeyRequestViewController.h | 62 +++++ .../RoomKeyRequestViewController.m | 195 +++++++++++++++ .../Modules/Settings/SettingsViewController.m | 44 +++- .../UserDevices/UsersDevicesViewController.m | 18 +- RiotNSE/NotificationService.swift | 14 +- RiotShareExtension/Shared/ShareManager.m | 5 + .../Service/MatrixSDK/QRLoginService.swift | 11 + .../Experiments/CryptoSDKFeatureTests.swift | 79 ++++++ .../SendMessage/SendMessageIntentHandler.m | 6 + 27 files changed, 889 insertions(+), 36 deletions(-) create mode 100644 Riot/Experiments/CryptoSDKFeature.swift create mode 100644 Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h create mode 100644 Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m create mode 100644 RiotTests/Experiments/CryptoSDKFeatureTests.swift diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index 8e00b0cdc..4b6068a96 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -91,7 +91,8 @@ class CommonConfiguration: NSObject, Configurable { sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature - sdkOptions.cryptoMigrationDelegate = self + // Configure Crypto SDK feature deciding which crypto module to use + sdkOptions.cryptoSDKFeature = CryptoSDKFeature.shared } private func makeASCIIUserAgent() -> String? { @@ -167,16 +168,14 @@ class CommonConfiguration: NSObject, Configurable { if RiotSettings.shared.allowStunServerFallback, let stunServerFallback = BWIBuildSettings.shared.stunServerFallbackUrlString { callManager.fallbackSTUNServer = stunServerFallback } - } -} - -extension CommonConfiguration: MXCryptoV2MigrationDelegate { - var needsVerificationUpgrade: Bool { - get { - RiotSettings.shared.showVerificationUpgradeAlert - } - set { - RiotSettings.shared.showVerificationUpgradeAlert = newValue - } } + + + // MARK: - Per loaded matrix session settings + + func setupSettingsWhenLoaded(for matrixSession: MXSession) { + // Do not warn for unknown devices. We have cross-signing now + (matrixSession.crypto as? MXLegacyCrypto)?.warnOnUnknowDevices = false + } + } diff --git a/Config/Configurable.swift b/Config/Configurable.swift index 2f1c46a03..acfb97605 100644 --- a/Config/Configurable.swift +++ b/Config/Configurable.swift @@ -24,4 +24,7 @@ import MatrixSDK // MARK: - Per matrix session settings func setupSettings(for matrixSession: MXSession) + + // MARK: - Per loaded matrix session settings + func setupSettingsWhenLoaded(for matrixSession: MXSession) } diff --git a/Podfile b/Podfile index c2f8d59bc..2405dd072 100644 --- a/Podfile +++ b/Podfile @@ -43,7 +43,7 @@ when String # specific MatrixSDK released version $matrixSDKVersionSpec = $matrixSDKVersion end -$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.10_bwi_beta' } +$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.10_bwi_4889' } # Method to import the MatrixSDK def import_MatrixSDK diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 690743765..247f3346a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -822,6 +822,9 @@ Tap the + to start adding people."; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast"; +"settings_labs_enable_crypto_sdk" = "Rust end-to-end encryption"; +"settings_labs_confirm_crypto_sdk" = "Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution."; +"settings_labs_disable_crypto_sdk" = "Rust end-to-end encryption (log out to disable)"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Categories/MXBugReportRestClient+Riot.swift b/Riot/Categories/MXBugReportRestClient+Riot.swift index 953180160..55fb902d7 100644 --- a/Riot/Categories/MXBugReportRestClient+Riot.swift +++ b/Riot/Categories/MXBugReportRestClient+Riot.swift @@ -70,6 +70,7 @@ extension MXBugReportRestClient { // SDKs userInfo["matrix_sdk_version"] = MatrixSDKVersion + userInfo["crypto_module"] = MXSDKOptions.sharedInstance().cryptoModuleId if let crypto = mainAccount?.mxSession?.crypto { userInfo["crypto_module_version"] = crypto.version } diff --git a/Riot/Experiments/CryptoSDKFeature.swift b/Riot/Experiments/CryptoSDKFeature.swift new file mode 100644 index 000000000..1e151fe79 --- /dev/null +++ b/Riot/Experiments/CryptoSDKFeature.swift @@ -0,0 +1,120 @@ +// +// Copyright 2023 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 MatrixSDKCrypto + +/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status +/// of `CryptoSDK`, and which uses feature flags to control rollout availability. +/// +/// The implementation uses both remote and local feature flags to control the availability of `CryptoSDK`. +/// Whilst remote is more convenient in that it allows changes to the rollout without new app releases, +/// it is not available to all users because it requires data tracking user consent. Remote therefore +/// represents the safer, albeit limited rollout strategy, whereas the local feature flags allows eventually +/// targetting all users, but each target change requires new app release. +/// +/// Additionally users can manually enable this feature from the settings if they are not already in the +/// feature group. +@objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature { + @objc static let shared = CryptoSDKFeature() + + var isEnabled: Bool { + BWIBuildSettings.shared.useRustEncryption + } + + var needsVerificationUpgrade: Bool { + get { + return RiotSettings.shared.showVerificationUpgradeAlert + } + set { + RiotSettings.shared.showVerificationUpgradeAlert = newValue + } + } + + private static let FeatureName = "ios-crypto-sdk" + private static let FeatureNameV2 = "ios-crypto-sdk-v2" + + private let remoteFeature: RemoteFeaturesClientProtocol + private let localFeature: PhasedRolloutFeature + + init( + remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared, + localTargetPercentage: Double = 1 + ) { + var targetPercentage = 0.0 + if BWIBuildSettings.shared.useRustEncryption { + targetPercentage = 1.0 + } + self.remoteFeature = remoteFeature + self.localFeature = PhasedRolloutFeature( + name: Self.FeatureName, + targetPercentage: targetPercentage + ) + } + + func enable() { + RiotSettings.shared.enableCryptoSDK = true + Analytics.shared.trackCryptoSDKEnabled() + + MXLog.debug("[CryptoSDKFeature] Crypto SDK enabled") + } + + func enableIfAvailable(forUserId userId: String!) { + guard !isEnabled else { + MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is already enabled") + return + } + + guard let userId else { + MXLog.failure("[CryptoSDKFeature] enableIfAvailable: Missing user id") + return + } + + guard isFeatureEnabled(userId: userId) else { + MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is currently not available for this user") + return + } + + MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature has become available for this user and will be enabled") + enable() + } + + @objc func canManuallyEnable(forUserId userId: String!) -> Bool { + guard let userId else { + MXLog.failure("[CryptoSDKFeature] canManuallyEnable: Missing user id") + return false + } + + // User can manually enable only if not already within the automatic feature group + return !isFeatureEnabled(userId: userId) + } + + @objc func reset() { + RiotSettings.shared.enableCryptoSDK = false + MXLog.debug("[CryptoSDKFeature] Crypto SDK disabled") + } + + private func isFeatureEnabled(userId: String) -> Bool { + // This feature includes app version with a bug, and thus will not be rolled out to 100% users + remoteFeature.isFeatureEnabled(Self.FeatureName) + + // Second version of the remote feature with a bugfix and released eventually to 100% users + || remoteFeature.isFeatureEnabled(Self.FeatureNameV2) + + // Local feature + || localFeature.isEnabled(userId: userId) + } +} diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 46da50d12..e26d57d37 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -7707,10 +7707,18 @@ public class VectorL10n: NSObject { public static var settingsLabs: String { return VectorL10n.tr("Vector", "settings_labs") } + /// Please be advised that as this feature is still in its experimental stage, it may not function as expected and could potentially have unintended consequences. To revert the feature, simply log out and log back in. Use at your own discretion and with caution. + public static var settingsLabsConfirmCryptoSdk: String { + return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk") + } /// Create conference calls with jitsi public static var settingsLabsCreateConferenceWithJitsi: String { return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi") } + /// Rust end-to-end encryption (log out to disable) + public static var settingsLabsDisableCryptoSdk: String { + return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk") + } /// End-to-End Encryption public static var settingsLabsE2eEncryption: String { return VectorL10n.tr("Vector", "settings_labs_e2e_encryption") @@ -7723,6 +7731,10 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableAutoReportDecryptionErrors: String { return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors") } + /// Rust end-to-end encryption + public static var settingsLabsEnableCryptoSdk: String { + return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk") + } /// Live location sharing - share current location (active development, and temporarily, locations persist in room history) public static var settingsLabsEnableLiveLocationSharing: String { return VectorL10n.tr("Vector", "settings_labs_enable_live_location_sharing") diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index d6f8b6fa3..5e5b20ece 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -274,7 +274,7 @@ extension Analytics { func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { let event = AnalyticsEvent.Error( context: context, - cryptoModule: .Rust, + cryptoModule: MXSDKOptions.sharedInstance().enableCryptoSDK ? .Rust : .Native, domain: .E2EE, name: reason.errorName ) diff --git a/Riot/Modules/Analytics/SentryMonitoringClient.swift b/Riot/Modules/Analytics/SentryMonitoringClient.swift index 54933a7ab..78450551b 100644 --- a/Riot/Modules/Analytics/SentryMonitoringClient.swift +++ b/Riot/Modules/Analytics/SentryMonitoringClient.swift @@ -46,6 +46,9 @@ struct SentryMonitoringClient { if let message = event.message?.formatted { event.fingerprint = [message] } + event.tags = [ + "crypto_module": MXSDKOptions.sharedInstance().cryptoModuleId + ] MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)") return event } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 3c998d90e..8824f36a1 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -34,6 +34,7 @@ #import "ContactDetailsViewController.h" #import "BugReportViewController.h" +#import "RoomKeyRequestViewController.h" #import "DecryptionFailureTracker.h" #import "Tools.h" @@ -114,6 +115,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni id roomKeyRequestObserver; id roomKeyRequestCancellationObserver; + /** + If any the currently displayed sharing key dialog + */ + RoomKeyRequestViewController *roomKeyRequestViewController; + /** Incoming key verification requests observers */ @@ -1888,6 +1894,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // start the call service [self.callPresenter start]; + [self.configuration setupSettingsWhenLoadedFor:mxSession]; + // Register to user new device sign in notification [self registerUserDidSignInOnNewDeviceNotificationForSession:mxSession]; @@ -1896,6 +1904,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Register to new key verification request [self registerNewRequestNotificationForSession:mxSession]; + [self checkLocalPrivateKeysInSession:mxSession]; + [self.pushNotificationService checkPushKitPushersInSession:mxSession]; } else if (mxSession.state == MXSessionStateRunning) @@ -2123,6 +2133,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // If any, disable the no VoIP support workaround [self disableNoVoIPOnMatrixSession:mxSession]; + // Disable listening of incoming key share requests + [self disableRoomKeyRequestObserver:mxSession]; + // Disable listening of incoming key verification requests [self disableIncomingKeyVerificationObserver:mxSession]; @@ -2365,6 +2378,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Clear cache [self clearCache]; + // Reset Crypto SDK configuration (labs flag for which crypto module to use) + [CryptoSDKFeature.shared reset]; + // Reset key backup banner preferences [SecureBackupBannerPreferences.shared reset]; @@ -2478,6 +2494,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni case MXSessionStateSyncInProgress: // Stay in launching during the first server sync if the store is empty. isLaunching = (mainSession.rooms.count == 0 && launchAnimationContainerView); + + if (mainSession.crypto.crossSigning && mainSession.crypto.crossSigning.state == MXCrossSigningStateCrossSigningExists && [mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + [(MXLegacyCrypto *)mainSession.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil]; + } break; case MXSessionStateRunning: self.clearingCache = NO; @@ -2537,6 +2558,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // This is the time to check existing requests MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests"); + [self checkPendingRoomKeyRequests]; [self checkPendingIncomingKeyVerificationsInSession:mainSession]; // TODO: When we will have an application state, we will do all of this in a dedicated initialisation state @@ -2545,6 +2567,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { MXLogDebug(@"[AppDelegate] handleAppState: Set up observers for the crypto module"); + // Enable listening of incoming key share requests + [self enableRoomKeyRequestObserver:mainSession]; + // Enable listening of incoming key verification requests [self enableIncomingKeyVerificationObserver:mainSession]; } @@ -2588,7 +2613,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [window addSubview:launchLoadingView]; } - */ + */ /* bwi: 4782 - new code from nv MXSession *mainSession = self.mxSessions.firstObject; @@ -2708,6 +2733,38 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni #endif } +- (void)checkLocalPrivateKeysInSession:(MXSession*)mxSession +{ + if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + return; + } + MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto; + + MXRecoveryService *recoveryService = mxSession.crypto.recoveryService; + NSUInteger keysCount = 0; + if ([recoveryService hasSecretWithSecretId:MXSecretId.keyBackup]) + { + keysCount++; + } + if ([recoveryService hasSecretWithSecretId:MXSecretId.crossSigningUserSigning]) + { + keysCount++; + } + if ([recoveryService hasSecretWithSecretId:MXSecretId.crossSigningSelfSigning]) + { + keysCount++; + } + + if ((keysCount > 0 && keysCount < 3) + || (mxSession.crypto.crossSigning.canTrustCrossSigning && !mxSession.crypto.crossSigning.canCrossSign)) + { + // We should have 3 of them. If not, request them again as mitigation + MXLogDebug(@"[AppDelegate] checkLocalPrivateKeysInSession: request keys because keysCount = %@", @(keysCount)); + [crypto requestAllPrivateKeys]; + } +} + - (void)authenticationDidComplete { [self handleAppState]; @@ -3621,6 +3678,173 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } + +#pragma mark - Incoming room key requests handling + +- (void)enableRoomKeyRequestObserver:(MXSession*)mxSession +{ + roomKeyRequestObserver = + [[NSNotificationCenter defaultCenter] addObserverForName:kMXCryptoRoomKeyRequestNotification + object:mxSession.crypto + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *notif) + { + [self checkPendingRoomKeyRequestsInSession:mxSession]; + }]; + + roomKeyRequestCancellationObserver = + [[NSNotificationCenter defaultCenter] addObserverForName:kMXCryptoRoomKeyRequestCancellationNotification + object:mxSession.crypto + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *notif) + { + [self checkPendingRoomKeyRequestsInSession:mxSession]; + }]; +} + +- (void)disableRoomKeyRequestObserver:(MXSession*)mxSession +{ + if (roomKeyRequestObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:roomKeyRequestObserver]; + roomKeyRequestObserver = nil; + } + + if (roomKeyRequestCancellationObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:roomKeyRequestCancellationObserver]; + roomKeyRequestCancellationObserver = nil; + } +} + +// Check if a key share dialog must be displayed for the given session +- (void)checkPendingRoomKeyRequestsInSession:(MXSession*)mxSession +{ + if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) + { + MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession called while the app is not active. Ignore it."); + return; + } + + if (![mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Only legacy crypto allows manually accepting/rejecting key requests"); + return; + } + MXLegacyCrypto *crypto = (MXLegacyCrypto *)mxSession.crypto; + + MXWeakify(self); + [crypto pendingKeyRequests:^(MXUsersDevicesMap *> *pendingKeyRequests) { + + MXStrongifyAndReturnIfNil(self); + MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: cross-signing state: %ld, pendingKeyRequests.count: %@. Already displayed: %@", + crypto.crossSigning.state, + @(pendingKeyRequests.count), + self->roomKeyRequestViewController ? @"YES" : @"NO"); + + if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) + { + if (self->roomKeyRequestViewController) + { + // Check if the current RoomKeyRequestViewController is still valid + MXSession *currentMXSession = self->roomKeyRequestViewController.mxSession; + NSString *currentUser = self->roomKeyRequestViewController.device.userId; + NSString *currentDevice = self->roomKeyRequestViewController.device.deviceId; + + NSArray *currentPendingRequest = [pendingKeyRequests objectForDevice:currentDevice forUser:currentUser]; + + if (currentMXSession == mxSession && currentPendingRequest.count == 0) + { + MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Cancel current dialog"); + + // The key request has been probably cancelled, remove the popup + [self->roomKeyRequestViewController hide]; + self->roomKeyRequestViewController = nil; + } + } + } + + if (!self->roomKeyRequestViewController && pendingKeyRequests.count) + { + // Pick the first coming user/device pair + NSString *userId = pendingKeyRequests.userIds.firstObject; + NSString *deviceId = [pendingKeyRequests deviceIdsForUser:userId].firstObject; + + // Give the client a chance to refresh the device list + MXWeakify(self); + [crypto downloadKeys:@[userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { + + MXStrongifyAndReturnIfNil(self); + MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:deviceId forUser:userId]; + if (deviceInfo) + { + if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped) + { + BOOL wasNewDevice = (deviceInfo.trustLevel.localVerificationStatus == MXDeviceUnknown); + + void (^openDialog)(void) = ^void() + { + MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Open dialog for %@", deviceInfo); + + self->roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession crypto:crypto onComplete:^{ + + self->roomKeyRequestViewController = nil; + + // Check next pending key request, if any + [self checkPendingRoomKeyRequests]; + }]; + + [self->roomKeyRequestViewController show]; + }; + + // If the device was new before, it's not any more. + if (wasNewDevice) + { + [crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil]; + } + else + { + openDialog(); + } + } + else if (deviceInfo.trustLevel.isVerified) + { + [crypto acceptAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ + [self checkPendingRoomKeyRequests]; + }]; + } + else + { + [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ + [self checkPendingRoomKeyRequests]; + }]; + } + } + else + { + MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: No details found for device %@:%@", userId, deviceId); + [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{ + [self checkPendingRoomKeyRequests]; + }]; + } + } failure:^(NSError *error) { + // Retry later + MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Failed to download device keys. Retry"); + [self checkPendingRoomKeyRequests]; + }]; + } + }]; +} + +// Check all opened MXSessions for key share dialog +- (void)checkPendingRoomKeyRequests +{ + for (MXSession *mxSession in mxSessionArray) + { + [self checkPendingRoomKeyRequestsInSession:mxSession]; + } +} + #pragma mark - Incoming key verification handling - (void)enableIncomingKeyVerificationObserver:(MXSession*)mxSession @@ -3778,6 +4002,12 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId { + id crypto = coordinatorBridgePresenter.session.crypto; + if ([crypto isKindOfClass:[MXLegacyCrypto class]] && (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled)) + { + MXLogDebug(@"[AppDelegate][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys"); + [(MXLegacyCrypto *)crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil]; + } [self dismissKeyVerificationCoordinatorBridgePresenter]; } diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index cb10aacfd..dbe9ea62e 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -781,6 +781,12 @@ extension AuthenticationCoordinator: AuthenticationServiceDelegate { // MARK: - KeyVerificationCoordinatorDelegate extension AuthenticationCoordinator: KeyVerificationCoordinatorDelegate { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { + if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup, + !backup.hasPrivateKeyInCryptoStore || !backup.enabled { + MXLog.debug("[AuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys") + crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + } + navigationRouter.dismissModule(animated: true) { [weak self] in self?.authenticationDidComplete() } diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift index e0c9a51b4..763d971e2 100644 --- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift @@ -235,6 +235,12 @@ extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate // MARK: - KeyVerificationCoordinatorDelegate extension LegacyAuthenticationCoordinator: KeyVerificationCoordinatorDelegate { func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { + if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup, + !backup.hasPrivateKeyInCryptoStore || !backup.enabled { + MXLog.debug("[LegacyAuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys") + crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + } + navigationRouter.dismissModule(animated: true) { [weak self] in self?.authenticationDidComplete() } diff --git a/Riot/Modules/Authentication/SessionVerificationListener.swift b/Riot/Modules/Authentication/SessionVerificationListener.swift index ffefd839a..214c76695 100644 --- a/Riot/Modules/Authentication/SessionVerificationListener.swift +++ b/Riot/Modules/Authentication/SessionVerificationListener.swift @@ -68,7 +68,14 @@ class SessionVerificationListener { return } - if session.state == .running { + if session.state == .storeDataReady { + if let crypto = session.crypto as? MXLegacyCrypto { + // Do not make key share requests while the "Complete security" is not complete. + // If the device is self-verified, the SDK will restore the existing key backup. + // Then, it will re-enable outgoing key share requests + crypto.setOutgoingKeyRequestsEnabled(false, onComplete: nil) + } + } else if session.state == .running { unregisterSessionStateChangeNotification() if let crypto = session.crypto { @@ -94,6 +101,7 @@ class SessionVerificationListener { self.completion?(.authenticationIsComplete) } failure: { error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed", context: error) + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } else { @@ -103,10 +111,12 @@ class SessionVerificationListener { self.completion?(.authenticationIsComplete) } failure: { error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Do not know how to bootstrap cross-signing. Skip it.") + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } } else { + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } case .crossSigningExists: @@ -114,10 +124,13 @@ class SessionVerificationListener { self.completion?(.needsVerification) default: MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Nothing to do") + + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self.completion?(.authenticationIsComplete) } } failure: { [weak self] error in MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state", context: error) + (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil) self?.completion?(.authenticationIsComplete) } } else { diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m index 680d330fa..3e8227e7c 100644 --- a/Riot/Modules/Call/CallViewController.m +++ b/Riot/Modules/Call/CallViewController.m @@ -370,16 +370,28 @@ CallAudioRouteMenuViewDelegate> { typeof(self) self = weakSelf; self->currentAlert = nil; + + // Acknowledge the existence of all devices + [self startActivityIndicator]; + if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + MXLogFailure(@"[CallViewController] call: Only legacy crypto supports manual setting of known devices"); + return; + } + [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{ - // Retry the call - if (call.isIncoming) - { - [call answer]; - } - else - { - [call callWithVideo:call.isVideoCall]; - } + [self stopActivityIndicator]; + + // Retry the call + if (call.isIncoming) + { + [call answer]; + } + else + { + [call callWithVideo:call.isVideoCall]; + } + }]; } }]]; diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index 80ec752a5..eac5fd48c 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -1115,7 +1115,8 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { let title: String let message: String - if MXSDKOptions.sharedInstance().cryptoMigrationDelegate?.needsVerificationUpgrade == true { + if let feature = MXSDKOptions.sharedInstance().cryptoSDKFeature, + feature.isEnabled && feature.needsVerificationUpgrade { title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage } else { diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift index c4cdee422..8398c659d 100644 --- a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift +++ b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift @@ -69,6 +69,9 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable { extension LaunchLoadingView: MXSessionStartupProgressDelegate { func sessionDidUpdateStartupProgress(state: MXSessionStartupProgress.State) { + guard MXSDKOptions.sharedInstance().enableStartupProgress else { + return + } update(with: state) } diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 1cd659943..70cc2f71a 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -953,7 +953,15 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes; [MXKRoomDataSourceManager removeSharedManagerForMatrixSession:mxSession]; if (clearStore) - { + { + // Force a reload of device keys at the next session start, unless we are just about to migrate + // all data and device keys into CryptoSDK. + // This will fix potential UISIs other peoples receive for our messages. + if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]] && !MXSDKOptions.sharedInstance.enableCryptoSDK) + { + [(MXLegacyCrypto *)mxSession.crypto resetDeviceKeys]; + } + // Clean other stores [mxSession.scanManager deleteAllAntivirusScans]; [mxSession.aggregations resetData]; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 98638dc2f..88741b98d 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6459,10 +6459,21 @@ static CGSize kThreadListBarButtonItemImageSize; self->currentAlert = nil; // Acknowledge the existence of all devices - self->unknownDevices = nil; + [self startActivityIndicator]; - // And resend pending messages - [self resendAllUnsentMessages]; + if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + MXLogFailure(@"[RoomVC] eventDidChangeSentState: Only legacy crypto supports manual setting of known devices"); + return; + } + [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:self->unknownDevices complete:^{ + + self->unknownDevices = nil; + [self stopActivityIndicator]; + + // And resend pending messages + [self resendAllUnsentMessages]; + }]; } }]]; diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h new file mode 100644 index 000000000..e9db3a583 --- /dev/null +++ b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h @@ -0,0 +1,62 @@ +/* + Copyright 2017 Vector Creations 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 + +#import + +/** + The `RoomKeyRequestViewController` display a modal dialog at the top of the + application asking the user if he wants to share room keys with a user's device. + For the moment, the user is himself. + */ +@interface RoomKeyRequestViewController : NSObject + +/** + The UIAlertController instance which handles the dialog. + */ +@property (nonatomic, readonly) UIAlertController *alertController; + +@property (nonatomic, readonly) MXSession *mxSession; +@property (nonatomic, readonly) MXDeviceInfo *device; + +/** + Initialise an `RoomKeyRequestViewController` instance. + + @param deviceInfo the device to share keys to. + @param wasNewDevice flag indicating whether this is the first time we meet the device. + @param session the related matrix session. + @param crypto the related (legacy) crypto module + @param onComplete a block called when the the dialog is closed. + @return the newly created instance. + */ +- (instancetype)initWithDeviceInfo:(MXDeviceInfo*)deviceInfo + wasNewDevice:(BOOL)wasNewDevice + andMatrixSession:(MXSession*)session + crypto:(MXLegacyCrypto *)crypto + onComplete:(void (^)(void))onComplete; + +/** + Show the dialog in a modal way. + */ +- (void)show; + +/** + Hide the dialog. + */ +- (void)hide; + +@end diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m new file mode 100644 index 000000000..6f638bd78 --- /dev/null +++ b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m @@ -0,0 +1,195 @@ +/* + Copyright 2017 Vector Creations 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 "RoomKeyRequestViewController.h" + +#import "GeneratedInterface-Swift.h" + +@interface RoomKeyRequestViewController () +{ + void (^onComplete)(void); + + KeyVerificationCoordinatorBridgePresenter *keyVerificationCoordinatorBridgePresenter; + + BOOL wasNewDevice; +} + +@property (nonatomic, strong) MXLegacyCrypto *crypto; + +@end + +@implementation RoomKeyRequestViewController + +- (instancetype)initWithDeviceInfo:(MXDeviceInfo *)deviceInfo + wasNewDevice:(BOOL)theWasNewDevice + andMatrixSession:(MXSession *)session + crypto:(MXLegacyCrypto *)crypto + onComplete:(void (^)(void))onCompleteBlock +{ + self = [super init]; + if (self) + { + _mxSession = session; + _crypto = crypto; + _device = deviceInfo; + wasNewDevice = theWasNewDevice; + onComplete = onCompleteBlock; + } + return self; +} + +- (void)show +{ + // Show it modally on the root view controller + UIViewController *rootViewController = [AppDelegate theDelegate].window.rootViewController; + if (rootViewController) + { + NSString *title = [VectorL10n e2eRoomKeyRequestTitle]; + NSString *message; + if (wasNewDevice) + { + message = [VectorL10n e2eRoomKeyRequestMessageNewDevice:_device.displayName]; + } + else + { + message = [VectorL10n e2eRoomKeyRequestMessage:_device.displayName]; + } + + _alertController = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + __weak typeof(self) weakSelf = self; + + [_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestStartVerification] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + + self->_alertController = nil; + [self showVerificationView]; + } + }]]; + + [_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestShareWithoutVerifying] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + + self->_alertController = nil; + + // Accept the received requests from this device + [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ + + self->onComplete(); + }]; + } + }]]; + + [_alertController addAction:[UIAlertAction actionWithTitle:[VectorL10n e2eRoomKeyRequestIgnoreRequest] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + + self->_alertController = nil; + + // Ignore all pending requests from this device + [self.crypto ignoreAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ + + self->onComplete(); + }]; + } + }]]; + + [rootViewController presentViewController:_alertController animated:YES completion:nil]; + } +} + +- (void)hide +{ + if (_alertController) + { + [_alertController dismissViewControllerAnimated:YES completion:nil]; + _alertController = nil; + } +} + + +- (void)showVerificationView +{ + // Show it modally on the root view controller + UIViewController *rootViewController = [AppDelegate theDelegate].window.rootViewController; + if (rootViewController) + { + keyVerificationCoordinatorBridgePresenter = [[KeyVerificationCoordinatorBridgePresenter alloc] initWithSession:_mxSession]; + keyVerificationCoordinatorBridgePresenter.delegate = self; + + [keyVerificationCoordinatorBridgePresenter presentFrom:rootViewController otherUserId:_device.userId otherDeviceId:_device.deviceId animated:YES]; + } +} + +#pragma mark - DeviceVerificationCoordinatorBridgePresenterDelegate + +- (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId +{ + [self dismissKeyVerificationCoordinatorBridgePresenter]; +} + +- (void)keyVerificationCoordinatorBridgePresenterDelegateDidCancel:(KeyVerificationCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [self dismissKeyVerificationCoordinatorBridgePresenter]; +} + +- (void)dismissKeyVerificationCoordinatorBridgePresenter +{ + [keyVerificationCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + keyVerificationCoordinatorBridgePresenter = nil; + + // Check device new status + [self.crypto downloadKeys:@[self.device.userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) { + + MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:self.device.deviceId forUser:self.device.userId]; + if (deviceInfo && deviceInfo.trustLevel.localVerificationStatus == MXDeviceVerified) + { + // Accept the received requests from this device + // As the device is now verified, all other key requests will be automatically accepted. + [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{ + + self->onComplete(); + }]; + } + else + { + // Come back to self.alertController - ie, reopen it + [self show]; + } + } failure:^(NSError *error) { + + // Should not happen (the device is in the crypto db) + [self show]; + }]; +} + +@end diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 751ab69c0..7780f60a1 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -232,7 +232,8 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) LABS_ENABLE_NEW_SESSION_MANAGER, LABS_ENABLE_NEW_CLIENT_INFO_FEATURE, LABS_ENABLE_WYSIWYG_COMPOSER, - LABS_ENABLE_VOICE_BROADCAST + LABS_ENABLE_VOICE_BROADCAST, + LABS_ENABLE_CRYPTO_SDK }; typedef NS_ENUM(NSUInteger, SECURITY) @@ -764,6 +765,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [sectionLabs addRowWithTag:LABS_ENABLE_NEW_SESSION_MANAGER]; } /* bwi: disabled for our apps + if ([CryptoSDKFeature.shared canManuallyEnableForUserId:self.mainSession.myUserId]) + { + [sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK]; + } + [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS]; @@ -2970,6 +2976,18 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableVoiceBroadcastFeature:) forControlEvents:UIControlEventTouchUpInside]; + cell = labelAndSwitchCell; + } + else if (row == LABS_ENABLE_CRYPTO_SDK) + { + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK; + labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk; + labelAndSwitchCell.mxkSwitch.on = isEnabled; + [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled]; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; + cell = labelAndSwitchCell; } } @@ -3861,6 +3879,30 @@ ChangePasswordCoordinatorBridgePresenterDelegate> RiotSettings.shared.enableVoiceBroadcast = sender.isOn; } +- (void)enableCryptoSDKFeature:(UISwitch *)sender +{ + [currentAlert dismissViewControllerAnimated:NO completion:nil]; + UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:VectorL10n.settingsLabsEnableCryptoSdk + message:VectorL10n.settingsLabsConfirmCryptoSdk + preferredStyle:UIAlertControllerStyleAlert]; + + MXWeakify(self); + [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + self->currentAlert = nil; + + [sender setOn:NO animated:YES]; + }]]; + + [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + [CryptoSDKFeature.shared enable]; + [[AppDelegate theDelegate] reloadMatrixSessions:YES]; + }]]; + + [self presentViewController:confirmationAlert animated:YES completion:nil]; + currentAlert = confirmationAlert; +} + - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; diff --git a/Riot/Modules/UserDevices/UsersDevicesViewController.m b/Riot/Modules/UserDevices/UsersDevicesViewController.m index fcd7bd567..3b5b8c9a8 100644 --- a/Riot/Modules/UserDevices/UsersDevicesViewController.m +++ b/Riot/Modules/UserDevices/UsersDevicesViewController.m @@ -273,12 +273,22 @@ - (IBAction)onDone:(id)sender { // Acknowledge the existence of all devices before leaving this screen - [self dismissViewControllerAnimated:YES completion:nil]; - - if (self->onCompleteBlock) + [self startActivityIndicator]; + if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]]) { - self->onCompleteBlock(YES); + MXLogFailure(@"[UsersDevicesViewController] onDone: Only legacy crypto supports manual setting of known devices"); + return; } + [(MXLegacyCrypto *)mxSession.crypto setDevicesKnown:usersDevices complete:^{ + + [self stopActivityIndicator]; + [self dismissViewControllerAnimated:YES completion:nil]; + + if (self->onCompleteBlock) + { + self->onCompleteBlock(YES); + } + }]; } - (IBAction)onCancel:(id)sender diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 2cf70a387..8a68f5b4d 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -42,6 +42,7 @@ class NotificationService: UNNotificationServiceExtension { private var ongoingVoIPPushRequests: [String: Bool] = [:] private var userAccount: MXKAccount? + private var isCryptoSDKEnabled = false /// Best attempt contents. Will be updated incrementally, if something fails during the process, this best attempt content will be showed as notification. Keys are eventId's private var bestAttemptContents: [String: UNMutableNotificationContent] = [:] @@ -268,12 +269,13 @@ class NotificationService: UNNotificationServiceExtension { self.userAccount = MXKAccountManager.shared()?.activeAccounts.first if let userAccount = userAccount { Self.backgroundServiceInitQueue.sync { - if NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { + if hasChangedCryptoSDK() || NotificationService.backgroundSyncService?.credentials != userAccount.mxCredentials { MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") self.logMemory() NotificationService.backgroundSyncService = MXBackgroundSyncService( withCredentials: userAccount.mxCredentials, + isCryptoSDKEnabled: isCryptoSDKEnabled, persistTokenDataHandler: { persistTokenDataHandler in MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in @@ -290,6 +292,16 @@ class NotificationService: UNNotificationServiceExtension { } } + /// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require + /// rebuilding `MXBackgroundSyncService` + private func hasChangedCryptoSDK() -> Bool { + guard isCryptoSDKEnabled != MXSDKOptions.sharedInstance().enableCryptoSDK else { + return false + } + isCryptoSDKEnabled = MXSDKOptions.sharedInstance().enableCryptoSDK + return true + } + /// Attempts to preprocess payload and attach room display name to the best attempt content /// - Parameters: /// - eventId: Event identifier to mutate best attempt content diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m index 0e1c74cf7..22d0063be 100644 --- a/RiotShareExtension/Shared/ShareManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -102,6 +102,11 @@ static MXSession *fakeSession; [session setStore:self.fileStore success:^{ MXStrongifyAndReturnIfNil(session); + if ([session.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + ((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now + } + self.selectedRooms = [NSMutableArray array]; for (NSString *roomIdentifier in roomIdentifiers) { MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift index 9cf127bb7..9c9100087 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift @@ -267,6 +267,17 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { let session = sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false) +// MXLog.debug("[QRLoginService] Session created without E2EE support. Inform the interlocutor of finishing") +// guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginFinish, outcome: .success)), +// case .success = await rendezvousService.send(data: requestData) else { +// await teardownRendezvous(state: .failed(error: .rendezvousFailed)) +// return +// } +// +// MXLog.debug("[QRLoginService] Login flow finished, returning session") +// state = .completed(session: session, securityCompleted: false) +// return + let cryptoResult = await withCheckedContinuation { continuation in session.enableCrypto(true) { response in continuation.resume(returning: response) diff --git a/RiotTests/Experiments/CryptoSDKFeatureTests.swift b/RiotTests/Experiments/CryptoSDKFeatureTests.swift new file mode 100644 index 000000000..a512b71c6 --- /dev/null +++ b/RiotTests/Experiments/CryptoSDKFeatureTests.swift @@ -0,0 +1,79 @@ +// +// Copyright 2023 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 XCTest +@testable import Element + +class CryptoSDKFeatureTests: XCTestCase { + class RemoteFeatureClient: RemoteFeaturesClientProtocol { + var isEnabled = false + func isFeatureEnabled(_ feature: String) -> Bool { + isEnabled + } + } + + var remote: RemoteFeatureClient! + var feature: CryptoSDKFeature! + + override func setUp() { + RiotSettings.shared.enableCryptoSDK = false + remote = RemoteFeatureClient() + feature = CryptoSDKFeature(remoteFeature: remote, localTargetPercentage: 0) + } + + override func tearDown() { + RiotSettings.shared.enableCryptoSDK = false + } + + func test_disabledByDefault() { + XCTAssertFalse(feature.isEnabled) + } + + func test_enable() { + feature.enable() + XCTAssertTrue(feature.isEnabled) + } + + func test_enableIfAvailable_remainsEnabledWhenRemoteClientDisabled() { + feature.enable() + remote.isEnabled = false + + feature.enableIfAvailable(forUserId: "alice") + + XCTAssertTrue(feature.isEnabled) + } + + func test_enableIfAvailable_notEnabledIfRemoteFeatureDisabled() { + remote.isEnabled = false + feature.enableIfAvailable(forUserId: "alice") + XCTAssertFalse(feature.isEnabled) + } + + func test_canManuallyEnable() { + remote.isEnabled = false + XCTAssertTrue(feature.canManuallyEnable(forUserId: "alice")) + + remote.isEnabled = true + XCTAssertFalse(feature.canManuallyEnable(forUserId: "alice")) + } + + func test_reset() { + feature.enable() + feature.reset() + XCTAssertFalse(RiotSettings.shared.enableCryptoSDK) + } +} diff --git a/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m index 5bc037790..34ebb66e9 100644 --- a/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m +++ b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m @@ -117,6 +117,12 @@ self.selectedRoom = [MXRoom loadRoomFromStore:fileStore withRoomId:roomID matrixSession:session]; + // Do not warn for unknown devices. We have cross-signing now + if ([session.crypto isKindOfClass:[MXLegacyCrypto class]]) + { + ((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO; + } + MXWeakify(self); [self.selectedRoom sendTextMessage:intent.content threadId:nil From f00b1029aa5a4af219af2de8101a70df69d90b3c Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Wed, 28 Jun 2023 13:30:26 +0200 Subject: [PATCH 141/149] MESSENGER-4819 --- .../EnterPinCode/EnterPinCodeViewController.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift index 6c6c05569..fe3ef3c6c 100644 --- a/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift +++ b/Riot/Modules/SetPinCode/EnterPinCode/EnterPinCodeViewController.swift @@ -84,9 +84,6 @@ final class EnterPinCodeViewController: UIViewController { UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") } - if BWIBuildSettings.shared.showBUMLottieAnimation { - inactiveLogoImageView.isHidden = true - } // BWI: accessibility description /* bwi: 4782 digitButtonReset.vc_setupAccessibilityTraitsButton(withTitle: BWIL10n.pinProtectionResetButtonAccessibilityLabel, hint: BWIL10n.pinProtectionResetButtonAccessibilityHint, isEnabled: true) @@ -192,8 +189,9 @@ final class EnterPinCodeViewController: UIViewController { } // show BuM Logo instead of Element logo - if BWIBuildSettings.shared.bwiEnableBuMUI { + if BWIBuildSettings.shared.bwiEnableBuMUI || BWIBuildSettings.shared.bwiLoginFlowLayout { logoImageView.image = Asset.Images.launchScreenLogo.image + inactiveLogoImageView.image = Asset.Images.launchScreenLogo.image } } From 9cada37bb321afe1e46f77d976d1f875cab1084d Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Thu, 29 Jun 2023 12:18:10 +0200 Subject: [PATCH 142/149] MESSENGER-4772 change font size --- bwi/MarkDown/MarkDownView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bwi/MarkDown/MarkDownView.swift b/bwi/MarkDown/MarkDownView.swift index 7256b24a9..bb3cf0820 100644 --- a/bwi/MarkDown/MarkDownView.swift +++ b/bwi/MarkDown/MarkDownView.swift @@ -49,7 +49,7 @@ struct UIMarkDownWrapper: UIViewRepresentable { func updateUIView(_ uiView: UILabel, context: Context) { let down = Down(markdownString: markDownString) - guard let attributedString = try? down.toAttributedString() else { return } + guard let attributedString = try? down.toAttributedString(stylesheet: "* {font-family: sans-serif; font-size: 16pt; } ") else { return } let mutableString = NSMutableAttributedString(attributedString: attributedString) mutableString.addAttributes([.foregroundColor: ThemeService.shared().theme.textPrimaryColor], range: NSRange(location: 0, length: attributedString.length)) From 792420f657ba15938ab4433e12cbd94144aebbfd Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Fri, 30 Jun 2023 16:28:35 +0200 Subject: [PATCH 143/149] MESSENGER-4882 refactor delete avatar --- Riot/Modules/Room/Settings/RoomSettingsViewController.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index 0bdc6c2cc..e096ee2e9 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -1423,7 +1423,8 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti if (shouldRemoveRoomAvatarImage) { shouldRemoveRoomAvatarImage = false; [updatedItemsDict removeObjectForKey:kRoomSettingsAvatarKey]; - pendingOperation = [mxRoom setAvatar:@"" success:^{ + // delete room avatar + pendingOperation = [mxRoom sendStateEventOfType:kMXEventTypeStringRoomAvatar content:@{} stateKey:@"" success:^(NSString *eventId) { if (weakSelf) { typeof(self) self = weakSelf; From 4b436752af53e01cead2ed1c2c8a3daae95cdee6 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Mon, 3 Jul 2023 15:27:36 +0200 Subject: [PATCH 144/149] MESSENGER-4929 show participant votes button when user has voted --- .../Modules/Room/TimelinePoll/TimelinePollModels.swift | 4 ++++ .../Modules/Room/TimelinePoll/View/TimelinePollView.swift | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 3c4d06e96..ca31dbae7 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -100,6 +100,10 @@ struct TimelinePollDetails { var representsPollEndedEvent: Bool { eventType == .ended } + + var shouldShowShowParticipantsButton: Bool { + return (showParticipants && hasCurrentUserVoted && (type == TimelinePollType.disclosed || closed)) + } } extension TimelinePollDetails: Identifiable { } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index dd69cc961..675e59ba5 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -74,8 +74,7 @@ struct TimelinePollView: View { .font(theme.fonts.footnote) .foregroundColor(theme.colors.tertiaryContent) - if poll.showParticipants && - (poll.type == TimelinePollType.disclosed || poll.closed) { + if poll.shouldShowShowParticipantsButton { Button(action: { viewModel.send(viewAction:.showParticipants) }) From cc3f55e0e2379e78db904d49ff87da2fb2f9ae92 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Tue, 4 Jul 2023 15:16:20 +0200 Subject: [PATCH 145/149] MESSENGER-4882 changelog and fixes --- CHANGES_BWI.md | 39 +++++++++++++++++++- Config/BWIBuildSettings.swift | 2 +- Podfile | 2 +- Riot/Assets/de.lproj/Bwi.strings | 2 +- Riot/Assets/en.lproj/Bwi.strings | 2 +- Riot/Assets/new_features.html | 29 +++++++++++++++ Riot/Generated/BWIStrings.swift | 2 +- Riot/Modules/Application/LegacyAppDelegate.m | 6 --- 8 files changed, 72 insertions(+), 12 deletions(-) diff --git a/CHANGES_BWI.md b/CHANGES_BWI.md index 964217202..c107588ba 100644 --- a/CHANGES_BWI.md +++ b/CHANGES_BWI.md @@ -1,4 +1,41 @@ -Changes in BWI project 2.5.0 (2023-05-09) +Changes in BWI project 2.7.0 (2023-07-04) +=================================================== + +Upstream merge ✨: + +- v1.10.12 + +Features ✨: +- New show participants toggle for polls (#4393) + +Improvements 🙌: +- Roomavatar can now be deleted (#4743) +- Remove "black" theme (#4744) +- Open links in system browser (#1678) +- Add imprint (#4682) +- Text changes for DM creation (#4736) +- Add accessibility statement (#4772) +- Matomo tracking of poll creation (#4795) +- Matomo tracking of voice messages (#4795) +- Matomo tracking of forwarding messages (#4795) + +Bugfix 🐛: +- Disable logout when there is no internet connection (#3539) +- Disable permalink sharing for private rooms (#4390) +- Fix manual verification (#4710) +- Fix QR code scanning (#4748) +- Show app logo in pin code screen (#4828) +- Update "all chats" filter on logout/login (#4573) + +Translations 🗣 : +- English translations passphrase (#4706) + +SDK API changes ⚠️: + +Build 🧱: + + +Changes in BWI project 2.6.0 (2023-05-09) =================================================== Upstream merge ✨: diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index 17450cf24..5702aae6d 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -501,7 +501,7 @@ class BWIBuildSettings: NSObject { var passwordIndicatorOnLogin = true // MARK: Displays the element base version on the settings screen - var elementBaseVersion = "1.10.11" + var elementBaseVersion = "1.10.12" var showElementBaseVersion = true diff --git a/Podfile b/Podfile index 2405dd072..94d6653a4 100644 --- a/Podfile +++ b/Podfile @@ -43,7 +43,7 @@ when String # specific MatrixSDK released version $matrixSDKVersionSpec = $matrixSDKVersion end -$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.10_bwi_4889' } +$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.10_bwi' } # Method to import the MatrixSDK def import_MatrixSDK diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index f227619be..7feabc31e 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -521,7 +521,7 @@ "bwi_settings_new_features_show_features" = "Neue Funktionen anzeigen"; "bwi_feature_banner_header" = "Neue Funktionen"; "bwi_feature_banner_show_more_button" = "Erfahre mehr"; -"bwi_feature_banner_advertisement_text" = "Du kannst jetzt aktive und vergangene Umfragen gesammelt in den Raumdetails einsehen (erreichbar unter Raumdetails, im Bereich \"Umfrageverlauf\")."; +"bwi_feature_banner_advertisement_text" = "Neue Umfragen können vom Ersteller so konfiguriert werden, dass angezeigt wird, wer für welche Option gestimmt hat."; // MARK: - Onboarding "onboarding_splash_login_button_title" = "Loslegen"; diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index 3d567b52b..f1fef0bd9 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -430,7 +430,7 @@ "bwi_settings_new_features_show_features" = "Show new features"; "bwi_feature_banner_header" = "New Features"; "bwi_feature_banner_show_more_button" = "Learn more"; -"bwi_feature_banner_advertisement_text" = "You can now see a poll history in the room details."; +"bwi_feature_banner_advertisement_text" = "New polls can be configured by the creator to show who voted for which option."; // MARK: - Onboarding "onboarding_splash_login_button_title" = "Let's go"; diff --git a/Riot/Assets/new_features.html b/Riot/Assets/new_features.html index d11191bfa..ef84e86ac 100644 --- a/Riot/Assets/new_features.html +++ b/Riot/Assets/new_features.html @@ -26,6 +26,35 @@ +
+

+ Version 2.7.0 +

+ +

+ Neue Funktionen +

    +
  • Neue Umfragen können vom Ersteller so konfiguriert werden, dass angezeigt wird, wer für welche Option gestimmt hat. +
+

+ +

+ Verbesserungen +

    +
  • Das Löschen des Raumavatars ist nun möglich. +
  • Website Verlinkungen werden nun im System-Browser geöffnet. +
+

+ +

+ Behobene Bugs +

    +
  • Das Abmelden ohne Internetverbindung kann zu Fehlern führen, deshalb ist dies nicht mehr möglich. +
  • Die Eingabe des Wiederherstellungsschlüssels war nicht möglich, wenn die Sprache der App auf Englisch eingestellt war. +
  • Man kann eine andere Sitzung nicht mehr von einem Gerät alleine verifizieren. +
+

+

Version 2.6.0 diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index 0c7105e8c..2ecb68314 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -223,7 +223,7 @@ public class BWIL10n: NSObject { public static var bwiErrorRoomNotAvailableTitle: String { return BWIL10n.tr("Bwi", "bwi_error_room_not_available_title") } - /// Du kannst jetzt aktive und vergangene Umfragen gesammelt in den Raumdetails einsehen (erreichbar unter Raumdetails, im Bereich "Umfrageverlauf"). + /// Neue Umfragen können vom Ersteller so konfiguriert werden, dass angezeigt wird, wer für welche Option gestimmt hat. public static var bwiFeatureBannerAdvertisementText: String { return BWIL10n.tr("Bwi", "bwi_feature_banner_advertisement_text") } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 8824f36a1..076b04fc6 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1678,13 +1678,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self peekInRoomWithNavigationParameters:roomPreviewNavigationParameters pathParams:pathParams]; } } failure:^(NSError *error) { - // bwi #4742: check if the room is still available - NSHTTPURLResponse *response = error.userInfo[@"httpResponse"]; - if (response.statusCode == 404) { - [self showAlertWithTitle:BWIL10n.bwiErrorRoomNotAvailableTitle message:BWIL10n.bwiErrorRoomNotAvailableMessage]; - } else { [self peekInRoomWithNavigationParameters:roomPreviewNavigationParameters pathParams:pathParams]; - } }]; } From 9a931d77341f8c38acca2787e9db10b8cab5e4f3 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Wed, 5 Jul 2023 16:56:35 +0200 Subject: [PATCH 146/149] change color on allchatsvc --- Riot/Modules/Home/AllChats/AllChatsViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index eac5fd48c..214713901 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -654,7 +654,8 @@ class AllChatsViewController: HomeViewController { // bwi: 4179 var allChatsEditButton = UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu) - + allChatsEditButton.tintColor = ThemeService.shared().theme.tintColor // bwi: #4883 + if BWIBuildSettings.shared.enableSpaces { self.toolbar.items = [ spacesButton, From 95452b58abc43352b9f9a5715b73b102ea65e127 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Mon, 10 Jul 2023 12:36:40 +0200 Subject: [PATCH 147/149] MESSENGER-4819 change icon --- .../launch_screen_logo.imageset/Contents.json | 16 +++------------- .../bundesmessenger-logo.svg | 14 ++++++++++++++ .../launch_screen_logo.imageset/launch_bwi.png | Bin 12171 -> 0 bytes .../launch_bwi@2x.png | Bin 27651 -> 0 bytes .../launch_bwi@3x.png | Bin 44477 -> 0 bytes 5 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/launch_screen_logo.imageset/bundesmessenger-logo.svg delete mode 100644 Riot/Assets/Images.xcassets/launch_screen_logo.imageset/launch_bwi.png delete mode 100644 Riot/Assets/Images.xcassets/launch_screen_logo.imageset/launch_bwi@2x.png delete mode 100644 Riot/Assets/Images.xcassets/launch_screen_logo.imageset/launch_bwi@3x.png diff --git a/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/Contents.json b/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/Contents.json index 80cb64990..f8ae0d35e 100644 --- a/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/Contents.json @@ -1,19 +1,8 @@ { "images" : [ { - "filename" : "launch_bwi.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "launch_bwi@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "launch_bwi@3x.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "bundesmessenger-logo.svg", + "idiom" : "universal" } ], "info" : { @@ -21,6 +10,7 @@ "version" : 1 }, "properties" : { + "preserves-vector-representation" : true, "template-rendering-intent" : "original" } } diff --git a/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/bundesmessenger-logo.svg b/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/bundesmessenger-logo.svg new file mode 100644 index 000000000..2bd1791f6 --- /dev/null +++ b/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/bundesmessenger-logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/launch_bwi.png b/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/launch_bwi.png deleted file mode 100644 index 1e06d2ded66efe77cc95d129d22ff16e4d103195..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12171 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 JtQI#3{$Kdlh6exu diff --git a/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/launch_bwi@2x.png b/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/launch_bwi@2x.png deleted file mode 100644 index 6e0c03e29d84583af467b6d8ac5afb5fdf08dcbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27651 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 diff --git a/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/launch_bwi@3x.png b/Riot/Assets/Images.xcassets/launch_screen_logo.imageset/launch_bwi@3x.png deleted file mode 100644 index c48e8d72fdd7c00db09c5422e400ac049ce73780..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44477 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

kQ2Ix!6V)ZQUpqkY!Fql{F+rC1 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 From cd5cf18b6cb780ee05e19eb97a889218d9095d45 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Mon, 10 Jul 2023 17:08:37 +0200 Subject: [PATCH 148/149] MESSENGER-4883 add color changes and manual app store upload for beta --- Riot/Modules/Home/AllChats/AllChatsViewController.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index 214713901..1c728b76d 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -137,8 +137,13 @@ class AllChatsViewController: HomeViewController { emptyViewBottomAnchor = toolbar.topAnchor // bwi: 4179 - toolbar.tintColor = ThemeService.shared().theme.tintColor - toolbar.barTintColor = ThemeService.shared().theme.backgroundColor + + if BWIBuildSettings.shared.useNewBumColors { // bwi: #4883 + toolbar.tintColor = ThemeService.shared().theme.tintColor + toolbar.barTintColor = ThemeService.shared().theme.backgroundColor + } else { + toolbar.tintColor = theme.colors.accent + } updateUI() From 0a215f376e9e7ce23a3fe70ea70298a4cd28d4d1 Mon Sep 17 00:00:00 2001 From: JanNiklas Grabowski Date: Mon, 10 Jul 2023 18:12:31 +0200 Subject: [PATCH 149/149] MESSENGER-4883 fix update room avatar --- .../RoomInfoList/Views/RoomInfoBasicView.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift index 6f548f9ca..16af8c4d5 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/Views/RoomInfoBasicView.swift @@ -78,15 +78,6 @@ class RoomInfoBasicView: UIView { func configure(withViewData viewData: RoomInfoBasicViewData) { let avatarImage = AvatarGenerator.generateAvatar(forMatrixItem: viewData.roomId, withDisplayName: viewData.roomDisplayName) - if BWIBuildSettings.shared.bwiUseCustomPersonalNotesAvatar { - if let session = AppDelegate.theDelegate().mxSessions.first as? MXSession { - let service = PersonalNotesDefaultService(mxSession: session) - if let personalNotesRoomId = service.personalNotesRoomId(), personalNotesRoomId == viewData.roomId { - avatarImageView.image = UIImage(named: service.avatarImageUrl()) - } - } - } - // bwi: update room avatar if let avatarUrl = viewData.avatarUrl { if !avatarUrl.isEmpty { @@ -105,6 +96,15 @@ class RoomInfoBasicView: UIView { } else { avatarImageView.image = avatarImage } + + if BWIBuildSettings.shared.bwiUseCustomPersonalNotesAvatar { + if let session = AppDelegate.theDelegate().mxSessions.first as? MXSession { + let service = PersonalNotesDefaultService(mxSession: session) + if let personalNotesRoomId = service.personalNotesRoomId(), personalNotesRoomId == viewData.roomId { + avatarImageView.image = UIImage(named: service.avatarImageUrl()) + } + } + } badgeImageView.image = viewData.encryptionImage roomNameLabel.text = viewData.roomDisplayName