Skip to content

Commit

Permalink
Read Receipts sheet + enabled RR by default (#2123)
Browse files Browse the repository at this point in the history
  • Loading branch information
Velin92 authored Nov 21, 2023
1 parent 053e134 commit 7b812a4
Show file tree
Hide file tree
Showing 21 changed files with 286 additions and 29 deletions.
48 changes: 28 additions & 20 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"a11y_poll_end" = "Ended poll";
"a11y_read_receipts_multiple" = "Read by %1$@ and %2$@";
"a11y_read_receipts_single" = "Read by %1$@";
"a11y_read_receipts_tap_to_show_all" = "Tap to show all";
"a11y_send_files" = "Send files";
"a11y_show_password" = "Show password";
"a11y_start_call" = "Start a call";
Expand Down
2 changes: 1 addition & 1 deletion ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions ElementX/Sources/Other/AvatarSize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ enum UserAvatarSizeOnScreen {
case memberDetails
case inviteUsers
case readReceipt
case readReceiptSheet
case editUserDetails
case suggestions

var value: CGFloat {
switch self {
case .readReceipt:
return 16
case .readReceiptSheet:
return 32
case .timeline:
return 32
case .home:
Expand Down
9 changes: 9 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ enum RoomScreenViewAction {
case retrySend(itemID: TimelineItemIdentifier)
case cancelSend(itemID: TimelineItemIdentifier)

case showReadReceipts(itemID: TimelineItemIdentifier)

case scrolledToBottom

case poll(RoomScreenViewPollAction)
Expand Down Expand Up @@ -160,6 +162,8 @@ struct RoomScreenViewStateBindings {
var sendFailedConfirmationDialogInfo: SendFailedConfirmationDialogInfo?

var reactionSummaryInfo: ReactionSummaryInfo?

var readReceiptsSummaryInfo: ReadReceiptSummaryInfo?
}

struct TimelineItemActionMenuInfo: Equatable, Identifiable {
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
processAudioAction(audioAction)
case .presentCall:
actionsSubject.send(.displayCallScreen)
case .showReadReceipts(itemID: let itemID):
showReadReceipts(for: itemID)
}
}

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 4 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
36 changes: 34 additions & 2 deletions UnitTests/Sources/RoomScreenViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -556,10 +556,42 @@ class RoomScreenViewModelTests: XCTestCase {

return (viewModel, roomProxy, timelineController, notificationCenter)
}

func testShowReadReceipts() async throws {
let receipts: [ReadReceipt] = [.init(userID: "@alice:matrix.org", formattedTimestamp: "12:00"),
.init(userID: "@charlie:matrix.org", formattedTimestamp: "11:00")]
// Given 3 messages from Bob where the middle message has a reaction.
let message = TextRoomTimelineItem(text: "Test",
sender: "bob",
addReadReceipts: receipts)
let id = message.id

// When showing them in a timeline.
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = [message]
let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "",
members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])),
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: userIndicatorControllerMock,
application: ApplicationMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())

let deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts
}

viewModel.context.send(viewAction: .showReadReceipts(itemID: id))
try await deferred.fulfill()
}
}

private extension TextRoomTimelineItem {
init(text: String, sender: String, addReactions: Bool = false) {
init(text: String, sender: String, addReactions: Bool = false, addReadReceipts: [ReadReceipt] = []) {
let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [ReactionSender(senderID: sender, timestamp: Date())])] : []
self.init(id: .random,
timestamp: "10:47 am",
Expand All @@ -569,7 +601,7 @@ private extension TextRoomTimelineItem {
isThreaded: false,
sender: .init(id: "@\(sender):server.com", displayName: sender),
content: .init(body: text),
properties: RoomTimelineItemProperties(reactions: reactions))
properties: RoomTimelineItemProperties(reactions: reactions, orderedReadReceipts: addReadReceipts))
}
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions UnitTests/__Snapshots__/PreviewTests/test_roomScreen.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions UnitTests/__Snapshots__/PreviewTests/test_timelineView.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions changelog.d/1053.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tapping on read receipts will open a detailed sheet of all the receipts.
1 change: 1 addition & 0 deletions changelog.d/pr-2123.change
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Read Receipts are enabled by default.

0 comments on commit 7b812a4

Please sign in to comment.