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
+3
View File
@@ -49,9 +49,12 @@ enum UserAvatarSizeOnScreen {
case startChat
case memberDetails
case inviteUsers
case readReceipt
var value: CGFloat {
switch self {
case .readReceipt:
return 16
case .timeline:
return 32
case .home:
@@ -77,10 +77,12 @@ struct RoomScreenViewState: BindableState {
var roomTitle = ""
var roomAvatarURL: URL?
var items: [RoomTimelineViewProvider] = []
var members: [String: RoomMemberState] = [:]
var canBackPaginate = true
var isBackPaginating = false
var showLoading = false
var timelineStyle: TimelineStyle
var readReceiptsEnabled: Bool
var bindings: RoomScreenViewStateBindings
@@ -123,3 +125,8 @@ enum RoomScreenErrorType: Hashable {
/// A specific error message shown in a toast.
case toast(String)
}
struct RoomMemberState {
let displayName: String?
let avatarURL: URL?
}
@@ -44,6 +44,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
roomTitle: roomProxy.roomTitle,
roomAvatarURL: roomProxy.avatarURL,
timelineStyle: ServiceLocator.shared.settings.timelineStyle,
readReceiptsEnabled: ServiceLocator.shared.settings.readReceiptsEnabled,
bindings: .init(composerText: "", composerFocused: false)),
imageProvider: mediaProvider)
@@ -88,6 +89,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
ServiceLocator.shared.settings.$timelineStyle
.weakAssign(to: \.state.timelineStyle, on: self)
.store(in: &cancellables)
ServiceLocator.shared.settings.$readReceiptsEnabled
.weakAssign(to: \.state.readReceiptsEnabled, on: self)
.store(in: &cancellables)
roomProxy.membersPublisher
.map { members in
members.reduce(into: [String: RoomMemberState]()) { dictionary, member in
dictionary[member.userID] = RoomMemberState(displayName: member.displayName, avatarURL: member.avatarURL)
}
}
.weakAssign(to: \.state.members, on: self)
.store(in: &cancellables)
buildTimelineViews()
}
@@ -68,7 +68,8 @@ struct RoomScreen: View {
TimelineView()
.id(context.viewState.roomId)
.environmentObject(context)
.timelineStyle(context.viewState.timelineStyle)
.environment(\.timelineStyle, context.viewState.timelineStyle)
.environment(\.readReceiptsEnabled, context.viewState.readReceiptsEnabled)
.overlay(alignment: .bottomTrailing) { scrollToBottomButton }
}
@@ -42,16 +42,28 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
header
.zIndex(1)
}
HStack {
if timelineItem.isOutgoing {
Spacer()
VStack(alignment: alignment, spacing: 0) {
HStack {
if timelineItem.isOutgoing {
Spacer()
}
messageBubbleWithReactions
}
.padding(timelineItem.isOutgoing ? .leading : .trailing, 40) // Extra padding to differentiate alignment.
messageBubbleWithReactions
HStack(spacing: 0) {
if !timelineItem.isOutgoing {
Spacer()
}
TimelineItemStatusView(timelineItem: timelineItem)
.environmentObject(context)
.padding(.top, 8)
.padding(.bottom, 3)
}
}
.padding(.horizontal, 16.0)
.padding(timelineItem.isOutgoing ? .leading : .trailing, 40) // Extra padding to differentiate alignment.
.padding(.horizontal, 16)
}
}
}
@@ -92,11 +104,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
context.send(viewAction: .sendReaction(key: key, eventID: timelineItem.id))
}
}
TimelineReceiptView(timelineItem: timelineItem)
.environmentObject(context)
.padding(.top, 10)
.padding(.bottom, 3)
}
}
@@ -120,7 +127,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
}
.padding(.top, messageBubbleTopPadding)
}
@ViewBuilder
var styledContent: some View {
if shouldAvoidBubbling {
@@ -254,6 +261,9 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
static var previews: some View {
mockTimeline
.previewDisplayName("Mock Timeline")
mockTimeline
.environment(\.readReceiptsEnabled, true)
.previewDisplayName("Mock Timeline with read receipts")
mockTimeline
.environment(\.layoutDirection, .rightToLeft)
.previewDisplayName("Mock Timeline RTL")
@@ -267,7 +277,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
item.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells
}
}
.timelineStyle(.bubbles)
.environment(\.timelineStyle, .bubbles)
.previewLayout(.sizeThatFits)
.environmentObject(viewModel.context)
}
@@ -42,7 +42,7 @@ struct TimelineItemPlainStylerView<Content: View>: View {
supplementaryViews
}
}
TimelineReceiptView(timelineItem: timelineItem)
TimelineItemStatusView(timelineItem: timelineItem)
.environmentObject(context)
}
}
@@ -139,7 +139,7 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider {
.padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells
}
}
.timelineStyle(.plain)
.environment(\.timelineStyle, .plain)
.previewLayout(.sizeThatFits)
.environmentObject(viewModel.context)
}
@@ -58,6 +58,10 @@ private struct TimelineGroupStyleKey: EnvironmentKey {
static let defaultValue = TimelineGroupStyle.single
}
private struct ReadReceiptsEnabledKey: EnvironmentKey {
static let defaultValue = false
}
extension EnvironmentValues {
var timelineStyle: TimelineStyle {
get { self[TimelineStyleKey.self] }
@@ -68,14 +72,9 @@ extension EnvironmentValues {
get { self[TimelineGroupStyleKey.self] }
set { self[TimelineGroupStyleKey.self] = newValue }
}
}
extension View {
func timelineStyle(_ style: TimelineStyle) -> some View {
environment(\.timelineStyle, style)
}
func timelineGroupStyle(_ groupStyle: TimelineGroupStyle) -> some View {
environment(\.timelineGroupStyle, groupStyle)
var readReceiptsEnabled: Bool {
get { self[ReadReceiptsEnabledKey.self] }
set { self[ReadReceiptsEnabledKey.self] = newValue }
}
}
@@ -16,10 +16,10 @@
import SwiftUI
// This is also going to be used for read receipts in the future
struct TimelineReceiptView: View {
struct TimelineItemStatusView: View {
let timelineItem: EventBasedTimelineItemProtocol
@Environment(\.timelineStyle) private var style
@Environment(\.readReceiptsEnabled) private var readReceiptsEnabled
@EnvironmentObject private var context: RoomScreenViewModel.Context
@State private var shouldShowDeliveryStatus = true
@@ -34,7 +34,9 @@ struct TimelineReceiptView: View {
}
var body: some View {
if shouldShowDeliveryStatus {
if !timelineItem.properties.orderedReadReceipts.isEmpty, readReceiptsEnabled {
readReceipts
} else if shouldShowDeliveryStatus {
deliveryStatus
.onChange(of: timelineItem.properties.deliveryStatus) { newValue in
if newValue == .sent, !isLast {
@@ -71,4 +73,9 @@ struct TimelineReceiptView: View {
// The bubbles handle the failure internally
}
}
var readReceipts: some View {
TimelineReadReceiptsView(timelineItem: timelineItem)
.environmentObject(context)
}
}
@@ -0,0 +1,97 @@
//
// 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
struct TimelineReadReceiptsView: View {
let displayNumber = 3
let timelineItem: EventBasedTimelineItemProtocol
@EnvironmentObject private var context: RoomScreenViewModel.Context
var body: some View {
HStack(spacing: 2) {
HStack(spacing: -4) {
let receiptsToDisplay = timelineItem.properties.orderedReadReceipts.prefix(displayNumber)
ForEach(0..<receiptsToDisplay.count, id: \.self) { index in
let receipt = receiptsToDisplay[index]
LoadableAvatarImage(url: context.viewState.members[receipt.userID]?.avatarURL,
name: context.viewState.members[receipt.userID]?.displayName,
contentID: receipt.userID,
avatarSize: .user(on: .readReceipt),
imageProvider: context.imageProvider)
.overlay(
RoundedRectangle(cornerRadius: .infinity)
.stroke(Color.element.background, lineWidth: 1)
)
.zIndex(Double(displayNumber - index))
}
}
if timelineItem.properties.orderedReadReceipts.count > displayNumber {
let remaining = timelineItem.properties.orderedReadReceipts.count - displayNumber
Text("+\(remaining)")
.font(.compound.bodySM)
.foregroundColor(.element.primaryContent)
}
}
}
}
struct TimelineReadReceiptsView_Previews: PreviewProvider {
static let members: [RoomMemberProxyMock] = [
.mockAlice,
.mockBob,
.mockCharlie,
.mockDan,
.mockMe
]
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
roomProxy: RoomProxyMock(with: .init(displayName: "Test",
members: members)))
static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before")]
static let tripleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before"),
ReadReceipt(userID: RoomMemberProxyMock.mockCharlie.userID, formattedTimestamp: "Way before")]
static let quadrupleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before"),
ReadReceipt(userID: RoomMemberProxyMock.mockCharlie.userID, formattedTimestamp: "Way before"),
ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")]
static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: UUID().uuidString,
timestamp: "Now",
isOutgoing: true,
isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"),
properties: .init(orderedReadReceipts: receipts))
}
static var previews: some View {
VStack {
TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: singleReceipt))
.environmentObject(viewModel.context)
TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: doubleReceipt))
.environmentObject(viewModel.context)
TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: tripleReceipt))
.environmentObject(viewModel.context)
TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: quadrupleReceipt))
.environmentObject(viewModel.context)
}
}
}
@@ -38,7 +38,9 @@ struct AudioRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -37,7 +37,9 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -47,7 +47,9 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -38,7 +38,9 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -134,7 +134,7 @@ struct FormattedBodyText_Previews: PreviewProvider {
static var previews: some View {
body
body
.timelineStyle(.plain)
.environment(\.timelineStyle, .plain)
}
@ViewBuilder
@@ -58,7 +58,9 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -48,7 +48,9 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -33,7 +33,8 @@ struct StateRoomTimelineView: View {
struct StateRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body
body.timelineStyle(.plain)
body
.environment(\.timelineStyle, .plain)
}
static var body: some View {
@@ -51,7 +51,9 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -37,7 +37,9 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -43,7 +43,9 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -70,7 +70,9 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
static var previews: some View {
body.environmentObject(viewModel.context)
body.timelineStyle(.plain).environmentObject(viewModel.context)
body
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
}
static var body: some View {
@@ -27,10 +27,12 @@ struct DeveloperOptionsScreenViewState: BindableState {
struct DeveloperOptionsScreenViewStateBindings {
var shouldCollapseRoomStateEvents: Bool
var userSuggestionsEnabled: Bool
var readReceiptsEnabled: Bool
}
enum DeveloperOptionsScreenViewAction {
case changedShouldCollapseRoomStateEvents
case changedUserSuggestionsEnabled
case changedReadReceiptsEnabled
case clearCache
}
@@ -26,7 +26,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
init() {
appSettings = ServiceLocator.shared.settings
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: appSettings.shouldCollapseRoomStateEvents,
userSuggestionsEnabled: appSettings.userSuggestionsEnabled)
userSuggestionsEnabled: appSettings.userSuggestionsEnabled,
readReceiptsEnabled: appSettings.readReceiptsEnabled)
let state = DeveloperOptionsScreenViewState(bindings: bindings)
super.init(initialViewState: state)
@@ -42,6 +43,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
appSettings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
case .changedUserSuggestionsEnabled:
appSettings.userSuggestionsEnabled = state.bindings.userSuggestionsEnabled
case .changedReadReceiptsEnabled:
appSettings.readReceiptsEnabled = state.bindings.readReceiptsEnabled
case .clearCache:
callback?(.clearCache)
}
@@ -36,6 +36,13 @@ struct DeveloperOptionsScreen: View {
.onChange(of: context.userSuggestionsEnabled) { _ in
context.send(viewAction: .changedUserSuggestionsEnabled)
}
Toggle(isOn: $context.readReceiptsEnabled) {
Text("Show read receipts")
}
.onChange(of: context.readReceiptsEnabled) { _ in
context.send(viewAction: .changedReadReceiptsEnabled)
}
}
Section {
@@ -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 {
@@ -197,6 +197,18 @@ class MockScreen: Identifiable {
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomSmallTimelineWithReadReceipts:
ServiceLocator.shared.settings.readReceiptsEnabled = true
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunkWithReadReceipts
let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: .init(displayName: "New room", avatarURL: URL.picturesDirectory)),
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider())
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomSmallTimelineIncomingAndSmallPagination:
let navigationStackCoordinator = NavigationStackCoordinator()
@@ -36,6 +36,7 @@ enum UITestsScreenIdentifier: String {
case roomPlainNoAvatar
case roomEncryptedWithAvatar
case roomSmallTimeline
case roomSmallTimelineWithReadReceipts
case roomSmallTimelineIncomingAndSmallPagination
case roomSmallTimelineLargePagination
case roomLayoutTop