mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Render pills in RTE (#1874)
* for now this can only work locally * works * added a test * updated RTE * revert package * pr suggestion * reverting package versions * better naming
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -985,7 +985,7 @@
|
||||
127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = "<group>"; };
|
||||
12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = "<group>"; };
|
||||
1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = "<group>"; };
|
||||
130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = "<group>"; };
|
||||
13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||
@@ -1147,7 +1147,7 @@
|
||||
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = "<group>"; };
|
||||
471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = "<group>"; };
|
||||
478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; };
|
||||
478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; };
|
||||
4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; };
|
||||
47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
||||
47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
@@ -1353,7 +1353,7 @@
|
||||
8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = "<group>"; };
|
||||
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
|
||||
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
|
||||
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = "<group>"; };
|
||||
8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = "<group>"; };
|
||||
8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = "<group>"; };
|
||||
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; };
|
||||
@@ -1471,7 +1471,7 @@
|
||||
B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = "<group>"; };
|
||||
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = "<group>"; };
|
||||
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = "<group>"; };
|
||||
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = "<group>"; };
|
||||
B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -1567,7 +1567,7 @@
|
||||
CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = "<group>"; };
|
||||
CE1CD5EC6265A09315772DB7 /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = "<group>"; };
|
||||
CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; };
|
||||
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = "<group>"; };
|
||||
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = "<group>"; };
|
||||
CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = "<group>"; };
|
||||
D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = "<group>"; };
|
||||
@@ -1659,7 +1659,7 @@
|
||||
ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = "<group>"; };
|
||||
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
|
||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = "<group>"; };
|
||||
ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; };
|
||||
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
|
||||
@@ -1673,7 +1673,7 @@
|
||||
F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = "<group>"; };
|
||||
F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = "<group>"; };
|
||||
F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = "<group>"; };
|
||||
F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = "<group>"; };
|
||||
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = "<group>"; };
|
||||
F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = "<group>"; };
|
||||
@@ -5996,7 +5996,7 @@
|
||||
repositoryURL = "https://github.com/matrix-org/matrix-wysiwyg-composer-swift";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 2.14.1;
|
||||
version = 2.14.2;
|
||||
};
|
||||
};
|
||||
96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = {
|
||||
|
||||
@@ -138,8 +138,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
|
||||
"state" : {
|
||||
"revision" : "6e1a77e6061c4148caa087221bc1750ceeb37724",
|
||||
"version" : "2.14.1"
|
||||
"revision" : "6e567f647f34b58f0ff6d48f2bb6e535e58a6593",
|
||||
"version" : "2.14.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -165,8 +165,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/PostHog/posthog-ios",
|
||||
"state" : {
|
||||
"revision" : "3a6afc24d6bde730a19470d4e6b713f44d076ad9",
|
||||
"version" : "2.0.3"
|
||||
"revision" : "9298825fe26899a58b976d400254e0b050daa578",
|
||||
"version" : "2.0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -210,8 +210,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections",
|
||||
"state" : {
|
||||
"revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
|
||||
"version" : "1.0.4"
|
||||
"revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307",
|
||||
"version" : "1.0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -220,15 +220,15 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func addAllUsersMention(_ attributedString: NSMutableAttributedString) {
|
||||
func addAllUsersMention(_ attributedString: NSMutableAttributedString) {
|
||||
MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []).forEach { match in
|
||||
if attributedString.attribute(.link, at: 0, longestEffectiveRange: nil, in: match.range) == nil {
|
||||
if attributedString.attribute(.link, at: 0, longestEffectiveRange: nil, in: match.range) as? URL == nil {
|
||||
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func detectPermalinks(_ attributedString: NSMutableAttributedString) {
|
||||
func detectPermalinks(_ attributedString: NSMutableAttributedString) {
|
||||
attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
|
||||
if value != nil {
|
||||
if let url = value as? URL {
|
||||
|
||||
@@ -37,7 +37,7 @@ struct MentionBuilder: MentionBuilderProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID]
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID, .font: font]
|
||||
if let blockquote {
|
||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
||||
attachmentAttributes[.MatrixBlockquote] = blockquote
|
||||
@@ -61,7 +61,7 @@ struct MentionBuilder: MentionBuilderProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [:]
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: font]
|
||||
if let blockquote {
|
||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
||||
attachmentAttributes[.MatrixBlockquote] = blockquote
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
final class MessageTextView: UITextView {
|
||||
final class MessageTextView: UITextView, PillAttachmentViewProviderDelegate {
|
||||
var roomContext: RoomScreenViewModel.Context?
|
||||
var updateClosure: (() -> Void)?
|
||||
|
||||
@@ -44,6 +44,10 @@ final class MessageTextView: UITextView {
|
||||
invalidateTextAttachmentsDisplay(update: false)
|
||||
super.layoutSubviews()
|
||||
}
|
||||
|
||||
func registerPillView(_ pillView: UIView) {
|
||||
// No need to be implemented in this view
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageText: UIViewRepresentable {
|
||||
|
||||
@@ -18,8 +18,17 @@ import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
import UIKit
|
||||
|
||||
import WysiwygComposer
|
||||
|
||||
protocol PillAttachmentViewProviderDelegate: AnyObject {
|
||||
var roomContext: RoomScreenViewModel.Context? { get }
|
||||
|
||||
func registerPillView(_ pillView: UIView)
|
||||
func invalidateTextAttachmentsDisplay(update: Bool)
|
||||
}
|
||||
|
||||
final class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
||||
private weak var messageTextView: MessageTextView?
|
||||
private weak var delegate: PillAttachmentViewProviderDelegate?
|
||||
|
||||
// MARK: - Override
|
||||
|
||||
@@ -27,7 +36,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
||||
super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location)
|
||||
|
||||
// Keep a reference to the parent text view for size adjustments and pills flushing.
|
||||
messageTextView = parentView?.superview as? MessageTextView
|
||||
delegate = parentView?.superview as? PillAttachmentViewProviderDelegate
|
||||
tracksTextAttachmentViewBounds = true
|
||||
}
|
||||
|
||||
@@ -46,7 +55,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
||||
// The mock viewModel simulates the loading logic for testing purposes
|
||||
context = PillContext.mock(type: .loadUser(isOwn: false))
|
||||
imageProvider = MockMediaProvider()
|
||||
} else if let roomContext = messageTextView?.roomContext {
|
||||
} else if let roomContext = delegate?.roomContext {
|
||||
context = PillContext(roomContext: roomContext, data: textAttachmentData)
|
||||
imageProvider = roomContext.imageProvider
|
||||
} else {
|
||||
@@ -55,12 +64,34 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
||||
}
|
||||
|
||||
let view = PillView(imageProvider: imageProvider, context: context) { [weak self] in
|
||||
self?.messageTextView?.invalidateTextAttachmentsDisplay(update: true)
|
||||
self?.delegate?.invalidateTextAttachmentsDisplay(update: true)
|
||||
}
|
||||
let controller = UIHostingController(rootView: view)
|
||||
controller.view.backgroundColor = .clear
|
||||
// This allows the text view to handle it as a link
|
||||
controller.view.isUserInteractionEnabled = false
|
||||
self.view = controller.view
|
||||
delegate?.registerPillView(controller.view)
|
||||
}
|
||||
}
|
||||
|
||||
final class ComposerMentionDisplayHelper: MentionDisplayHelper {
|
||||
weak var roomContext: RoomScreenViewModel.Context?
|
||||
|
||||
init(roomContext: RoomScreenViewModel.Context) {
|
||||
self.roomContext = roomContext
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static var mock: Self {
|
||||
Self(roomContext: RoomScreenViewModel.mock.context)
|
||||
}
|
||||
}
|
||||
|
||||
extension WysiwygTextView: PillAttachmentViewProviderDelegate {
|
||||
var roomContext: RoomScreenViewModel.Context? {
|
||||
(mentionDisplayHelper as? ComposerMentionDisplayHelper)?.roomContext
|
||||
}
|
||||
|
||||
func invalidateTextAttachmentsDisplay(update: Bool) { }
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum PillType: Codable {
|
||||
enum PillType: Codable, Equatable {
|
||||
/// A pill that mentions a user
|
||||
case user(userID: String)
|
||||
/// A pill that mentions all users in a room
|
||||
@@ -29,7 +29,7 @@ struct PillTextAttachmentData {
|
||||
|
||||
/// Pill type
|
||||
let type: PillType
|
||||
|
||||
|
||||
/// Font for the display name
|
||||
let font: UIFont
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
|
||||
private var currentLinkData: WysiwygLinkData?
|
||||
|
||||
init(wysiwygViewModel: WysiwygComposerViewModel, completionSuggestionService: CompletionSuggestionServiceProtocol, mediaProvider: MediaProviderProtocol, appSettings: AppSettings) {
|
||||
init(wysiwygViewModel: WysiwygComposerViewModel, completionSuggestionService: CompletionSuggestionServiceProtocol, mediaProvider: MediaProviderProtocol, appSettings: AppSettings, mentionDisplayHelper: MentionDisplayHelper) {
|
||||
self.wysiwygViewModel = wysiwygViewModel
|
||||
self.completionSuggestionService = completionSuggestionService
|
||||
self.appSettings = appSettings
|
||||
|
||||
|
||||
super.init(initialViewState: ComposerToolbarViewState(areSuggestionsEnabled: completionSuggestionService.areSuggestionsEnabled, bindings: .init()), imageProvider: mediaProvider)
|
||||
|
||||
context.$viewState
|
||||
@@ -84,6 +84,10 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
completionSuggestionService.suggestionsPublisher
|
||||
.weakAssign(to: \.state.suggestions, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
if appSettings.mentionsEnabled {
|
||||
setupMentionsHandling(mentionDisplayHelper: mentionDisplayHelper)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -155,6 +159,27 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupMentionsHandling(mentionDisplayHelper: MentionDisplayHelper) {
|
||||
wysiwygViewModel.textView.mentionDisplayHelper = mentionDisplayHelper
|
||||
|
||||
let attributedStringBuilder = AttributedStringBuilder(cacheKey: "Composer", permalinkBaseURL: appSettings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: appSettings.mentionsEnabled))
|
||||
|
||||
wysiwygViewModel.mentionReplacer = ComposerMentionReplacer { urlString, string in
|
||||
let attributedString: NSMutableAttributedString
|
||||
// This is the all room mention case
|
||||
if urlString == "#" {
|
||||
attributedString = NSMutableAttributedString(string: string, attributes: [.MatrixAllUsersMention: true])
|
||||
} else {
|
||||
attributedString = NSMutableAttributedString(string: string, attributes: [.link: URL(string: urlString) as Any])
|
||||
}
|
||||
attributedStringBuilder.detectPermalinks(attributedString)
|
||||
|
||||
// In RTE mentions don't need to be handled as links
|
||||
attributedString.removeAttribute(.link, range: NSRange(location: 0, length: attributedString.length))
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSuggestion(_ suggestion: SuggestionItem) {
|
||||
switch suggestion {
|
||||
case let .user(item):
|
||||
@@ -177,6 +202,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
}
|
||||
|
||||
private func set(text: String) {
|
||||
wysiwygViewModel.textView.flushPills()
|
||||
wysiwygViewModel.setHtmlContent(text)
|
||||
}
|
||||
|
||||
@@ -293,3 +319,25 @@ private extension LinkAction {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
private final class ComposerMentionReplacer: MentionReplacer {
|
||||
let replacementForMentionClosure: (_ urlString: String, _ text: String) -> (NSAttributedString?)
|
||||
|
||||
init(replacementForMentionClosure: @escaping (String, String) -> (NSAttributedString?)) {
|
||||
self.replacementForMentionClosure = replacementForMentionClosure
|
||||
}
|
||||
|
||||
// There is no internal Markdown to RTE switch implemented yet in the room so this one is never called
|
||||
func postProcessMarkdown(in attributedString: NSAttributedString) -> NSAttributedString {
|
||||
attributedString
|
||||
}
|
||||
|
||||
// There is no internal RTE to Markdown switch implemented yet in the room so this one is never called
|
||||
func restoreMarkdown(in attributedString: NSAttributedString) -> String {
|
||||
attributedString.string
|
||||
}
|
||||
|
||||
func replacementForMention(_ url: String, text: String) -> NSAttributedString? {
|
||||
replacementForMentionClosure(url, text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,8 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
||||
static let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
|
||||
static let suggestions: [SuggestionItem] = [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)),
|
||||
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory))]
|
||||
|
||||
@@ -223,7 +224,8 @@ extension ComposerToolbar {
|
||||
let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
|
||||
return ComposerToolbar(context: composerViewModel.context,
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
keyCommandHandler: { _ in false })
|
||||
|
||||
@@ -118,7 +118,8 @@ struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = ComposerToolbarViewModel(wysiwygViewModel: WysiwygComposerViewModel(),
|
||||
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
|
||||
|
||||
static var previews: some View {
|
||||
RoomAttachmentPicker(context: viewModel.context)
|
||||
|
||||
@@ -71,7 +71,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
|
||||
completionSuggestionService: parameters.completionSuggestionService,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
appSettings: parameters.appSettings)
|
||||
appSettings: parameters.appSettings,
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper(roomContext: viewModel.context))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
@@ -31,13 +31,15 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
AppSettings.reset()
|
||||
appSettings = AppSettings()
|
||||
appSettings.richTextEditorEnabled = true
|
||||
appSettings.mentionsEnabled = true
|
||||
ServiceLocator.shared.register(appSettings: appSettings)
|
||||
wysiwygViewModel = WysiwygComposerViewModel()
|
||||
completionSuggestionServiceMock = CompletionSuggestionServiceMock(configuration: .init())
|
||||
viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
|
||||
completionSuggestionService: completionSuggestionServiceMock,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: appSettings,
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
|
||||
}
|
||||
|
||||
func testComposerFocus() {
|
||||
@@ -107,7 +109,8 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
|
||||
completionSuggestionService: mockCompletionSuggestionService,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
|
||||
|
||||
XCTAssertEqual(viewModel.state.suggestions, suggestions)
|
||||
}
|
||||
@@ -126,4 +129,13 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(wysiwygViewModel.content.html, "<a data-mention-type=\"user\" href=\"https://matrix.to/#/@test:matrix.org\" contenteditable=\"false\">Test</a> ")
|
||||
}
|
||||
|
||||
func testUserMentionInRTE() {
|
||||
let userID = "@test:matrix.org"
|
||||
let suggestion = SuggestionItem.user(item: .init(id: userID, displayName: "Test", avatarURL: nil))
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
let attachment = wysiwygViewModel.textView.attributedText.attribute(.attachment, at: 0, effectiveRange: nil) as? PillTextAttachment
|
||||
XCTAssertEqual(attachment?.pillData?.type, .user(userID: userID))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -115,7 +115,8 @@ packages:
|
||||
minorVersion: 2.0.0
|
||||
WysiwygComposer:
|
||||
url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift
|
||||
exactVersion: 2.14.1
|
||||
exactVersion: 2.14.2
|
||||
# path: ../matrix-wysiwyg/platforms/ios/lib/WysiwygComposer
|
||||
SwiftOGG:
|
||||
url: https://github.com/vector-im/swift-ogg
|
||||
branch: 0.0.1
|
||||
|
||||
Reference in New Issue
Block a user