Retry send and cancel send for failed to send messages (#1088)

* is now possible to open the confirmation dialog and pass the transaction id

* task completed added some tests

* changelog

* title visibility set to .visible
This commit is contained in:
Mauro
2023-06-16 11:00:54 +02:00
committed by GitHub
parent 163e38b1a9
commit ac41704821
17 changed files with 281 additions and 23 deletions
+6
View File
@@ -702,6 +702,12 @@ public enum L10n {
public static var screenRoomMemberListPendingHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_pending_header_title") }
/// Room members
public static var screenRoomMemberListRoomMembersHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_room_members_header_title") }
/// Remove
public static var screenRoomRetrySendMenuRemoveAction: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_remove_action") }
/// Send again
public static var screenRoomRetrySendMenuSendAgainAction: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_send_again_action") }
/// Your message failed to send
public static var screenRoomRetrySendMenuTitle: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_title") }
/// Create a new conversation or room
public static var screenRoomlistA11yCreateMessage: String { return L10n.tr("Localizable", "screen_roomlist_a11y_create_message") }
/// All Chats
@@ -685,6 +685,38 @@ class RoomProxyMock: RoomProxyProtocol {
return sendFileUrlFileInfoReturnValue
}
}
//MARK: - retrySend
var retrySendTransactionIDCallsCount = 0
var retrySendTransactionIDCalled: Bool {
return retrySendTransactionIDCallsCount > 0
}
var retrySendTransactionIDReceivedTransactionID: String?
var retrySendTransactionIDReceivedInvocations: [String] = []
var retrySendTransactionIDClosure: ((String) async -> Void)?
func retrySend(transactionID: String) async {
retrySendTransactionIDCallsCount += 1
retrySendTransactionIDReceivedTransactionID = transactionID
retrySendTransactionIDReceivedInvocations.append(transactionID)
await retrySendTransactionIDClosure?(transactionID)
}
//MARK: - cancelSend
var cancelSendTransactionIDCallsCount = 0
var cancelSendTransactionIDCalled: Bool {
return cancelSendTransactionIDCallsCount > 0
}
var cancelSendTransactionIDReceivedTransactionID: String?
var cancelSendTransactionIDReceivedInvocations: [String] = []
var cancelSendTransactionIDClosure: ((String) async -> Void)?
func cancelSend(transactionID: String) async {
cancelSendTransactionIDCallsCount += 1
cancelSendTransactionIDReceivedTransactionID = transactionID
cancelSendTransactionIDReceivedInvocations.append(transactionID)
await cancelSendTransactionIDClosure?(transactionID)
}
//MARK: - editMessage
var editMessageOriginalCallsCount = 0
@@ -0,0 +1,50 @@
//
// 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 SwiftUI
protocol ConfirmationDialogProtocol {
var title: String { get }
}
extension View {
func confirmationDialog<Item, Actions>(item: Binding<Item?>,
titleVisibility: Visibility = .automatic,
@ViewBuilder actions: (Item) -> Actions) -> some View where Item: ConfirmationDialogProtocol, Actions: View {
let binding = Binding<Bool>(get: {
item.wrappedValue != nil
}, set: { newValue in
if !newValue {
item.wrappedValue = nil
}
})
return confirmationDialog(item.wrappedValue?.title ?? "", isPresented: binding, titleVisibility: titleVisibility, presenting: item.wrappedValue, actions: actions)
}
func confirmationDialog<Item, Actions, Message>(item: Binding<Item?>,
titleVisibility: Visibility = .automatic,
@ViewBuilder actions: (Item) -> Actions,
@ViewBuilder message: (Item) -> Message) -> some View where Item: ConfirmationDialogProtocol, Actions: View, Message: View {
let binding = Binding<Bool>(get: {
item.wrappedValue != nil
}, set: { newValue in
if !newValue {
item.wrappedValue = nil
}
})
return confirmationDialog(item.wrappedValue?.title ?? "", isPresented: binding, titleVisibility: titleVisibility, presenting: item.wrappedValue, actions: actions, message: message)
}
}
@@ -70,6 +70,9 @@ enum RoomScreenViewAction {
case handlePasteOrDrop(provider: NSItemProvider)
case tappedOnUser(userID: String)
case retrySend(transactionID: String?)
case cancelSend(transactionID: String?)
}
struct RoomScreenViewState: BindableState {
@@ -109,6 +112,8 @@ struct RoomScreenViewStateBindings {
var debugInfo: TimelineItemDebugInfo?
var actionMenuInfo: TimelineItemActionMenuInfo?
var sendFailedConfirmationDialogInfo: SendFailedConfirmationDialogInfo?
}
struct TimelineItemActionMenuInfo: Identifiable {
@@ -119,6 +124,12 @@ struct TimelineItemActionMenuInfo: Identifiable {
}
}
struct SendFailedConfirmationDialogInfo: ConfirmationDialogProtocol {
let title = L10n.screenRoomRetrySendMenuTitle
let transactionID: String?
}
enum RoomScreenErrorType: Hashable {
/// A specific error message shown in an alert.
case alert(String)
@@ -105,10 +105,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
handlePasteOrDrop(provider)
case .tappedOnUser(userID: let userID):
Task { await handleTappedUser(userID: userID) }
case .displayEmojiPicker(let itemID):
guard let item = state.items.first(where: { $0.id == itemID }), item.isReactable else { return }
callback?(.displayEmojiPicker(itemID: itemID))
case .retrySend(let transactionID):
Task { await handleRetrySend(transactionID: transactionID) }
case .cancelSend(let transactionID):
Task { await handleCancelSend(transactionID: transactionID) }
}
}
@@ -470,6 +473,22 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
}
private func handleRetrySend(transactionID: String?) async {
guard let transactionID else {
return
}
await roomProxy.retrySend(transactionID: transactionID)
}
private func handleCancelSend(transactionID: String?) async {
guard let transactionID else {
return
}
await roomProxy.cancelSend(transactionID: transactionID)
}
private static let loadingIndicatorIdentifier = "RoomScreenLoadingIndicator"
private func showLoadingIndicator(with delay: Duration) -> Task<Void, Never> {
@@ -62,6 +62,14 @@ struct RoomScreen: View {
context.send(viewAction: .handlePasteOrDrop(provider: provider))
return true
}
.confirmationDialog(item: $context.sendFailedConfirmationDialogInfo, titleVisibility: .visible) { item in
Button(L10n.screenRoomRetrySendMenuSendAgainAction) {
context.send(viewAction: .retrySend(transactionID: item.transactionID))
}
Button(L10n.screenRoomRetrySendMenuRemoveAction, role: .destructive) {
context.send(viewAction: .cancelSend(transactionID: item.transactionID))
}
}
}
private var timeline: some View {
@@ -146,30 +146,47 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
@ViewBuilder
var contentWithTimestamp: some View {
if isTextItem {
if isTextItem || isMediaType {
ZStack(alignment: .bottomTrailing) {
contentWithReply
localizedSendInfo
}
} else if isMediaType {
ZStack(alignment: .bottomTrailing) {
contentWithReply
localizedSendInfo
.padding(.horizontal, 4)
.padding(.vertical, 2)
.background(Color.element.system)
.cornerRadius(10)
.padding(.trailing, 4)
.padding(.bottom, 4)
interactiveLocalizedSendInfo
}
} else {
HStack(alignment: .bottom, spacing: 4) {
contentWithReply
localizedSendInfo
interactiveLocalizedSendInfo
}
}
}
@ViewBuilder
var interactiveLocalizedSendInfo: some View {
if timelineItem.properties.deliveryStatus == .sendingFailed {
backgroundedLocalizedSendInfo
.onTapGesture {
context.sendFailedConfirmationDialogInfo = .init(transactionID: timelineItem.properties.transactionID)
}
} else {
backgroundedLocalizedSendInfo
}
}
@ViewBuilder
var backgroundedLocalizedSendInfo: some View {
if isMediaType {
localizedSendInfo
.padding(.horizontal, 4)
.padding(.vertical, 2)
.background(Color.element.system)
.cornerRadius(10)
.padding(.trailing, 4)
.padding(.bottom, 4)
} else {
localizedSendInfo
}
}
@ViewBuilder
var localizedSendInfo: some View {
HStack(spacing: 4) {
@@ -69,6 +69,9 @@ struct TimelineItemStatusView: View {
.resizable()
.foregroundColor(.element.alert)
.frame(width: 16, height: 16)
.onTapGesture {
context.sendFailedConfirmationDialogInfo = .init(transactionID: timelineItem.properties.transactionID)
}
}
// The bubbles handle the failure internally
}
@@ -326,6 +326,28 @@ class RoomProxy: RoomProxyProtocol {
}
}
func retrySend(transactionID: String) async {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
return await Task.dispatch(on: userInitiatedDispatchQueue) {
self.room.retrySend(txnId: transactionID)
}
}
func cancelSend(transactionID: String) async {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
return await Task.dispatch(on: userInitiatedDispatchQueue) {
self.room.cancelSend(txnId: transactionID)
}
}
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
@@ -101,6 +101,12 @@ protocol RoomProxyProtocol {
func sendFile(url: URL, fileInfo: FileInfo) async -> Result<Void, RoomProxyError>
/// Retries sending a failed message given its transaction ID
func retrySend(transactionID: String) async
/// Cancels a failed message given its transaction ID from the timeline
func cancelSend(transactionID: String) async
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError>
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
@@ -26,4 +26,6 @@ struct RoomTimelineItemProperties: Hashable {
var deliveryStatus: TimelineItemDeliveryStatus?
/// The read receipts of the item, ordered from newest to oldest
var orderedReadReceipts: [ReadReceipt] = []
/// The original transaction id transmitted by the client
var transactionID: String?
}
@@ -52,6 +52,10 @@ struct EventTimelineItemProxy {
var id: String {
item.uniqueIdentifier()
}
var transactionID: String? {
item.transactionId()
}
var deliveryStatus: TimelineItemDeliveryStatus? {
guard let localSendState = item.localSendState() else {
@@ -133,7 +133,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
blurhash: imageInfo.blurhash,
properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
}
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
@@ -184,7 +185,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
}
private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -201,7 +203,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
}
private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -218,7 +221,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
}
private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -235,7 +239,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
}
private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -252,7 +257,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
}
private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -269,7 +275,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
}
private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -286,7 +293,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
transactionID: eventItemProxy.transactionID))
}
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {