Read Receipts display with the user avatar (#1038)

* added the read receipt into the code

* fixed some things lost during merge

* implemented the status view to be always at the trailing side

* preview code for better testing

* removed timelineStyle modifier

* removed also the group style

* added some UI tests for the read receipts
This commit is contained in:
Mauro
2023-06-08 12:05:56 +02:00
committed by GitHub
parent c7ee69d75d
commit 75a3751f78
38 changed files with 360 additions and 65 deletions
@@ -52,7 +52,8 @@ enum RoomTimelineItemFixtures {
isOutgoing: false,
isEditable: false,
sender: .init(id: "", displayName: "Helena"),
content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!")),
content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"),
properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: UUID().uuidString,
timestamp: "5 PM",
isOutgoing: true,
@@ -68,7 +69,11 @@ enum RoomTimelineItemFixtures {
properties: RoomTimelineItemProperties(reactions: [
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
])),
],
orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil),
ReadReceipt(userID: "bob", formattedTimestamp: nil),
ReadReceipt(userID: "charlie", formattedTimestamp: nil),
ReadReceipt(userID: "dan", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: UUID().uuidString,
timestamp: "5 PM",
isOutgoing: false,
@@ -85,6 +90,28 @@ enum RoomTimelineItemFixtures {
TextRoomTimelineItem(text: "How are you?",
senderDisplayName: "Alice")]
}
/// A small chunk of events, containing 2 text items.
static var smallChunkWithReadReceipts: [RoomTimelineItemProtocol] {
[TextRoomTimelineItem(text: "Hey there 👋",
senderDisplayName: "Alice")
.withReadReceipts([ReadReceipt(userID: "a1", formattedTimestamp: nil)]),
TextRoomTimelineItem(text: "How are you?",
senderDisplayName: "Alice")
.withReadReceipts([ReadReceipt(userID: "a2", formattedTimestamp: nil),
ReadReceipt(userID: "b2", formattedTimestamp: nil)]),
TextRoomTimelineItem(text: "Fine, Thanks!",
senderDisplayName: "Bob")
.withReadReceipts([ReadReceipt(userID: "a3", formattedTimestamp: nil),
ReadReceipt(userID: "b3", formattedTimestamp: nil),
ReadReceipt(userID: "c3", formattedTimestamp: nil)]),
TextRoomTimelineItem(text: "What about you?",
senderDisplayName: "Bob")
.withReadReceipts([ReadReceipt(userID: "a4", formattedTimestamp: nil),
ReadReceipt(userID: "b4", formattedTimestamp: nil),
ReadReceipt(userID: "c4", formattedTimestamp: nil),
ReadReceipt(userID: "d4", formattedTimestamp: nil)])]
}
/// A chunk of events that contains a single text item.
static var singleMessageChunk: [RoomTimelineItemProtocol] {
@@ -194,3 +221,11 @@ private extension TextRoomTimelineItem {
content: .init(body: text))
}
}
private extension TextRoomTimelineItem {
func withReadReceipts(_ receipts: [ReadReceipt]) -> TextRoomTimelineItem {
var newSelf = self
newSelf.properties.orderedReadReceipts = receipts
return newSelf
}
}
@@ -0,0 +1,26 @@
//
// 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 Foundation
struct ReadReceipt: Hashable {
let userID: String
let formattedTimestamp: String?
}
extension ReadReceipt: Identifiable {
var id: String { userID }
}
@@ -24,4 +24,6 @@ struct RoomTimelineItemProperties: Hashable {
var reactions: [AggregatedReaction] = []
/// The delivery status for this item. If a sent message is echoed the value is nil.
var deliveryStatus: TimelineItemDeliveryStatus?
/// The read receipts of the item, ordered from newest to oldest
var orderedReadReceipts: [ReadReceipt] = []
}
@@ -132,7 +132,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
aspectRatio: aspectRatio,
blurhash: imageInfo.blurhash,
properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
@@ -182,7 +183,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -198,7 +200,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -214,7 +217,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -230,7 +234,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -246,7 +251,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -262,7 +268,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -278,7 +285,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()),
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
}
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
@@ -287,6 +295,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
return AggregatedReaction(key: reaction.key, count: Int(reaction.count), isHighlighted: isHighlighted)
}
}
private func orderReadReceipts(_ receipts: [String: Receipt]) -> [ReadReceipt] {
receipts
.sorted { firstElement, secondElement in
// If there is no timestamp we order them as last
let firstTimestamp = firstElement.value.dateTimestamp ?? Date(timeIntervalSince1970: 0)
let secondTimestamp = secondElement.value.dateTimestamp ?? Date(timeIntervalSince1970: 0)
return firstTimestamp > secondTimestamp
}
.map { key, receipt in
ReadReceipt(userID: key, formattedTimestamp: receipt.dateTimestamp?.formatted(date: .omitted, time: .shortened))
}
}
// MARK: - Message events content
@@ -139,7 +139,7 @@ enum RoomTimelineViewProvider: Identifiable, Hashable {
extension RoomTimelineViewProvider: View {
var body: some View {
timelineView
.timelineGroupStyle(timelineGroupStyle)
.environment(\.timelineGroupStyle, timelineGroupStyle)
}
@ViewBuilder private var timelineView: some View {