From 7386d02681a8a2ee5667cd6ba3b539363b71dbf8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 2 Oct 2023 00:47:08 +0100 Subject: [PATCH 1/8] implement emotes (/me) requires matthew/emotes branch of matrix-rust-sdk --- .../RoomScreen/RoomScreenViewModel.swift | 11 ++++++- .../Sources/Services/Room/RoomProxy.swift | 33 +++++++++++++++---- .../EmoteRoomTimelineItemContent.swift | 2 ++ .../RoomTimelineItemFactory.swift | 2 +- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 88535075e6..0f6a07308c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -21,6 +21,7 @@ import SwiftUI typealias RoomScreenViewModelType = StateStoreViewModel +// swiftlint:disable type_body_length class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol { private enum Constants { static let backPaginationEventLimit: UInt = 20 @@ -615,10 +616,16 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } else { text = messageTimelineItem.body } + case .emote(let emoteItem): + if ServiceLocator.shared.settings.richTextEditorEnabled, let formattedBodyHTMLString = emoteItem.formattedBodyHTMLString { + text = "/me " + formattedBodyHTMLString + } else { + text = "/me " + messageTimelineItem.body + } default: text = messageTimelineItem.body } - + actionsSubject.send(.composer(action: .setText(text: text))) actionsSubject.send(.composer(action: .setMode(mode: .edit(originalItemId: messageTimelineItem.id)))) case .copyPermalink: @@ -882,6 +889,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } +// swiftlint:enable type_body_length + private extension RoomProxyProtocol { /// Checks if the other person left the room in a direct chat var isUserAloneInDirectRoom: Bool { diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 9f7c48705a..553ecc339c 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -253,17 +253,34 @@ class RoomProxy: RoomProxyProtocol { } } + func checkEmote(body: inout String, htmlBody: inout String?) -> Bool { + if body.starts(with: "/me ") { + body = body.replacing("/me ", with: "") + let html = htmlBody + if let html { + htmlBody = html.replacing("/me ", with: "") + } + return true + } else { + return false + } + } + func sendMessage(_ message: String, html: String?, inReplyTo eventID: String? = nil) async -> Result { sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) defer { sendMessageBackgroundTask?.stop() } + var body: String = message + var htmlBody: String? = html + var emote: Bool = checkEmote(body: &body, htmlBody: &htmlBody) + let messageContent: RoomMessageEventContentWithoutRelation - if let html { - messageContent = messageEventContentFromHtml(body: message, htmlBody: html) + if let htmlBody { + messageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody, emote: emote) } else { - messageContent = messageEventContentFromMarkdown(md: message) + messageContent = messageEventContentFromMarkdown(md: body, emote: emote) } return await Task.dispatch(on: messageSendingDispatchQueue) { do { @@ -441,11 +458,15 @@ class RoomProxy: RoomProxyProtocol { sendMessageBackgroundTask?.stop() } + var body: String = newMessage + var htmlBody: String? = html + let emote: Bool = checkEmote(body: &body, htmlBody: &htmlBody) + let newMessageContent: RoomMessageEventContentWithoutRelation - if let html { - newMessageContent = messageEventContentFromHtml(body: newMessage, htmlBody: html) + if let htmlBody { + newMessageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody, emote: emote) } else { - newMessageContent = messageEventContentFromMarkdown(md: newMessage) + newMessageContent = messageEventContentFromMarkdown(md: body, emote: emote) } return await Task.dispatch(on: messageSendingDispatchQueue) { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItemContent.swift index d190e369b3..6afb976433 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItemContent.swift @@ -19,4 +19,6 @@ import UIKit struct EmoteRoomTimelineItemContent: Hashable { let body: String var formattedBody: AttributedString? + /// The original textual representation of the formatted body directly from the event (usually HTML code) + var formattedBodyHTMLString: String? } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 92aa6a6c93..bec7d786aa 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -558,7 +558,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { formattedBody = attributedStringBuilder.fromPlain(L10n.commonEmote(name, messageContent.body)) } - return .init(body: messageContent.body, formattedBody: formattedBody) + return .init(body: messageContent.body, formattedBody: formattedBody, formattedBodyHTMLString: htmlBody) } // MARK: - State Events From 095853ac98ee5bf53c63181df43a1582b34eee72 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 2 Oct 2023 00:53:47 +0100 Subject: [PATCH 2/8] changelog + whitespace --- changelog.d/1841.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1841.feature diff --git a/changelog.d/1841.feature b/changelog.d/1841.feature new file mode 100644 index 0000000000..488d7887a8 --- /dev/null +++ b/changelog.d/1841.feature @@ -0,0 +1 @@ +Implement /me From 88988d9edd65e975e820f070f2d2894a742d1a42 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 2 Oct 2023 00:55:03 +0100 Subject: [PATCH 3/8] remove apparently superfluous swiftlint --- ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 0f6a07308c..8375ca8370 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -21,7 +21,6 @@ import SwiftUI typealias RoomScreenViewModelType = StateStoreViewModel -// swiftlint:disable type_body_length class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol { private enum Constants { static let backPaginationEventLimit: UInt = 20 @@ -889,8 +888,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } -// swiftlint:enable type_body_length - private extension RoomProxyProtocol { /// Checks if the other person left the room in a direct chat var isUserAloneInDirectRoom: Bool { From 26bfd09891e03ca04ede2cdf5237c00c9da2e840 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 2 Oct 2023 00:59:28 +0100 Subject: [PATCH 4/8] fix constness --- ElementX/Sources/Services/Room/RoomProxy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 553ecc339c..00d44800e0 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -274,7 +274,7 @@ class RoomProxy: RoomProxyProtocol { var body: String = message var htmlBody: String? = html - var emote: Bool = checkEmote(body: &body, htmlBody: &htmlBody) + let emote: Bool = checkEmote(body: &body, htmlBody: &htmlBody) let messageContent: RoomMessageEventContentWithoutRelation if let htmlBody { From 93778539c65c36cd87aa2a2b81493f2bf64e63c6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 2 Oct 2023 12:24:16 +0100 Subject: [PATCH 5/8] remove inout params and incorporate review --- .../Sources/Services/Room/RoomProxy.swift | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 00d44800e0..667843f6b8 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -252,20 +252,7 @@ class RoomProxy: RoomProxyProtocol { return .success(()) } } - - func checkEmote(body: inout String, htmlBody: inout String?) -> Bool { - if body.starts(with: "/me ") { - body = body.replacing("/me ", with: "") - let html = htmlBody - if let html { - htmlBody = html.replacing("/me ", with: "") - } - return true - } else { - return false - } - } - + func sendMessage(_ message: String, html: String?, inReplyTo eventID: String? = nil) async -> Result { sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) defer { @@ -274,13 +261,16 @@ class RoomProxy: RoomProxyProtocol { var body: String = message var htmlBody: String? = html - let emote: Bool = checkEmote(body: &body, htmlBody: &htmlBody) + let isEmote: Bool = isEmote(body: body, htmlBody: htmlBody) + if isEmote { + (body, htmlBody) = buildEmote(body: body, htmlBody: htmlBody) + } let messageContent: RoomMessageEventContentWithoutRelation if let htmlBody { - messageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody, emote: emote) + messageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody, emote: isEmote) } else { - messageContent = messageEventContentFromMarkdown(md: body, emote: emote) + messageContent = messageEventContentFromMarkdown(md: body, emote: isEmote) } return await Task.dispatch(on: messageSendingDispatchQueue) { do { @@ -460,13 +450,16 @@ class RoomProxy: RoomProxyProtocol { var body: String = newMessage var htmlBody: String? = html - let emote: Bool = checkEmote(body: &body, htmlBody: &htmlBody) - + let isEmote: Bool = isEmote(body: body, htmlBody: htmlBody) + if isEmote { + (body, htmlBody) = buildEmote(body: body, htmlBody: htmlBody) + } + let newMessageContent: RoomMessageEventContentWithoutRelation if let htmlBody { - newMessageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody, emote: emote) + newMessageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody, emote: isEmote) } else { - newMessageContent = messageEventContentFromMarkdown(md: body, emote: emote) + newMessageContent = messageEventContentFromMarkdown(md: body, emote: isEmote) } return await Task.dispatch(on: messageSendingDispatchQueue) { @@ -728,7 +721,20 @@ class RoomProxy: RoomProxyProtocol { } // MARK: - Private + + private func isEmote(body: String, htmlBody: String?) -> Bool { + body.starts(with: "/me ") + } + private func buildEmote(body: String, htmlBody: String?) -> (String, String?) { + let newBody = body.replacing("/me ", with: "") + var newHtmlBody = htmlBody + if let htmlBody { + newHtmlBody = htmlBody.replacing("/me ", with: "") + } + return (newBody, newHtmlBody) + } + /// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener /// This should become automatic on the RustSDK side at some point private func fetchMembers() async { From 6c38f720f957c55e973e502f613050614afbad78 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 2 Oct 2023 17:31:43 +0100 Subject: [PATCH 6/8] switch to new api based on sdk PR feedback --- .../Sources/Services/Room/RoomProxy.swift | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 667843f6b8..9fb9c6ad83 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -268,9 +268,17 @@ class RoomProxy: RoomProxyProtocol { let messageContent: RoomMessageEventContentWithoutRelation if let htmlBody { - messageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody, emote: isEmote) + if isEmote { + messageContent = messageEventContentFromHtmlAsEmote(body: body, htmlBody: htmlBody) + } else { + messageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody) + } } else { - messageContent = messageEventContentFromMarkdown(md: body, emote: isEmote) + if isEmote { + messageContent = messageEventContentFromMarkdownAsEmote(md: body) + } else { + messageContent = messageEventContentFromMarkdown(md: body) + } } return await Task.dispatch(on: messageSendingDispatchQueue) { do { @@ -457,11 +465,18 @@ class RoomProxy: RoomProxyProtocol { let newMessageContent: RoomMessageEventContentWithoutRelation if let htmlBody { - newMessageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody, emote: isEmote) + if isEmote { + newMessageContent = messageEventContentFromHtmlAsEmote(body: body, htmlBody: htmlBody) + } else { + newMessageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody) + } } else { - newMessageContent = messageEventContentFromMarkdown(md: body, emote: isEmote) + if isEmote { + newMessageContent = messageEventContentFromMarkdownAsEmote(md: body) + } else { + newMessageContent = messageEventContentFromMarkdown(md: body) + } } - return await Task.dispatch(on: messageSendingDispatchQueue) { do { let originalEvent = try self.room.getEventTimelineItemByEventId(eventId: eventID) From f5cf1dbf87bc645b3a58a8e7369e9ae5805306a1 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 3 Oct 2023 15:30:34 +0300 Subject: [PATCH 7/8] Bump the RustSDK to v1.1.21 --- ElementX.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- project.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 1efe0f50ef..a2215b29b7 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -5801,7 +5801,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.1.20; + version = 1.1.21; }; }; 821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4fd9ea8f9a..0d1990987d 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -129,8 +129,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "5f3a195f6a461b4d40a8a705a3af8235711f12f5", - "version" : "1.1.20" + "revision" : "22461632db17a6dc1193dcdcfa3231614649c517", + "version" : "1.1.21" } }, { diff --git a/project.yml b/project.yml index b019236cb4..4c0c702e91 100644 --- a/project.yml +++ b/project.yml @@ -46,7 +46,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 1.1.20 + exactVersion: 1.1.21 # path: ../matrix-rust-sdk DesignKit: path: DesignKit From 984e9a523e497bf624f073bbbd6c132ea9f3e658 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 3 Oct 2023 15:30:48 +0300 Subject: [PATCH 8/8] Address PR comments --- .../Sources/Services/Room/RoomProxy.swift | 81 +++++++------------ 1 file changed, 30 insertions(+), 51 deletions(-) diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 9fb9c6ad83..fde921aaba 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -259,27 +259,8 @@ class RoomProxy: RoomProxyProtocol { sendMessageBackgroundTask?.stop() } - var body: String = message - var htmlBody: String? = html - let isEmote: Bool = isEmote(body: body, htmlBody: htmlBody) - if isEmote { - (body, htmlBody) = buildEmote(body: body, htmlBody: htmlBody) - } + let messageContent = buildMessageContentFor(message, html: html) - let messageContent: RoomMessageEventContentWithoutRelation - if let htmlBody { - if isEmote { - messageContent = messageEventContentFromHtmlAsEmote(body: body, htmlBody: htmlBody) - } else { - messageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody) - } - } else { - if isEmote { - messageContent = messageEventContentFromMarkdownAsEmote(md: body) - } else { - messageContent = messageEventContentFromMarkdown(md: body) - } - } return await Task.dispatch(on: messageSendingDispatchQueue) { do { if let eventID { @@ -450,37 +431,18 @@ class RoomProxy: RoomProxyProtocol { } } - func editMessage(_ newMessage: String, html: String?, original eventID: String) async -> Result { + func editMessage(_ message: String, html: String?, original eventID: String) async -> Result { sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) defer { sendMessageBackgroundTask?.stop() } - var body: String = newMessage - var htmlBody: String? = html - let isEmote: Bool = isEmote(body: body, htmlBody: htmlBody) - if isEmote { - (body, htmlBody) = buildEmote(body: body, htmlBody: htmlBody) - } - - let newMessageContent: RoomMessageEventContentWithoutRelation - if let htmlBody { - if isEmote { - newMessageContent = messageEventContentFromHtmlAsEmote(body: body, htmlBody: htmlBody) - } else { - newMessageContent = messageEventContentFromHtml(body: body, htmlBody: htmlBody) - } - } else { - if isEmote { - newMessageContent = messageEventContentFromMarkdownAsEmote(md: body) - } else { - newMessageContent = messageEventContentFromMarkdown(md: body) - } - } + let messageContent = buildMessageContentFor(message, html: html) + return await Task.dispatch(on: messageSendingDispatchQueue) { do { let originalEvent = try self.room.getEventTimelineItemByEventId(eventId: eventID) - try self.room.edit(newContent: newMessageContent, editItem: originalEvent) + try self.room.edit(newContent: messageContent, editItem: originalEvent) return .success(()) } catch { return .failure(.failedEditingMessage) @@ -737,17 +699,34 @@ class RoomProxy: RoomProxyProtocol { // MARK: - Private - private func isEmote(body: String, htmlBody: String?) -> Bool { - body.starts(with: "/me ") + private func buildMessageContentFor(_ message: String, html: String?) -> RoomMessageEventContentWithoutRelation { + let emoteSlashCommand = "/me " + let isEmote: Bool = message.starts(with: emoteSlashCommand) + + guard isEmote else { + if let html { + return messageEventContentFromHtml(body: message, htmlBody: html) + } else { + return messageEventContentFromMarkdown(md: message) + } + } + + let emoteMessage = String(message.dropFirst(emoteSlashCommand.count)) + + var emoteHtml: String? + if let html { + emoteHtml = String(html.dropFirst(emoteSlashCommand.count)) + } + + return buildEmoteMessageContentFor(emoteMessage, html: emoteHtml) } - private func buildEmote(body: String, htmlBody: String?) -> (String, String?) { - let newBody = body.replacing("/me ", with: "") - var newHtmlBody = htmlBody - if let htmlBody { - newHtmlBody = htmlBody.replacing("/me ", with: "") + private func buildEmoteMessageContentFor(_ message: String, html: String?) -> RoomMessageEventContentWithoutRelation { + if let html { + return messageEventContentFromHtmlAsEmote(body: message, htmlBody: html) + } else { + return messageEventContentFromMarkdownAsEmote(md: message) } - return (newBody, newHtmlBody) } /// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener