From c933526f22552b852cd59c957be3e7a384201074 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 8 Mar 2023 15:16:19 +0100 Subject: [PATCH 01/55] 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 | 88 +++++++++++++------ .../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, 265 insertions(+), 37 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 44f6bd53ca..751383169d 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 ba03ef61af..07806eddc4 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 0000000000..44a4d7cbf8 --- /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 a9df99fd46..334a39e73e 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 2e641916a7..17ab27fc48 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5149,6 +5149,11 @@ - (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbar [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 a177281f36..1da92a731a 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)) + guard let attributedText = inputToolbarView.attributedTextMessage else { return } + let newAttributedString = NSMutableAttributedString(attributedString: attributedText) + + 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 { - newAttributedString.appendString(roomMember.displayname.count > 0 ? roomMember.displayname : roomMember.userId) + 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(": ") } - newAttributedString.appendString(": ") - } - inputToolbar.attributedTextMessage = newAttributedString - inputToolbar.becomeFirstResponder() + 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 af84b462dc..ee1a032e02 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 db6cc81939..aefef24596 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 98d7febf6d..c964536675 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 1413912c2a..fb6ed88516 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 a78018f606..6448b9de33 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 c6d86a6558..59b25ef867 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 c5b68eeee0..4605547ebb 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 bf8fa00a5f..0f161ee389 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 81edb0df97..43006dbed9 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 acc69ccdc7..3df4c94ff9 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 3b09fcc0c8082aee26420ff18e45f4e9ca434663 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 8 Mar 2023 17:30:50 +0100 Subject: [PATCH 02/55] 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 d7bf9d8fca..bc9b8e0b26 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 9581df2a79..44199cc5bd 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m @@ -358,6 +358,10 @@ - (void)pasteText:(NSString *)text 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 334a39e73e..4882ad51cd 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 1da92a731a..b36447654a 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 aefef24596..1c5a4233ab 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 732583e4bd75bfb816cf9d38c91bbf99a1d2edfd Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 9 Mar 2023 12:05:02 +0100 Subject: [PATCH 03/55] 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 751383169d..daf871ac64 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 3df4c94ff9..64ab238d2e 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 185029945540294fe0498d1da7c078ea2d8eca7f Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 9 Mar 2023 12:11:58 +0100 Subject: [PATCH 04/55] 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 1c5a4233ab..a092a18ea0 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 a23987bce25b0e5814ba72c4e51d45bd727c557a Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 21 Mar 2023 10:26:37 +0100 Subject: [PATCH 05/55] 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 daf871ac64..a087c8ac32 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 b36447654a..a51beba3bf 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 ee1a032e02..ebbb8305a7 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 a092a18ea0..78989a23e6 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 64ab238d2e..ff745767a4 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 88aac572ccd0a1aa96a2ac5a16080f0f5730528d Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 21 Mar 2023 10:27:08 +0100 Subject: [PATCH 06/55] 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 f33d661bd4..b7a62a8bf2 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -39,6 +39,7 @@ + From 5fb426f77258d68fddbf07cc1303e6057178167b Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 22 Mar 2023 15:49:42 +0100 Subject: [PATCH 07/55] 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 17ab27fc48..e13b5c0383 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5154,6 +5154,11 @@ - (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern [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 ebbb8305a7..454134d288 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 78989a23e6..d50be4e3a0 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 35a628d020..b7d20d38ac 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 c964536675..6f7bab1652 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 fb6ed88516..93793fb729 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 59b25ef867..38776fbd3a 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 4605547ebb..a7615e43f2 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 1e1f490fc0..3999447b7e 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 1d89ca9b4e..40318c5df1 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 2b61b5bc200baa2c4048d90d667ec9d27e3083cb Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 11:47:15 +0100 Subject: [PATCH 08/55] 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 60363bc47a..ddff3084b1 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 4882ad51cd..675e824bad 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. - 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 + guard !matches.isEmpty else { return markdownString } - // 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)]) - } + let pillProvider = PillProvider(withSession: session, + eventFormatter: eventFormatter, + event: nil, + roomState: roomState, + andLatestRoomState: nil, + isEditMode: true) - // 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) + 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) } } @@ -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) - } - return permalink.starts(with: baseUrl) ? String(permalink.dropFirst(baseUrl.count)) : nil - } +@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 [] } - /// 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) + 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 + } + } } } diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index a51beba3bf..3d9ac22606 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 9c46f607aa4ce59a7d4b5b67a53c8a9bb32f6e4c Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 14:30:17 +0100 Subject: [PATCH 09/55] 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 3d9ac22606..00de9de959 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 5b2ce259319038c5d2182ed11a6885604bac35a9 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 14:50:49 +0100 Subject: [PATCH 10/55] 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 d50be4e3a0..343f020f80 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 7b5a46f29ed98f35db2a66fbbaea0136f3e64d11 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 14:51:56 +0100 Subject: [PATCH 11/55] 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 93793fb729..824e04d73b 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 859b0b4145..9c32b892fd 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 b38ba7306f4c219c600f00d80e5ee7d251a86c0f Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 15:02:19 +0100 Subject: [PATCH 12/55] 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 ddff3084b1..1941f8af12 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 052acddc3b1a5f54602c76f8d06db1a4977487f3 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 15:45:31 +0100 Subject: [PATCH 13/55] 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 e13b5c0383..6fa73faa58 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5154,11 +5154,16 @@ - (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern [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 454134d288..897922832d 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 343f020f80..e6b191e2b3 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 b7d20d38ac..8b5327b14d 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 824e04d73b..e4317a2759 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 38776fbd3a..de56c07364 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 a7615e43f2..9dbebdbf33 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 40318c5df1..33aa5bb795 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 f83599b83ffe323615a4e5e5588da5cb15391728 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 17:12:54 +0100 Subject: [PATCH 14/55] 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 a087c8ac32..75c0b64386 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 fdf92cab5d..0c3ba03e2b 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 9683ac6214..3674172823 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 ff745767a4..bb958c6da5 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 3f9d6540dd009135fe5b45f506a6211d4e9d6c65 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 23 Mar 2023 17:15:27 +0100 Subject: [PATCH 15/55] 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 0000000000..aeb75b57d0 --- /dev/null +++ b/changelog.d/7442.change @@ -0,0 +1 @@ +Labs: Rich Text Editor: Integrate version 1.4.0 with mention Pills support. From 0e25cf1238aa5d90057c75001c72941455716e66 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 4 Apr 2023 15:00:52 +0300 Subject: [PATCH 16/55] 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 69908f4d49..dea3ab8b77 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 0aef3397830f2bac04dbd13f9d10ccb0180ca3c1 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 5 Apr 2023 09:27:51 +0300 Subject: [PATCH 17/55] Pin Xcode version to 14.2. The app is currently failing ASC validation on using private symbols from Down. --- changelog.d/7476.build | 1 + fastlane/Fastfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7476.build diff --git a/changelog.d/7476.build b/changelog.d/7476.build new file mode 100644 index 0000000000..09ac016f17 --- /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 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ec9e02beba..6670e90647 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,7 +21,7 @@ platform :ios do before_all do # Ensure used Xcode version - xcversion(version: "~> 14.2") + xcversion(version: "14.2") end #### Public #### From dadeef7efae4c6aa499cf6eeb46742d938cd8186 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 7 Apr 2023 14:21:27 +0200 Subject: [PATCH 18/55] 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 3af8ef1fdd..c993d08320 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -69,8 +69,9 @@ + (void)initialize 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 8b90de15b8..53b42e0e26 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 0000000000..842d621b2d --- /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 9d55eb0ce656d6fcbb9e5cc3f7ea0d261d1eb828 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 11 Apr 2023 14:28:48 +0200 Subject: [PATCH 19/55] 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 75c0b64386..9870eb6da0 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 e47331a36c..5d57bb3b27 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 e6b191e2b3..cb9a162b3b 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 bb958c6da5..6a207706dd 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 f9623e351e3e68b7e92fbca6608dac5e8a29946a Mon Sep 17 00:00:00 2001 From: aringenbach Date: Tue, 11 Apr 2023 14:54:55 +0200 Subject: [PATCH 20/55] 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 bc9b8e0b26..e366ae2393 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 44199cc5bd..d05cd9f53c 100644 --- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m +++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.m @@ -358,7 +358,7 @@ - (void)pasteText:(NSString *)text 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 00de9de959..3fec13de94 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 897922832d..df71790bed 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 9abfde4213..2cead382a0 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -154,7 +154,7 @@ - (void)setAttributedTextMessage:(NSAttributedString *)attributedTextMessage { 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 @@ - (NSString *)textMessage 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 cb9a162b3b..f3fc1111bd 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 b3419fc153af47356ef7517d378b183661448f34 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 12 Apr 2023 14:43:52 +0200 Subject: [PATCH 21/55] 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 8e31a229cd..2cc989d99b 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 996e33b4a9..608f142342 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 ea245dcc83e4bd1b1d4f5f424e5e5dc25a3b0a2a Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 12 Apr 2023 14:45:59 +0200 Subject: [PATCH 22/55] 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 0000000000..3a80a3b909 --- /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 524af383db84cad158316d632c8f2aaa20dbdefb Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 12 Apr 2023 14:55:59 +0200 Subject: [PATCH 23/55] 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 675e824bad..1b6256835a 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 573fd234c7..a520117763 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 c1abd2a0ab2ca5c9d1b66cc90e87ff65255b154e Mon Sep 17 00:00:00 2001 From: aringenbach Date: Wed, 12 Apr 2023 14:56:33 +0200 Subject: [PATCH 24/55] 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 aeb75b57d0..f8ae96d5b2 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 592b58a153e2d7bd82d06a3c6b095fd7405018ce Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Wed, 12 Apr 2023 17:38:47 +0100 Subject: [PATCH 25/55] 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 dea3ab8b77..5f74246b2c 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 3c973921fa7fca5c16bd5eb6709829ddd975d9e0 Mon Sep 17 00:00:00 2001 From: aringenbach Date: Thu, 13 Apr 2023 17:40:58 +0200 Subject: [PATCH 26/55] 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 9c32b892fd..e509a58b3f 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 0000000000..a9ff595c9f --- /dev/null +++ b/changelog.d/7492.bugfix @@ -0,0 +1 @@ +Fix user suggestion list item height on iOS 16+ From 38651245ca94a93b82bb736825e2bc031a226a66 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sun, 9 Apr 2023 09:09:34 +0000 Subject: [PATCH 27/55] 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 e236e4a3de..30c871e372 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 219726861e5cdcecd8a04f0b535f5156aa973730 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Fri, 14 Apr 2023 17:35:25 +0200 Subject: [PATCH 28/55] 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 5ea1d5f2d0..278d902a57 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -103,7 +103,8 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event @"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 0000000000..2d61505079 --- /dev/null +++ b/changelog.d/pr-7501.bugfix @@ -0,0 +1 @@ +Add a foreground color attribute for the unformattable event error message. From a97059830174ff281f18f2cf7d81353651367a78 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 21 Mar 2023 18:13:00 +0000 Subject: [PATCH 29/55] 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 27481e7783..7461173c62 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 0f2e79c84e9d57e86302df6c3c7f399239949f1d Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Tue, 21 Mar 2023 14:29:13 +0000 Subject: [PATCH 30/55] 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 91fc0beeec..00f561e7ed 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 5734b54c829c1212b23c3ec6624934497c325149 Mon Sep 17 00:00:00 2001 From: random Date: Tue, 21 Mar 2023 14:57:22 +0000 Subject: [PATCH 31/55] 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 930ace2770..50678a2e47 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 d75a2d45b4ff18a0ee4bfc1c99fdfaad58bb7527 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 21 Mar 2023 20:43:38 +0000 Subject: [PATCH 32/55] 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 7b321d90ba..f63ae10c68 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 523cc41e729b69c45bf60990e8387ed1b8b8a56c 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 33/55] 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 a3750917e9..6ff1adf2e2 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 72ea691e4187fc5e9b7fb79d7638afee0ce553f5 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 21 Mar 2023 21:51:11 +0000 Subject: [PATCH 34/55] 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 7f2255f4b6..7a81166c17 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 f1040486dd3be957423b1c75be34eecbdbb38b6f Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 23 Mar 2023 09:38:53 +0000 Subject: [PATCH 35/55] 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 5990734f3d..b5077050d5 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 cb2cd86890f5a442585cd12eac3ab8bc1e443388 Mon Sep 17 00:00:00 2001 From: Vri Date: Tue, 28 Mar 2023 20:45:52 +0000 Subject: [PATCH 36/55] 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 7461173c62..6bd0096b3a 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 46e79fb5a30e26470d640924458831666948d032 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Wed, 29 Mar 2023 07:48:34 +0000 Subject: [PATCH 37/55] 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 00f561e7ed..499d511112 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 3bce03b641f2dfbd4a3d8bad813099fce573a129 Mon Sep 17 00:00:00 2001 From: random Date: Wed, 29 Mar 2023 12:32:22 +0000 Subject: [PATCH 38/55] 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 50678a2e47..d07f51b641 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 0354b9d7d08955c504628ef4aa8b5bc324219377 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 28 Mar 2023 18:01:13 +0000 Subject: [PATCH 39/55] 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 f63ae10c68..65ff758cb7 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 09213b053611403f6d5127c2e426e192b5acae76 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 40/55] 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 6ff1adf2e2..12a8f95579 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 62698d7a1cb0eab782ce74d94b058a863927b666 Mon Sep 17 00:00:00 2001 From: Linerly Date: Tue, 28 Mar 2023 23:49:36 +0000 Subject: [PATCH 41/55] 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 b5077050d5..1aea826f30 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 1000a208d19ed3f1a499b60a62d9603b20a7e9c3 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sun, 9 Apr 2023 09:49:23 +0000 Subject: [PATCH 42/55] 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 07a5f408ba..9bc6bfb821 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 ba834cc65646c2b6261561b99b7e20a99d2b01c6 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 28 Mar 2023 23:44:02 +0000 Subject: [PATCH 43/55] 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 7a81166c17..8399cd6ff4 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 36172a10c64e99cbf934bdb561e9fc4b37a6b3ad Mon Sep 17 00:00:00 2001 From: Vri Date: Fri, 31 Mar 2023 08:07:53 +0000 Subject: [PATCH 44/55] 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 6bd0096b3a..6a4f9c0189 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 043804a8a244cf83aa9b062b47724562ba4d6bec Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 3 Apr 2023 16:23:28 +0000 Subject: [PATCH 45/55] 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 499d511112..e16253fdfc 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 3f335c788fd99a4dd6d5af08bff86ef41cfe6486 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Mon, 3 Apr 2023 18:12:25 +0000 Subject: [PATCH 46/55] 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 e16253fdfc..d19d880cfe 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 5fb5d913bccdf24a31285de0b3844ea060dc4a98 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Thu, 6 Apr 2023 17:29:25 +0000 Subject: [PATCH 47/55] 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 d19d880cfe..04b8c5dfd0 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 f147b37f85c4d2c3981de876a4e3138f3a7db596 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 5 Apr 2023 16:06:11 +0000 Subject: [PATCH 48/55] 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 c3f99464a3..a77322f083 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 cd314d158630d2e2bb7ff66f8a4154a8d8329c88 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 5 Apr 2023 17:11:42 +0000 Subject: [PATCH 49/55] 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 4274f5dc5a..fa28608958 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 56ec8be2fd6d3844192ebbbed1d1e5592df8eef4 Mon Sep 17 00:00:00 2001 From: Peter Chen Date: Sun, 9 Apr 2023 09:52:37 +0000 Subject: [PATCH 50/55] 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 04b8c5dfd0..167652a1d5 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 57d3b27b82a9a74ad4447c986f3e6561f18621c2 Mon Sep 17 00:00:00 2001 From: SmallJinn Date: Mon, 17 Apr 2023 09:22:12 +0000 Subject: [PATCH 51/55] 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 8578457076..0f2f21fd42 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 d72dcf40de4f5898350653c415bbed0ddadd7605 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 17:41:11 +0100 Subject: [PATCH 52/55] 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 f716233aea..f4cbeda08c 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 0000000000..0562050568 --- /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 89d891876a4e0e31aad71828409326e171638f88 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 17:41:11 +0100 Subject: [PATCH 53/55] 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 af7fdc039b..428bd30f7a 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 f8ae96d5b2..0000000000 --- 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 3a80a3b909..0000000000 --- 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 09ac016f17..0000000000 --- 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 a9ff595c9f..0000000000 --- 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 842d621b2d..0000000000 --- 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 2d61505079..0000000000 --- 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 0562050568..0000000000 --- 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 075d1c8779fb1e2317068910db4ec088c0159cd5 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 12:02:01 +0100 Subject: [PATCH 54/55] 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 9c9d7f4288..e2a9e4a01e 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -344,7 +344,7 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent*)event 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 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent*)event // 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 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent*)event 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 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent*)event 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 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent*)event } 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 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent*)event // 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 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent*)event 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 @@ - (NSString*)buildHTMLStringForEvent:(MXEvent*)event inReplyToEvent:(MXEvent*)re // 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 @@ - (NSString*)buildHTMLStringForEvent:(MXEvent*)event inReplyToEvent:(MXEvent*)re } else { - MXLogWarning(@"[MXKEventFormatter] Unable to build reply event %@", event.description) + MXLogWarning(@"[MXKEventFormatter] Unable to build reply event %@", event.eventId) } return html; From 0d47edfbd4c7401915763b9c7c0c74396d75b504 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 18 Apr 2023 20:11:34 +0100 Subject: [PATCH 55/55] finish version++ --- Podfile.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index c4d534f968..f47ccb4f34 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