Skip to content

Commit 607fa50

Browse files
Updated the design to match the requirements and organized the reaction_user_sheet into a seperate file and added tests
1 parent c40edd9 commit 607fa50

File tree

3 files changed

+1092
-250
lines changed

3 files changed

+1092
-250
lines changed

lib/widgets/emoji_reaction.dart

+2-250
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@ import '../api/route/messages.dart';
66
import '../generated/l10n/zulip_localizations.dart';
77
import '../model/autocomplete.dart';
88
import '../model/emoji.dart';
9-
import '../model/store.dart';
109
import 'color.dart';
11-
import 'content.dart';
1210
import 'dialog.dart';
1311
import 'emoji.dart';
1412
import 'inset_shadow.dart';
15-
import 'profile.dart';
13+
import 'reaction_users_sheet.dart';
1614
import 'store.dart';
1715
import 'text.dart';
1816
import 'theme.dart';
@@ -130,7 +128,7 @@ class ReactionChipsList extends StatelessWidget {
130128
context: context,
131129
builder: (BuildContext context) => PerAccountStoreWidget(
132130
accountId: store.accountId,
133-
child: _ReactionUsersSheet(
131+
child: ReactionUsersSheet(
134132
reactions: reactions,
135133
initialSelectedReaction: selectedReaction,
136134
store: store,
@@ -155,252 +153,6 @@ class ReactionChipsList extends StatelessWidget {
155153
}
156154
}
157155

158-
class _ReactionUsersSheet extends StatefulWidget {
159-
const _ReactionUsersSheet({
160-
required this.reactions,
161-
required this.initialSelectedReaction,
162-
required this.store,
163-
});
164-
165-
final Reactions reactions;
166-
final ReactionWithVotes initialSelectedReaction;
167-
final PerAccountStore store;
168-
169-
@override
170-
State<_ReactionUsersSheet> createState() => _ReactionUsersSheetState();
171-
}
172-
173-
class _ReactionUsersSheetState extends State<_ReactionUsersSheet> {
174-
late ReactionWithVotes? _selectedReaction;
175-
176-
/// Cache for emoji displays to avoid recomputing them
177-
final Map<String, Widget> _emojiCache = {};
178-
179-
@override
180-
void initState() {
181-
super.initState();
182-
_selectedReaction = widget.initialSelectedReaction;
183-
_prepareEmojiCache();
184-
}
185-
186-
/// Pre-compute emoji displays for better performance
187-
void _prepareEmojiCache() {
188-
for (final reaction in widget.reactions.aggregated) {
189-
final key = '${reaction.reactionType}_${reaction.emojiCode}';
190-
if (!_emojiCache.containsKey(key)) {
191-
final emojiDisplay = widget.store.emojiDisplayFor(
192-
emojiType: reaction.reactionType,
193-
emojiCode: reaction.emojiCode,
194-
emojiName: reaction.emojiName,
195-
).resolve(widget.store.userSettings);
196-
197-
final emoji = switch (emojiDisplay) {
198-
UnicodeEmojiDisplay() => _UnicodeEmoji(
199-
emojiDisplay: emojiDisplay),
200-
ImageEmojiDisplay() => _ImageEmoji(
201-
emojiDisplay: emojiDisplay, emojiName: reaction.emojiName, selected: false),
202-
TextEmojiDisplay() => _TextEmoji(
203-
emojiDisplay: emojiDisplay, selected: false),
204-
};
205-
206-
_emojiCache[key] = SizedBox(
207-
width: 20,
208-
height: 20,
209-
child: emoji,
210-
);
211-
}
212-
}
213-
}
214-
215-
Widget _getEmojiWidget(ReactionWithVotes reaction) {
216-
final key = '${reaction.reactionType}_${reaction.emojiCode}';
217-
return _emojiCache[key]!;
218-
}
219-
220-
Widget _buildEmojiButton(ReactionWithVotes reaction) {
221-
final isSelected = _selectedReaction == reaction;
222-
final reactionTheme = EmojiReactionTheme.of(context);
223-
224-
return Material(
225-
color: Colors.transparent,
226-
child: InkWell(
227-
onTap: () {
228-
setState(() {
229-
_selectedReaction = reaction;
230-
});
231-
},
232-
child: Padding(
233-
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 4),
234-
child: Column(
235-
mainAxisSize: MainAxisSize.min,
236-
children: [
237-
Container(
238-
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
239-
decoration: BoxDecoration(
240-
color: isSelected ? reactionTheme.bgSelected.withValues(alpha: 0.1) : Colors.transparent,
241-
borderRadius: BorderRadius.circular(20),
242-
),
243-
child: Row(
244-
mainAxisSize: MainAxisSize.min,
245-
children: [
246-
_getEmojiWidget(reaction),
247-
const SizedBox(width: 4),
248-
Text(
249-
reaction.userIds.length.toString(),
250-
style: TextStyle(
251-
fontSize: 14,
252-
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
253-
color: isSelected ? reactionTheme.textSelected : reactionTheme.textUnselected,
254-
),
255-
),
256-
],
257-
),
258-
),
259-
AnimatedContainer(
260-
duration: const Duration(milliseconds: 300),
261-
margin: const EdgeInsets.only(top: 4),
262-
height: 2,
263-
width: isSelected ? 20 : 0,
264-
decoration: BoxDecoration(
265-
color: isSelected ? reactionTheme.textSelected : Colors.transparent,
266-
borderRadius: BorderRadius.circular(1),
267-
),
268-
),
269-
],
270-
),
271-
),
272-
),
273-
);
274-
}
275-
276-
Widget _buildAllButton() {
277-
final reactionTheme = EmojiReactionTheme.of(context);
278-
final isSelected = _selectedReaction == null;
279-
280-
return Material(
281-
color: Colors.transparent,
282-
child: InkWell(
283-
onTap: () {
284-
setState(() {
285-
_selectedReaction = null;
286-
});
287-
},
288-
child: Padding(
289-
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 4),
290-
child: Column(
291-
mainAxisSize: MainAxisSize.min,
292-
children: [
293-
Container(
294-
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
295-
decoration: BoxDecoration(
296-
color: isSelected ? reactionTheme.bgSelected.withValues(alpha: 0.1) : Colors.transparent,
297-
borderRadius: BorderRadius.circular(20),
298-
),
299-
child: Text(
300-
'All ${widget.reactions.total}',
301-
style: TextStyle(
302-
fontSize: 14,
303-
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
304-
color: isSelected ? reactionTheme.textSelected : reactionTheme.textUnselected,
305-
),
306-
),
307-
),
308-
AnimatedContainer(
309-
duration: const Duration(milliseconds: 300),
310-
margin: const EdgeInsets.only(top: 4),
311-
height: 2,
312-
width: isSelected ? 20 : 0,
313-
decoration: BoxDecoration(
314-
color: isSelected ? reactionTheme.textSelected : Colors.transparent,
315-
borderRadius: BorderRadius.circular(1),
316-
),
317-
),
318-
],
319-
),
320-
),
321-
),
322-
);
323-
}
324-
325-
List<({String name, Widget emoji, int userId})> _getUserNamesWithEmojis() {
326-
if (_selectedReaction == null) {
327-
// Show all users when "All" is selected
328-
final allUserReactions = <({String name, Widget emoji, int userId})>[];
329-
330-
for (final reaction in widget.reactions.aggregated) {
331-
// Add each user-reaction combination separately
332-
for (final userId in reaction.userIds) {
333-
allUserReactions.add((
334-
name: widget.store.users[userId]?.fullName ?? '(unknown user)',
335-
emoji: _getEmojiWidget(reaction),
336-
userId: userId,
337-
));
338-
}
339-
}
340-
341-
// Sort by name to group the same user's reactions together
342-
return allUserReactions..sort((a, b) => a.name.compareTo(b.name));
343-
} else {
344-
// Show users for selected reaction
345-
return _selectedReaction!.userIds.map((userId) => (
346-
name: widget.store.users[userId]?.fullName ?? '(unknown user)',
347-
emoji: _getEmojiWidget(_selectedReaction!),
348-
userId: userId,
349-
)).toList()..sort((a, b) => a.name.compareTo(b.name));
350-
}
351-
}
352-
353-
@override
354-
Widget build(BuildContext context) {
355-
final users = _getUserNamesWithEmojis();
356-
357-
return SafeArea(
358-
child: Column(
359-
mainAxisSize: MainAxisSize.min,
360-
crossAxisAlignment: CrossAxisAlignment.start,
361-
children: [
362-
Padding(
363-
padding: const EdgeInsets.all(16),
364-
child: SingleChildScrollView(
365-
scrollDirection: Axis.horizontal,
366-
child: Row(
367-
children: [
368-
_buildAllButton(),
369-
...widget.reactions.aggregated.map((reaction) => _buildEmojiButton(reaction)),
370-
],
371-
),
372-
),
373-
),
374-
Flexible(
375-
child: ListView.builder(
376-
shrinkWrap: true,
377-
itemCount: users.length,
378-
itemBuilder: (context, index) => InkWell(
379-
onTap: () => Navigator.push(context,
380-
ProfilePage.buildRoute(context: context,
381-
userId: users[index].userId)),
382-
child: ListTile(
383-
leading: Avatar(
384-
size: 32,
385-
borderRadius: 3,
386-
userId: users[index].userId,
387-
),
388-
title: Row(
389-
children: [
390-
Expanded(child: Text(users[index].name)),
391-
users[index].emoji,
392-
],
393-
),
394-
),
395-
),
396-
),
397-
),
398-
],
399-
),
400-
);
401-
}
402-
}
403-
404156
class ReactionChip extends StatelessWidget {
405157
final bool showName;
406158
final int messageId;

0 commit comments

Comments
 (0)