mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Read Receipts sheet + enabled RR by default (#2123)
This commit is contained in:
@@ -266,7 +266,7 @@ final class AppSettings {
|
||||
@UserPreference(key: UserDefaultsKeys.userSuggestionsEnabled, defaultValue: false, storageType: .volatile)
|
||||
var userSuggestionsEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.readReceiptsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
@UserPreference(key: UserDefaultsKeys.readReceiptsEnabled, defaultValue: true, storageType: .userDefaults(store))
|
||||
var readReceiptsEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.swiftUITimelineEnabled, defaultValue: false, storageType: .volatile)
|
||||
|
||||
@@ -44,6 +44,8 @@ public enum L10n {
|
||||
public static func a11yReadReceiptsSingle(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "a11y_read_receipts_single", String(describing: p1))
|
||||
}
|
||||
/// Tap to show all
|
||||
public static var a11yReadReceiptsTapToShowAll: String { return L10n.tr("Localizable", "a11y_read_receipts_tap_to_show_all") }
|
||||
/// Send files
|
||||
public static var a11ySendFiles: String { return L10n.tr("Localizable", "a11y_send_files") }
|
||||
/// Show password
|
||||
|
||||
@@ -50,6 +50,7 @@ enum UserAvatarSizeOnScreen {
|
||||
case memberDetails
|
||||
case inviteUsers
|
||||
case readReceipt
|
||||
case readReceiptSheet
|
||||
case editUserDetails
|
||||
case suggestions
|
||||
|
||||
@@ -57,6 +58,8 @@ enum UserAvatarSizeOnScreen {
|
||||
switch self {
|
||||
case .readReceipt:
|
||||
return 16
|
||||
case .readReceiptSheet:
|
||||
return 32
|
||||
case .timeline:
|
||||
return 32
|
||||
case .home:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user