mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Store and restore drafts (#2898)
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
final class ComposerDraftService: ComposerDraftServiceProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let timelineItemfactory: RoomTimelineItemFactoryProtocol
|
||||
|
||||
init(roomProxy: RoomProxyProtocol, timelineItemfactory: RoomTimelineItemFactoryProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
self.timelineItemfactory = timelineItemfactory
|
||||
}
|
||||
|
||||
func saveDraft(_ draft: ComposerDraftProxy) async -> Result<Void, ComposerDraftServiceError> {
|
||||
switch await roomProxy.saveDraft(draft.toRust) {
|
||||
case .success:
|
||||
MXLog.info("Successfully saved draft")
|
||||
return .success(())
|
||||
case .failure(let error):
|
||||
MXLog.info("Failed to save draft: \(error)")
|
||||
return .failure(.failedToSaveDraft)
|
||||
}
|
||||
}
|
||||
|
||||
func loadDraft() async -> Result<ComposerDraftProxy?, ComposerDraftServiceError> {
|
||||
switch await roomProxy.loadDraft() {
|
||||
case .success(let draft):
|
||||
guard let draft else {
|
||||
return .success(nil)
|
||||
}
|
||||
return .success(ComposerDraftProxy(from: draft))
|
||||
case .failure(let error):
|
||||
MXLog.info("Failed to load draft: \(error)")
|
||||
return .failure(.failedToLoadDraft)
|
||||
}
|
||||
}
|
||||
|
||||
func getReply(eventID: String) async -> Result<TimelineItemReply, ComposerDraftServiceError> {
|
||||
switch await roomProxy.timeline.getLoadedReplyDetails(eventID: eventID) {
|
||||
case .success(let replyDetails):
|
||||
return await .success(timelineItemfactory.buildReply(details: replyDetails))
|
||||
case .failure(let error):
|
||||
MXLog.error("Could not load reply: \(error)")
|
||||
return .failure(.failedToLoadReply)
|
||||
}
|
||||
}
|
||||
|
||||
func clearDraft() async -> Result<Void, ComposerDraftServiceError> {
|
||||
switch await roomProxy.clearDraft() {
|
||||
case .success:
|
||||
MXLog.info("Successfully cleared draft")
|
||||
return .success(())
|
||||
case .failure(let error):
|
||||
MXLog.info("Failed to clear draft: \(error)")
|
||||
return .failure(.failedToClearDraft)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
struct ComposerDraftProxy: Equatable {
|
||||
enum ComposerDraftType: Equatable {
|
||||
case newMessage
|
||||
case reply(eventID: String)
|
||||
case edit(eventID: String)
|
||||
|
||||
var toRust: MatrixRustSDK.ComposerDraftType {
|
||||
switch self {
|
||||
case .newMessage:
|
||||
return .newMessage
|
||||
case .edit(let eventID):
|
||||
return .edit(eventId: eventID)
|
||||
case .reply(let eventID):
|
||||
return .reply(eventId: eventID)
|
||||
}
|
||||
}
|
||||
|
||||
init(from rustDraftType: MatrixRustSDK.ComposerDraftType) {
|
||||
switch rustDraftType {
|
||||
case .newMessage:
|
||||
self = .newMessage
|
||||
case .edit(let eventID):
|
||||
self = .edit(eventID: eventID)
|
||||
case .reply(let eventID):
|
||||
self = .reply(eventID: eventID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let plainText: String
|
||||
let htmlText: String?
|
||||
let draftType: ComposerDraftType
|
||||
|
||||
var toRust: ComposerDraft {
|
||||
ComposerDraft(plainText: plainText, htmlText: htmlText, draftType: draftType.toRust)
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposerDraftProxy {
|
||||
init(from rustDraft: ComposerDraft) {
|
||||
plainText = rustDraft.plainText
|
||||
htmlText = rustDraft.htmlText
|
||||
draftType = ComposerDraftType(from: rustDraft.draftType)
|
||||
}
|
||||
}
|
||||
|
||||
enum ComposerDraftServiceError: Error {
|
||||
case failedToLoadDraft
|
||||
case failedToLoadReply
|
||||
case failedToSaveDraft
|
||||
case failedToClearDraft
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol ComposerDraftServiceProtocol {
|
||||
func saveDraft(_ draft: ComposerDraftProxy) async -> Result<Void, ComposerDraftServiceError>
|
||||
func loadDraft() async -> Result<ComposerDraftProxy?, ComposerDraftServiceError>
|
||||
func clearDraft() async -> Result<Void, ComposerDraftServiceError>
|
||||
func getReply(eventID: String) async -> Result<TimelineItemReply, ComposerDraftServiceError>
|
||||
}
|
||||
@@ -565,6 +565,37 @@ class RoomProxy: RoomProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Drafts
|
||||
|
||||
func saveDraft(_ draft: ComposerDraft) async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await room.saveComposerDraft(draft: draft)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed saving draft with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func loadDraft() async -> Result<ComposerDraft?, RoomProxyError> {
|
||||
do {
|
||||
return try await .success(room.loadComposerDraft())
|
||||
} catch {
|
||||
MXLog.error("Failed restoring draft with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func clearDraft() async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await room.clearComposerDraft()
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed clearing draft with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
@@ -126,7 +126,6 @@ protocol RoomProxyProtocol {
|
||||
// MARK: - Element Call
|
||||
|
||||
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError>
|
||||
|
||||
func elementCallWidgetDriver() -> ElementCallWidgetDriverProtocol
|
||||
|
||||
func sendCallNotificationIfNeeeded() async -> Result<Void, RoomProxyError>
|
||||
@@ -134,8 +133,13 @@ protocol RoomProxyProtocol {
|
||||
// MARK: - Permalinks
|
||||
|
||||
func matrixToPermalink() async -> Result<URL, RoomProxyError>
|
||||
|
||||
func matrixToEventPermalink(_ eventID: String) async -> Result<URL, RoomProxyError>
|
||||
|
||||
// MARK: - Drafts
|
||||
|
||||
func saveDraft(_ draft: ComposerDraft) async -> Result<Void, RoomProxyError>
|
||||
func loadDraft() async -> Result<ComposerDraft?, RoomProxyError>
|
||||
func clearDraft() async -> Result<Void, RoomProxyError>
|
||||
}
|
||||
|
||||
extension RoomProxyProtocol {
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TimelineItemReply {
|
||||
let details: TimelineItemReplyDetails
|
||||
let isThreaded: Bool
|
||||
}
|
||||
|
||||
enum TimelineItemReplyDetails: Hashable {
|
||||
case notLoaded(eventID: String)
|
||||
case loading(eventID: String)
|
||||
|
||||
@@ -216,7 +216,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildTextTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -236,7 +236,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildImageTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -256,7 +256,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildVideoTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -276,7 +276,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildAudioTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -296,7 +296,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildAudioTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -316,7 +316,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildFileTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -336,7 +336,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildNoticeTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -356,7 +356,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -376,7 +376,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
isThreaded: isThreaded,
|
||||
sender: eventItemProxy.sender,
|
||||
content: buildLocationTimelineItemContent(messageContent),
|
||||
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
|
||||
replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageTimelineItem.inReplyTo()),
|
||||
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
@@ -645,14 +645,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
// MARK: - Reply details
|
||||
|
||||
private func buildReplyToDetailsFrom(details: InReplyToDetails?) -> TimelineItemReplyDetails? {
|
||||
guard let details else { return nil }
|
||||
|
||||
func buildReply(details: InReplyToDetails) -> TimelineItemReply {
|
||||
let isThreaded = details.event.isThreaded
|
||||
switch details.event {
|
||||
case .unavailable:
|
||||
return .notLoaded(eventID: details.eventId)
|
||||
return .init(details: .notLoaded(eventID: details.eventId), isThreaded: isThreaded)
|
||||
case .pending:
|
||||
return .loading(eventID: details.eventId)
|
||||
return .init(details: .loading(eventID: details.eventId), isThreaded: isThreaded)
|
||||
case let .ready(timelineItem, senderID, senderProfile):
|
||||
let sender: TimelineItemSender
|
||||
switch senderProfile {
|
||||
@@ -672,7 +671,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
switch timelineItem.kind() {
|
||||
case .message:
|
||||
return timelineItemReplyDetails(sender: sender, eventID: details.eventId, messageType: timelineItem.asMessage()?.msgtype())
|
||||
return .init(details: timelineItemReplyDetails(sender: sender, eventID: details.eventId, messageType: timelineItem.asMessage()?.msgtype()), isThreaded: isThreaded)
|
||||
case .poll(let question, _, _, _, _, _, _):
|
||||
replyContent = .poll(question: question)
|
||||
case .sticker(let body, _, _):
|
||||
@@ -683,12 +682,20 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
replyContent = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
|
||||
}
|
||||
|
||||
return .loaded(sender: sender, eventID: details.eventId, eventContent: replyContent)
|
||||
return .init(details: .loaded(sender: sender, eventID: details.eventId, eventContent: replyContent), isThreaded: isThreaded)
|
||||
case let .error(message):
|
||||
return .error(eventID: details.eventId, message: message)
|
||||
return .init(details: .error(eventID: details.eventId, message: message), isThreaded: isThreaded)
|
||||
}
|
||||
}
|
||||
|
||||
private func buildReplyToDetailsFromDetailsIfAvailable(details: InReplyToDetails?) -> TimelineItemReplyDetails? {
|
||||
guard let details else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return buildReply(details: details).details
|
||||
}
|
||||
|
||||
private func timelineItemReplyDetails(sender: TimelineItemSender, eventID: String, messageType: MessageType?) -> TimelineItemReplyDetails {
|
||||
let replyContent: EventBasedMessageTimelineItemContentType
|
||||
|
||||
@@ -733,3 +740,14 @@ extension Poll.Kind {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension RepliedToEventDetails {
|
||||
var isThreaded: Bool {
|
||||
switch self {
|
||||
case .ready(let content, _, _):
|
||||
return content.asMessage()?.isThreaded() ?? false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
@MainActor
|
||||
protocol RoomTimelineItemFactoryProtocol {
|
||||
func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol?
|
||||
func buildReply(details: InReplyToDetails) -> TimelineItemReply
|
||||
}
|
||||
|
||||
@@ -172,6 +172,15 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func getLoadedReplyDetails(eventID: String) async -> Result<InReplyToDetails, TimelineProxyError> {
|
||||
do {
|
||||
return try await .success(timeline.loadReplyDetails(eventIdStr: eventID))
|
||||
} catch {
|
||||
MXLog.error("Failed getting reply details for event \(eventID) with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sending
|
||||
|
||||
func sendAudio(url: URL,
|
||||
|
||||
@@ -107,6 +107,8 @@ protocol TimelineProxyProtocol {
|
||||
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineProxyError>
|
||||
|
||||
func getLoadedReplyDetails(eventID: String) async -> Result<InReplyToDetails, TimelineProxyError>
|
||||
}
|
||||
|
||||
extension TimelineProxyProtocol {
|
||||
|
||||
Reference in New Issue
Block a user