mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
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:
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
+24
-14
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user