@@ -6,13 +6,11 @@ import '../api/route/messages.dart';
66import '../generated/l10n/zulip_localizations.dart' ;
77import '../model/autocomplete.dart' ;
88import '../model/emoji.dart' ;
9- import '../model/store.dart' ;
109import 'color.dart' ;
11- import 'content.dart' ;
1210import 'dialog.dart' ;
1311import 'emoji.dart' ;
1412import 'inset_shadow.dart' ;
15- import 'profile .dart' ;
13+ import 'reaction_users_sheet .dart' ;
1614import 'store.dart' ;
1715import 'text.dart' ;
1816import '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-
404156class ReactionChip extends StatelessWidget {
405157 final bool showName;
406158 final int messageId;
0 commit comments