diff --git a/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/Contents.json index beeaf3fb9..6c7aeb055 100644 --- a/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/Contents.json +++ b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" } } diff --git a/ElementX/Sources/Mocks/AggregatedReactionMock.swift b/ElementX/Sources/Mocks/AggregatedReactionMock.swift index bf9581ebb..43e34a1f2 100644 --- a/ElementX/Sources/Mocks/AggregatedReactionMock.swift +++ b/ElementX/Sources/Mocks/AggregatedReactionMock.swift @@ -69,4 +69,12 @@ extension AggregatedReaction { mockReaction(key: "🐼", senderIDs: mockIds(1)) ] } + + static var mockReactionWithLongText: AggregatedReaction { + mockReaction(key: "Matrix defines a set of open APIs for decentralised communication.", senderIDs: [alice]) + } + + static var mockReactionWithLongTextRTL: AggregatedReaction { + mockReaction(key: "تحدد المصفوفة مجموعة من واجهات برمجة التطبيقات المفتوحة للتواصل اللامركزي.", senderIDs: [alice]) + } } diff --git a/ElementX/Sources/Other/Extensions/String.swift b/ElementX/Sources/Other/Extensions/String.swift index 2096acaf3..d319ab666 100644 --- a/ElementX/Sources/Other/Extensions/String.swift +++ b/ElementX/Sources/Other/Extensions/String.swift @@ -78,3 +78,12 @@ extension String { return whiteSpaces + "\u{2800}" } } + +extension String { + func ellipsize(length: Int) -> String { + guard count > length else { + return self + } + return "\(prefix(length))…" + } +} diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift index 471eea484..dbe249af2 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift @@ -25,6 +25,8 @@ struct EmojiPickerScreen: View { @ScaledMetric(relativeTo: .title) var minimumWidth: Double = 64 + private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) + var body: some View { NavigationStack { ScrollView { @@ -33,6 +35,7 @@ struct EmojiPickerScreen: View { Section { ForEach(category.emojis) { emoji in Button { + feedbackGenerator.impactOccurred() context.send(viewAction: .emojiTapped(emoji: emoji)) } label: { Text(emoji.value) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift index b6bca5adc..d799f3a2b 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift @@ -21,8 +21,6 @@ struct TimelineItemPlainStylerView: View { @EnvironmentObject private var context: RoomScreenViewModel.Context @Environment(\.timelineGroupStyle) private var timelineGroupStyle - private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) - let timelineItem: EventBasedTimelineItemProtocol @ViewBuilder let content: () -> Content diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/ReactionsSummaryView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/ReactionsSummaryView.swift index 92ac2c75d..c100d7f6a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/ReactionsSummaryView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/ReactionsSummaryView.swift @@ -40,7 +40,7 @@ struct ReactionsSummaryView: View { private var reactionButtons: some View { ScrollView(.horizontal, showsIndicators: false) { ScrollViewReader { scrollView in - HStack { + HStack(spacing: 8) { ForEach(reactions, id: \.self) { reaction in ReactionSummaryButton(reaction: reaction, highlighted: selectedReactionKey == reaction.key) { key in selectedReactionKey = key @@ -50,7 +50,9 @@ struct ReactionsSummaryView: View { } .padding(.horizontal, 20) .onAppear { - scrollView.scrollTo(selectedReactionKey, anchor: .leading) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + scrollView.scrollTo(selectedReactionKey) + } } .onChange(of: selectedReactionKey) { _ in scrollView.scrollTo(selectedReactionKey) @@ -91,7 +93,7 @@ private struct ReactionSummaryButton: View { var label: some View { HStack(spacing: 4) { - Text(reaction.key) + Text(reaction.displayKey) .font(.compound.headingSM) if reaction.count > 1 { Text(String(reaction.count)) @@ -99,8 +101,8 @@ private struct ReactionSummaryButton: View { .foregroundColor(highlighted ? Color.compound.textOnSolidPrimary : Color.compound.textSecondary) } } - .padding(.vertical, 6) - .padding(.horizontal, 8) + .padding(.vertical, 8) + .padding(.horizontal, 12) .background(highlighted ? Color.compound.bgActionPrimaryRest : .clear, in: Capsule()) .accessibilityElement(children: .combine) } @@ -124,19 +126,20 @@ private struct ReactionSummarySenderView: View { imageProvider: imageProvider) VStack(alignment: .leading) { - Text(displayName) - .font(.compound.bodyMDSemibold) + HStack { + Text(displayName) + .font(.compound.bodyMDSemibold) + .frame(maxWidth: .infinity, alignment: .leading) + Text(sender.timestamp.formattedMinimal()) + .font(.compound.bodyXS) + .foregroundColor(.compound.textSecondary) + } Text(sender.senderID) .font(.compound.bodySM) .foregroundColor(.compound.textSecondary) } - .frame(maxWidth: .infinity, alignment: .leading) - - Text(sender.timestamp.formattedMinimal()) - .font(.compound.bodyXS) - .foregroundColor(.compound.textSecondary) } - .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 8) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift index 65ff1137e..d79b90052 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift @@ -17,11 +17,9 @@ import SwiftUI struct TimelineReactionsView: View { - /// We use a coordinate space for measuring the reactions within their container. - /// For some reason when using .local the origin of reactions always shown as (0, 0) - private static let flowCoordinateSpace = "flowCoordinateSpace" private static let horizontalSpacing: CGFloat = 4 private static let verticalSpacing: CGFloat = 4 + private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) @EnvironmentObject private var context: RoomScreenViewModel.Context @Environment(\.layoutDirection) private var layoutDirection: LayoutDirection @Namespace private var animation @@ -34,6 +32,7 @@ struct TimelineReactionsView: View { CollapsibleReactionLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) { ForEach(reactions, id: \.self) { reaction in TimelineReactionButton(reaction: reaction) { key in + feedbackGenerator.impactOccurred() context.send(viewAction: .toggleReaction(key: key, itemID: itemID)) } showReactionSummary: { key in context.send(viewAction: .reactionSummary(itemID: itemID, key: key)) @@ -44,18 +43,18 @@ struct TimelineReactionsView: View { collapsed.toggle() } label: { TimelineCollapseButtonLabel(collapsed: collapsed) + .transaction { $0.animation = nil } } .reactionLayoutItem(.expandCollapse) - .animation(.easeOut, value: collapsed) Button { context.send(viewAction: .displayEmojiPicker(itemID: itemID)) } label: { TimelineReactionAddMoreButtonLabel() } - .animation(.easeOut, value: collapsed) .reactionLayoutItem(.addMore) } - .coordinateSpace(name: Self.flowCoordinateSpace) + .animation(.easeInOut(duration: 0.1), value: reactions) + .padding(.leading, 4) } } @@ -92,9 +91,7 @@ struct TimelineCollapseButtonLabel: View { TimelineReactionButtonLabel { Text(collapsed ? L10n.screenRoomReactionsShowMore : L10n.screenRoomReactionsShowLess) .padding(.vertical, 6) - .padding(.horizontal, 8) - .layoutPriority(1) - .drawingGroup() + .padding(.horizontal, 12) .font(.compound.bodyMD) .foregroundColor(.compound.textPrimary) } @@ -119,7 +116,7 @@ struct TimelineReactionButton: View { var label: some View { TimelineReactionButtonLabel(isHighlighted: reaction.isHighlighted) { HStack(spacing: 4) { - Text(reaction.key) + Text(reaction.displayKey) .font(.compound.bodyMD) if reaction.count > 1 { Text(String(reaction.count)) @@ -128,7 +125,7 @@ struct TimelineReactionButton: View { } } .padding(.vertical, 6) - .padding(.horizontal, 8) + .padding(.horizontal, 12) } } @@ -149,6 +146,7 @@ struct TimelineReactionAddMoreButtonLabel: View { // matches the height of the text based buttons. .padding(.horizontal, 10) .frame(maxHeight: .infinity, alignment: .center) + .foregroundColor(.compound.iconSecondary) } } } @@ -159,11 +157,15 @@ struct TimelineReactionViewPreviewsContainer: View { var body: some View { VStack { - TimelineReactionsView(itemID: .init(timelineID: "1"), reactions: Array(AggregatedReaction.mockReactions.prefix(3)), collapsed: .constant(true)) + TimelineReactionsView(itemID: .init(timelineID: "1"), reactions: [ + AggregatedReaction.mockReactionWithLongText, + AggregatedReaction.mockReactionWithLongTextRTL + ], collapsed: .constant(true)) + TimelineReactionsView(itemID: .init(timelineID: "2"), reactions: Array(AggregatedReaction.mockReactions.prefix(3)), collapsed: .constant(true)) Divider() - TimelineReactionsView(itemID: .init(timelineID: "2"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1) + TimelineReactionsView(itemID: .init(timelineID: "3"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1) Divider() - TimelineReactionsView(itemID: .init(timelineID: "3"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState2) + TimelineReactionsView(itemID: .init(timelineID: "4"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState2) .environment(\.layoutDirection, .rightToLeft) } .background(Color.red) diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift index 9a9a18742..df8aa580b 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift @@ -124,6 +124,7 @@ public struct TimelineItemMenu: View { let item: EventBasedTimelineItemProtocol let actions: TimelineItemMenuActions + private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) public var body: some View { VStack { @@ -218,6 +219,7 @@ public struct TimelineItemMenu: View { private func reactionButton(for emoji: String) -> some View { Button { + feedbackGenerator.impactOccurred() dismiss() context.send(viewAction: .toggleReaction(key: emoji, itemID: item.id)) } label: { diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/AggregratedReaction.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/AggregratedReaction.swift index 2b9d0ec2a..f5e02d41b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/AggregratedReaction.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/AggregratedReaction.swift @@ -18,6 +18,11 @@ import Foundation /// Represents all reactions of the same type for a single event. struct AggregatedReaction: Hashable { + /// Length at which we ellipsize a reaction key for display + /// Reactions can be free text, so we need to limit the length + /// displayed on screen. + private static let maxDisplayChars = 16 + /// The id of the account owner let accountOwnerID: String /// The reaction that was sent. @@ -44,4 +49,9 @@ extension AggregatedReaction { var isHighlighted: Bool { senders.contains(where: { $0.senderID == accountOwnerID }) } + + /// The key to be displayed on screen. See `maxDisplayChars`. + var displayKey: String { + key.ellipsize(length: Self.maxDisplayChars) + } } diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomEncryptedWithAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomEncryptedWithAvatar.png index cb77d3709..199fdbff0 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomEncryptedWithAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomEncryptedWithAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c23c7cdf9392e0a9c6d4506147ea724240273c99b7ef8f94bb9e14e3c9e533ba -size 297394 +oid sha256:8538909b4eb58f736fb4be966726cdf0c000caf3b9e285f98b97326f8cbb5964 +size 297160 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPlainNoAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPlainNoAvatar.png index e110ae733..1370f20f0 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPlainNoAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPlainNoAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:859eed113d4847d36d2e57a40a3cf725aa10cf04f3aed279f96718aec96148e7 -size 297139 +oid sha256:359a17ae2ab3c7f11348eeab34bb416097704dedbf6ff95d198fbd420e243a02 +size 297042 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomEncryptedWithAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomEncryptedWithAvatar.png index 73484fbdc..b68128892 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomEncryptedWithAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomEncryptedWithAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:004096089c03d37c0806c889e12cb6d3b48327c7d37cca55e69d3c0410202e8c -size 392571 +oid sha256:57441eaeba880a5cccab827ac5b9817b2a3605a219553ad37bb67ea78b871d3a +size 364717 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPlainNoAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPlainNoAvatar.png index 60045693a..95e1e0f3a 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPlainNoAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPlainNoAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20d5d19b75a2f0ac721bb73128c147381a5d76b0cee15e3c0f4c218537384c94 -size 392078 +oid sha256:8b1788c514b01a82938458a571d52550eea9552575a0c85d2b62a0a4bb19fde2 +size 364426 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomEncryptedWithAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomEncryptedWithAvatar.png index 034cff40e..49aeb9196 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomEncryptedWithAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomEncryptedWithAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f3c7e90abe271e563a99ebfcb108d6896553ab00599013695cfe265939e9821 -size 298482 +oid sha256:05d9d894f8c655acd034615af8fc5098bf6adb64b475219d1a58da9eea9697e6 +size 298240 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPlainNoAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPlainNoAvatar.png index 9283a90fa..ecbcf333c 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPlainNoAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPlainNoAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:587bda739d2b923f4d760136520e43ca38911ff93829d7d9aee92a281da69d06 -size 298223 +oid sha256:ef52847f31d85009e37e06340fc4ad21317f907be2fbf4a873a43f4b4740d5c6 +size 298123 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomEncryptedWithAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomEncryptedWithAvatar.png index 451229b48..4d38523e2 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomEncryptedWithAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomEncryptedWithAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02e573a04518ef8ae2a2fc05a8b767c8aae2b1f826df324aab28f367663e8f7d -size 385149 +oid sha256:af701156aa31a33ba3787cdcf29e9c9d0e96bedc017308c88acedae08c80f12c +size 357151 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPlainNoAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPlainNoAvatar.png index 391807b32..e01e34d1a 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPlainNoAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPlainNoAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d92f99aa7aacc07b86cd05cff5c4336c037580ca5e45b2dc385154832448826b -size 384656 +oid sha256:ea0906e449a16419f404bf56233440193dfc071cac5fb668c711eedca657ed76 +size 356860 diff --git a/UnitTests/Sources/StringTests.swift b/UnitTests/Sources/StringTests.swift index b40a4e7a0..809f21b1b 100644 --- a/UnitTests/Sources/StringTests.swift +++ b/UnitTests/Sources/StringTests.swift @@ -80,4 +80,12 @@ class StringTests: XCTestCase { result = "" XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft), result) } + + func testEllipsizeWorks() { + XCTAssertEqual("ellipsize".ellipsize(length: 5), "ellip…") + } + + func testEllipsizeNotNeeded() { + XCTAssertEqual("ellipsize".ellipsize(length: 15), "ellipsize") + } }