diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index f9e1fffec..f66d40726 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -142,6 +142,7 @@ 5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */; }; 5D7960B32C350FA93F48D02B /* OnboardingModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */; }; 5D9F0695DC6C0057F85C12B6 /* UserNotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */; }; + 5DA73F4329252C3A00651C22 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA73F4229252C3A00651C22 /* TimelineDeliveryStatusView.swift */; }; 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; }; 5E540CAEF764D7FBD8D80776 /* VideoPlayerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3FC45B7643298BF361CEB1 /* VideoPlayerModels.swift */; }; 5F1FDE49DFD0C680386E48F9 /* TemplateViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */; }; @@ -581,6 +582,7 @@ 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelProtocol.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = ""; }; + 5DA73F4229252C3A00651C22 /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -651,12 +653,12 @@ 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = ""; }; 8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = ""; }; - 8B9A55AC2FB0FE0AEAA3DF1F /* LICENSE */ = {isa = PBXFileReference; path = LICENSE; sourceTree = ""; }; + 8B9A55AC2FB0FE0AEAA3DF1F /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = ""; }; 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerProtocol.swift; sourceTree = ""; }; 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = ""; }; 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; @@ -1830,6 +1832,7 @@ CCD48459CA34A1928EC7A26A /* Supplementary */ = { isa = PBXGroup; children = ( + 5DA73F4229252C3A00651C22 /* TimelineDeliveryStatusView.swift */, 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */, ); path = Supplementary; @@ -2535,6 +2538,7 @@ 485A7A97076C7D19104BDC1D /* MediaPlayerModels.swift in Sources */, 8D332A24CD23B4216E33EC5C /* MediaPlayerScreen.swift in Sources */, 46F8817A235DC41228128BE7 /* MediaPlayerViewModel.swift in Sources */, + 5DA73F4329252C3A00651C22 /* TimelineDeliveryStatusView.swift in Sources */, 80997E933A5B2C0868D80B45 /* MediaPlayerViewModelProtocol.swift in Sources */, EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */, 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index a9b607029..edff8f750 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -37,6 +37,10 @@ struct TimelineItemBubbledStylerView: View { HStack { Spacer() styledContentWithReactions + if timelineItem.isOutgoing { + TimelineDeliveryStatusView(deliveryStatus: timelineItem.properties.deliveryStatus) + .padding(.top, 6) + } } .padding(.trailing, 16) .padding(.leading, 16) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineDeliveryStatusView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineDeliveryStatusView.swift new file mode 100644 index 000000000..6d76a6d40 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineDeliveryStatusView.swift @@ -0,0 +1,67 @@ +// +// Copyright 2022 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 +struct TimelineDeliveryStatusView: View { + let deliveryStatus: MessageTimelineItemDeliveryStatus + + @State var showDeliveryStatus: Bool + + private var systemImageName: String { + switch deliveryStatus { + case .sending, .unknown: + return "circle" + case .sent: + return "checkmark.circle" + } + } + + init(deliveryStatus: MessageTimelineItemDeliveryStatus) { + self.deliveryStatus = deliveryStatus + + switch deliveryStatus { + case .sending: + showDeliveryStatus = true + case let .sent(elapsedTime: elapsedTime): + showDeliveryStatus = elapsedTime < 3 + case .unknown: + showDeliveryStatus = false + } + } + + var body: some View { + if showDeliveryStatus { + Image(systemName: systemImageName) + .task { + if case .sent = deliveryStatus { + try? await Task.sleep(nanoseconds: 1_000_000_000) + withAnimation { + showDeliveryStatus = false + } + } + } + } + } +} + +struct TimelineDeliveryStatusView_Previews: PreviewProvider { + static var previews: some View { + VStack { + TimelineDeliveryStatusView(deliveryStatus: .sending) + TimelineDeliveryStatusView(deliveryStatus: .sent(elapsedTime: Date().timeIntervalSince1970)) + } + } +} diff --git a/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift index 3e627a66b..b7a58e7a2 100644 --- a/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift @@ -23,6 +23,13 @@ protocol MessageContentProtocol: RoomMessageEventContentProtocol { var body: String { get } } +/// The delivery status for the item. +enum MessageTimelineItemDeliveryStatus: Equatable { + case unknown + case sending + case sent(elapsedTime: TimeInterval) +} + /// A timeline item that represents an `m.room.message` event. struct MessageTimelineItem { let item: MatrixRustSDK.EventTimelineItem @@ -37,6 +44,15 @@ struct MessageTimelineItem { return eventID } } + + var deliveryStatus: MessageTimelineItemDeliveryStatus { + switch item.key() { + case .transactionId: + return .sending + case .eventId: + return .sent(elapsedTime: Date().timeIntervalSince1970 - originServerTs.timeIntervalSince1970) + } + } var body: String { content.body diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/RoomTimelineItemProperties.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/RoomTimelineItemProperties.swift index d4cf07a52..f7a5092bd 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/RoomTimelineItemProperties.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/RoomTimelineItemProperties.swift @@ -22,4 +22,6 @@ struct RoomTimelineItemProperties: Equatable { var isEdited = false /// The aggregated reactions that have been sent for this item. var reactions: [AggregatedReaction] = [] + /// The delivery status for this item. + var deliveryStatus: MessageTimelineItemDeliveryStatus = .unknown } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index e432bdda3..9af3aa977 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -166,7 +166,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { senderDisplayName: displayName, senderAvatar: avatarImage, properties: RoomTimelineItemProperties(isEdited: message.isEdited, - reactions: aggregateReactions(message.reactions))) + reactions: aggregateReactions(message.reactions), + deliveryStatus: message.deliveryStatus)) } private func buildImageTimelineItemFromMessage(_ message: MessageTimelineItem, @@ -196,7 +197,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { aspectRatio: aspectRatio, blurhash: message.blurhash, properties: RoomTimelineItemProperties(isEdited: message.isEdited, - reactions: aggregateReactions(message.reactions))) + reactions: aggregateReactions(message.reactions), + deliveryStatus: message.deliveryStatus)) } private func buildVideoTimelineItemFromMessage(_ message: MessageTimelineItem, @@ -228,7 +230,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { aspectRatio: aspectRatio, blurhash: message.blurhash, properties: RoomTimelineItemProperties(isEdited: message.isEdited, - reactions: aggregateReactions(message.reactions))) + reactions: aggregateReactions(message.reactions), + deliveryStatus: message.deliveryStatus)) } private func buildFileTimelineItemFromMessage(_ message: MessageTimelineItem, @@ -248,7 +251,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { source: message.source, thumbnailSource: message.thumbnailSource, properties: RoomTimelineItemProperties(isEdited: message.isEdited, - reactions: aggregateReactions(message.reactions))) + reactions: aggregateReactions(message.reactions), + deliveryStatus: message.deliveryStatus)) } private func buildNoticeTimelineItemFromMessage(_ message: MessageTimelineItem, @@ -270,7 +274,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { senderDisplayName: displayName, senderAvatar: avatarImage, properties: RoomTimelineItemProperties(isEdited: message.isEdited, - reactions: aggregateReactions(message.reactions))) + reactions: aggregateReactions(message.reactions), + deliveryStatus: message.deliveryStatus)) } private func buildEmoteTimelineItemFromMessage(_ message: MessageTimelineItem, @@ -292,7 +297,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { senderDisplayName: displayName, senderAvatar: avatarImage, properties: RoomTimelineItemProperties(isEdited: message.isEdited, - reactions: aggregateReactions(message.reactions))) + reactions: aggregateReactions(message.reactions), + deliveryStatus: message.deliveryStatus)) } private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] { diff --git a/changelog.d/27.feature b/changelog.d/27.feature new file mode 100644 index 000000000..61fc9e0f6 --- /dev/null +++ b/changelog.d/27.feature @@ -0,0 +1 @@ +Timeline: Sending and sent state for timeline messages.