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
@@ -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 {