Read Receipts sheet + enabled RR by default (#2123)

This commit is contained in:
Mauro
2023-11-21 13:38:39 +01:00
committed by GitHub
parent 053e134a54
commit 7b812a45c3
21 changed files with 286 additions and 29 deletions
@@ -96,6 +96,8 @@ enum RoomScreenViewAction {
case retrySend(itemID: TimelineItemIdentifier)
case cancelSend(itemID: TimelineItemIdentifier)
case showReadReceipts(itemID: TimelineItemIdentifier)
case scrolledToBottom
case poll(RoomScreenViewPollAction)
@@ -160,6 +162,8 @@ struct RoomScreenViewStateBindings {
var sendFailedConfirmationDialogInfo: SendFailedConfirmationDialogInfo?
var reactionSummaryInfo: ReactionSummaryInfo?
var readReceiptsSummaryInfo: ReadReceiptSummaryInfo?
}
struct TimelineItemActionMenuInfo: Equatable, Identifiable {
@@ -189,6 +193,11 @@ struct ReactionSummaryInfo: Identifiable {
}
}
struct ReadReceiptSummaryInfo: Identifiable {
let orderedReceipts: [ReadReceipt]
let id: TimelineItemIdentifier
}
enum RoomScreenErrorType: Hashable {
/// A specific error message shown in an alert.
case alert(String)
@@ -166,6 +166,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
processAudioAction(audioAction)
case .presentCall:
actionsSubject.send(.displayCallScreen)
case .showReadReceipts(itemID: let itemID):
showReadReceipts(for: itemID)
}
}
@@ -655,6 +657,17 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.bindings.reactionSummaryInfo = .init(reactions: eventTimelineItem.properties.reactions, selectedKey: selectedKey)
}
// MARK: - Read Receipts
private func showReadReceipts(for itemID: TimelineItemIdentifier) {
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
return
}
state.bindings.readReceiptsSummaryInfo = .init(orderedReceipts: eventTimelineItem.properties.orderedReadReceipts, id: eventTimelineItem.id)
}
// MARK: - User Indicators
private func displayError(_ type: RoomScreenErrorType) {
@@ -0,0 +1,87 @@
//
// 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 ReadReceiptCell: View {
let readReceipt: ReadReceipt
let memberState: RoomMemberState?
let imageProvider: ImageProviderProtocol?
private var title: String {
memberState?.displayName ?? readReceipt.userID
}
private var subtitle: String {
guard title != readReceipt.userID else {
return ""
}
return readReceipt.userID
}
var body: some View {
HStack(spacing: 12) {
LoadableAvatarImage(url: memberState?.avatarURL,
name: memberState?.displayName,
contentID: readReceipt.userID,
avatarSize: .user(on: .readReceiptSheet),
imageProvider: imageProvider)
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 12) {
Text(title)
.font(.compound.bodyMDSemibold)
.foregroundColor(.compound.textPrimary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
if let formattedTimestamp = readReceipt.formattedTimestamp {
Text(formattedTimestamp)
.font(.compound.bodyXS)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
}
}
Text(subtitle)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
}
}
.padding(.vertical, 8)
.padding(.horizontal, 16)
}
}
struct ReadReceiptCell_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org",
formattedTimestamp: "10:00"),
memberState: .init(displayName: "Test",
avatarURL: nil),
imageProvider: MockMediaProvider())
.previewDisplayName("No Image")
ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org",
formattedTimestamp: "10:00"),
memberState: .init(displayName: "Test",
avatarURL: URL.documentsDirectory),
imageProvider: MockMediaProvider())
.previewDisplayName("With Image")
ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org",
formattedTimestamp: "10:00"),
memberState: nil,
imageProvider: MockMediaProvider())
.previewDisplayName("Loading Member")
}
}
@@ -0,0 +1,80 @@
//
// 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 ReadReceiptsSummaryView: View {
let orderedReadReceipts: [ReadReceipt]
@EnvironmentObject private var context: RoomScreenViewModel.Context
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(L10n.commonSeenBy)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
.padding(.horizontal, 16)
ScrollView {
LazyVStack(spacing: 0) {
ForEach(orderedReadReceipts) { receipt in
ReadReceiptCell(readReceipt: receipt,
memberState: context.viewState.members[receipt.userID],
imageProvider: context.imageProvider)
}
}
}
}
.padding(.top, 24)
.presentationDetents([.medium, .large])
.presentationBackground(Color.compound.bgCanvasDefault)
.presentationDragIndicator(.visible)
}
}
struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
static let viewModel = {
let members: [RoomMemberProxyMock] = [
.mockAlice,
.mockBob,
.mockCharlie,
.mockDan
]
let roomProxyMock = RoomProxyMock(with: .init(displayName: "Room", members: members))
let mock = RoomScreenViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: UserIndicatorControllerMock(),
application: ApplicationMock(),
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
return mock
}()
static let orderedReadReceipts: [ReadReceipt] = [
.init(userID: "@alice:matrix.org", formattedTimestamp: "10:00"),
.init(userID: "@bob:matrix.org", formattedTimestamp: "9:30"),
.init(userID: "@charlie:matrix.org", formattedTimestamp: "9:00"),
.init(userID: "@dan:matrix.org", formattedTimestamp: "8:30"),
.init(userID: "@loading:matrix.org", formattedTimestamp: "Long time ago")
]
static var previews: some View {
ReadReceiptsSummaryView(orderedReadReceipts: orderedReadReceipts)
.environmentObject(viewModel.context)
}
}
@@ -65,6 +65,10 @@ struct RoomScreen: View {
ReactionsSummaryView(reactions: $0.reactions, members: context.viewState.members, imageProvider: context.imageProvider, selectedReactionKey: $0.selectedKey)
.edgesIgnoringSafeArea([.bottom])
}
.sheet(item: $context.readReceiptsSummaryInfo) {
ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts)
.environmentObject(context)
}
.interactiveQuickLook(item: $context.mediaPreviewItem)
.track(screen: .room)
.onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in
@@ -45,8 +45,12 @@ struct TimelineReadReceiptsView: View {
.foregroundColor(.compound.textPrimary)
}
}
.onTapGesture {
context.send(viewAction: .showReadReceipts(itemID: timelineItem.id))
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityLabel)
.accessibilityHint(L10n.a11yReadReceiptsTapToShowAll)
}
private var remaining: Int {