mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Reactions - Design review tweaks (#1417)
* Design review tweaks - truncate reactions as per spec - add haptic feedback on reacting - Remove all collapse/expand animations on reactions - tweak reaction and summary padding - Fix alignment of time on reaction summary view - Made sure reaction summary button in horizontal scroller is on screen on appear - update screenshots * Update TimelineReactionsView.swift * Remove dark add more button and check long rtl reactions * Set colour of templated add more button * Fix padding and only inset from the bubble on the leading side as per designs - Fix padding - Only inset from the bubble on the leading side as per designs * Update screenshots * Fix line length warning.
This commit is contained in:
ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/Contents.json
Vendored
+2
-1
@@ -10,6 +10,7 @@
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,8 +21,6 @@ struct TimelineItemPlainStylerView<Content: View>: 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+16
-14
@@ -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)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user