Skip to content

Commit 0a511fb

Browse files
authored
Re-enable reactions bar in message context menu (#506)
* Re-enable reactions bar in context menu with drag dismiss Uncomment the reactions bar in MessageContextMenuOverlay so it appears above the message bubble during long-press. Add interactive drag-dismiss behavior matching the action menu: scale down, follow drag offset at 60%, and fade out on dismiss. * Animate context menu for emoji picker keyboard When the custom emoji picker opens: - Hide action menu with matching appear/disappear animation - Shift bubble and reactions bar up only as needed to clear keyboard - Track keyboard height via NotificationCenter observers Fix smiley face icon in collapsed reactions bar: - Use colorTextSecondary instead of black - Remove showMoreAppeared dependency to prevent opacity flicker * Don't flash action menu when selecting custom emoji Remove early showingEmojiPicker=false from selectReaction so the action menu stays hidden until dismissMenu sets appeared=false in the same animation pass. * Fix reactions bar button style and toolbar overlap Use plain button style on emoji and plus buttons to prevent white shape artifact when scrolling the reactions bar. Clamp reactions bar Y position to window safe area top so it never overlaps the toolbar. Use window scene safe area insets as fallback when GeometryReader reports 0 due to ignoresSafeArea. * Scrollable reactions drawer for many reactions (#508) * Fix lint: remove blank line before closing brace
1 parent 6a47d6e commit 0a511fb

4 files changed

Lines changed: 587 additions & 229 deletions

File tree

Convos/Conversation Detail/Messages/Messages View Controller/Reactions/ReactionIndicatorView.swift

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ struct ReactionIndicatorView: View {
99
private var uniqueEmojis: [String] {
1010
var seen = Set<String>()
1111
var result: [String] = []
12-
for reaction in reactions where !seen.contains(reaction.emoji) {
12+
for reaction in reactions.sorted(by: { $0.date > $1.date }) where !seen.contains(reaction.emoji) {
1313
seen.insert(reaction.emoji)
1414
result.append(reaction.emoji)
1515
}
@@ -39,7 +39,8 @@ struct ReactionIndicatorView: View {
3939
ReactionPillView(
4040
emojis: uniqueEmojis,
4141
count: totalCount,
42-
isSelected: currentUserHasReacted
42+
isSelected: currentUserHasReacted,
43+
maxWidth: UIScreen.main.bounds.width * Constant.maxWidth * 0.5 - 20
4344
)
4445
}
4546
.buttonStyle(.plain)
@@ -55,11 +56,13 @@ private struct ReactionPillView: View {
5556
let emojis: [String]
5657
let count: Int
5758
let isSelected: Bool
59+
let maxWidth: CGFloat
5860

5961
@State private var appearedEmojis: Set<String> = []
6062
@State private var pillAppeared: Bool = false
63+
@State private var contentWidth: CGFloat = 0
6164

62-
var body: some View {
65+
private var emojiContent: some View {
6366
HStack(spacing: DesignConstants.Spacing.stepHalf) {
6467
ForEach(emojis, id: \.self) { emoji in
6568
let appeared = appearedEmojis.contains(emoji)
@@ -77,14 +80,69 @@ private struct ReactionPillView: View {
7780
}
7881
}
7982
}
80-
if count > 1 {
81-
Text("\(count)")
82-
.font(.footnote)
83-
.foregroundStyle(.colorTextSecondary)
84-
}
8583
}
8684
.padding(.horizontal, DesignConstants.Spacing.step3x)
85+
}
86+
87+
private var needsScrolling: Bool {
88+
contentWidth > maxWidth
89+
}
90+
91+
@State private var countBadgeWidth: CGFloat = 0
92+
93+
var body: some View {
94+
Group {
95+
if needsScrolling {
96+
ZStack(alignment: .trailing) {
97+
ScrollView(.horizontal, showsIndicators: false) {
98+
emojiContent
99+
}
100+
.scrollBounceBehavior(.basedOnSize)
101+
.contentMargins(.trailing, countBadgeWidth, for: .scrollContent)
102+
.mask(
103+
HStack(spacing: 0) {
104+
Rectangle().fill(.black)
105+
LinearGradient(
106+
colors: [.black, .black.opacity(0)],
107+
startPoint: .leading,
108+
endPoint: .trailing
109+
)
110+
.frame(width: 12)
111+
Rectangle().fill(.clear)
112+
.frame(width: countBadgeWidth)
113+
}
114+
)
115+
116+
Text("\(count)")
117+
.font(.footnote)
118+
.foregroundStyle(.colorTextSecondary)
119+
.padding(.leading, DesignConstants.Spacing.stepX)
120+
.padding(.trailing, DesignConstants.Spacing.step3x)
121+
.fixedSize()
122+
.background(GeometryReader { geo in
123+
Color.clear.preference(key: CountBadgeWidthKey.self, value: geo.size.width)
124+
})
125+
.onPreferenceChange(CountBadgeWidthKey.self) { width in
126+
countBadgeWidth = width
127+
}
128+
}
129+
.frame(width: maxWidth)
130+
} else {
131+
emojiContent
132+
}
133+
}
87134
.padding(.vertical, DesignConstants.Spacing.step2x)
135+
.background(
136+
emojiContent
137+
.fixedSize()
138+
.hidden()
139+
.background(GeometryReader { geo in
140+
Color.clear.preference(key: ContentWidthKey.self, value: geo.size.width)
141+
})
142+
)
143+
.onPreferenceChange(ContentWidthKey.self) { width in
144+
contentWidth = width + DesignConstants.Spacing.step2x * 2
145+
}
88146
.background(isSelected ? .colorFillMinimal : .clear)
89147
.clipShape(Capsule())
90148
.overlay(
@@ -116,6 +174,20 @@ private struct ReactionPillView: View {
116174
}
117175
}
118176

177+
private struct ContentWidthKey: PreferenceKey {
178+
nonisolated(unsafe) static var defaultValue: CGFloat = 0
179+
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
180+
value = max(value, nextValue())
181+
}
182+
}
183+
184+
private struct CountBadgeWidthKey: PreferenceKey {
185+
nonisolated(unsafe) static var defaultValue: CGFloat = 0
186+
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
187+
value = max(value, nextValue())
188+
}
189+
}
190+
119191
#Preview("Not Selected (outline)") {
120192
let reactions = [
121193
MessageReaction.mock(emoji: "❤️", sender: .mock(isCurrentUser: false, name: "Alice")),

Convos/Conversation Detail/Messages/Messages View Controller/Reactions/ReactionsDrawerView.swift

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,38 @@ struct ReactionsDrawerView: View {
1818
if !first.sender.isCurrentUser && second.sender.isCurrentUser {
1919
return false
2020
}
21-
return false
21+
return first.date > second.date
2222
}
2323
}
2424

25+
private var maxDrawerHeight: CGFloat {
26+
UIScreen.main.bounds.height * 0.7
27+
}
28+
2529
var body: some View {
2630
VStack(alignment: .leading, spacing: DesignConstants.Spacing.step4x) {
2731
Text("Reactions")
2832
.font(.system(.largeTitle))
2933
.fontWeight(.bold)
3034
.padding(.bottom, DesignConstants.Spacing.step2x)
3135

32-
ForEach(sortedReactions, id: \.id) { reaction in
33-
ReactionRowView(
34-
reaction: reaction,
35-
onRemove: reaction.sender.isCurrentUser ? { onRemoveReaction?(reaction) } : nil
36-
)
36+
ScrollView {
37+
VStack(alignment: .leading, spacing: DesignConstants.Spacing.step4x) {
38+
ForEach(sortedReactions, id: \.id) { reaction in
39+
ReactionRowView(
40+
reaction: reaction,
41+
onRemove: reaction.sender.isCurrentUser ? { onRemoveReaction?(reaction) } : nil
42+
)
43+
}
44+
}
3745
}
46+
.scrollBounceBehavior(.basedOnSize)
47+
.scrollIndicatorsFlash(onAppear: true)
48+
.scrollContentBackground(.hidden)
3849
}
3950
.padding([.leading, .top, .trailing], DesignConstants.Spacing.step10x)
4051
.padding(.bottom, DesignConstants.Spacing.step3x)
41-
.frame(minHeight: 160)
52+
.frame(minHeight: 160, maxHeight: maxDrawerHeight)
4253
}
4354
}
4455

@@ -74,6 +85,7 @@ private struct ReactionRowView: View {
7485

7586
Text(reaction.emoji)
7687
.font(.title2)
88+
.padding(.trailing, DesignConstants.Spacing.step2x)
7789
}
7890
.contentShape(Rectangle())
7991
.onTapGesture {
@@ -96,9 +108,19 @@ private struct ReactionRowView: View {
96108
let reactions = [
97109
MessageReaction.mock(emoji: "❤️", sender: .mock(isCurrentUser: true)),
98110
MessageReaction.mock(emoji: "❤️", sender: .mock(isCurrentUser: false, name: "Shane")),
99-
MessageReaction.mock(emoji: "❤️", sender: .mock(isCurrentUser: false, name: "Jarod")),
100-
MessageReaction.mock(emoji: "❤️", sender: .mock(isCurrentUser: false, name: "Andrew")),
101-
MessageReaction.mock(emoji: "❤️", sender: .mock(isCurrentUser: false, name: "Louis")),
111+
MessageReaction.mock(emoji: "😂", sender: .mock(isCurrentUser: false, name: "Jarod")),
112+
MessageReaction.mock(emoji: "👍", sender: .mock(isCurrentUser: false, name: "Andrew")),
113+
MessageReaction.mock(emoji: "🤔", sender: .mock(isCurrentUser: false, name: "Louis")),
114+
MessageReaction.mock(emoji: "😮", sender: .mock(isCurrentUser: false, name: "Alex")),
115+
MessageReaction.mock(emoji: "👎", sender: .mock(isCurrentUser: false, name: "Chris")),
116+
MessageReaction.mock(emoji: "🔥", sender: .mock(isCurrentUser: false, name: "Taylor")),
117+
MessageReaction.mock(emoji: "💯", sender: .mock(isCurrentUser: false, name: "Jordan")),
118+
MessageReaction.mock(emoji: "🎉", sender: .mock(isCurrentUser: false, name: "Sam")),
119+
MessageReaction.mock(emoji: "😍", sender: .mock(isCurrentUser: false, name: "Morgan")),
120+
MessageReaction.mock(emoji: "🙏", sender: .mock(isCurrentUser: false, name: "Riley")),
121+
MessageReaction.mock(emoji: "", sender: .mock(isCurrentUser: false, name: "Casey")),
122+
MessageReaction.mock(emoji: "💀", sender: .mock(isCurrentUser: false, name: "Quinn")),
123+
MessageReaction.mock(emoji: "🫡", sender: .mock(isCurrentUser: false, name: "Drew")),
102124
]
103125
let message = Message.mock(reactions: reactions)
104126
let anyMessage = AnyMessage.message(message, .existing)

0 commit comments

Comments
 (0)