Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read Receipts sheet + enabled RR by default #2123

Merged
merged 8 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

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
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)
Velin92 marked this conversation as resolved.
Show resolved Hide resolved
}

// 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: 0) {
Text(title)
.font(.compound.bodyMDSemibold)
.foregroundColor(.compound.textPrimary)
.lineLimit(1)
Spacer()
Velin92 marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
}
Velin92 marked this conversation as resolved.
Show resolved Hide resolved
}
.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,6 +45,9 @@ struct TimelineReadReceiptsView: View {
.foregroundColor(.compound.textPrimary)
}
}
.onTapGesture {
context.send(viewAction: .showReadReceipts(itemID: timelineItem.id))
}
Velin92 marked this conversation as resolved.
Show resolved Hide resolved
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityLabel)
}
Expand Down
38 changes: 35 additions & 3 deletions UnitTests/Sources/RoomScreenViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ class RoomScreenViewModelTests: XCTestCase {
// Then the second call should be ignored.
XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 1)
}

Velin92 marked this conversation as resolved.
Show resolved Hide resolved
// swiftlint:enable force_unwrapping
// swiftlint:disable:next large_tuple
private func readReceiptsConfiguration(with items: [RoomTimelineItemProtocol]) -> (RoomScreenViewModel,
Expand Down 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.
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.