@@ -6,13 +6,11 @@ import '../api/route/messages.dart';
6
6
import '../generated/l10n/zulip_localizations.dart' ;
7
7
import '../model/autocomplete.dart' ;
8
8
import '../model/emoji.dart' ;
9
- import '../model/store.dart' ;
10
9
import 'color.dart' ;
11
- import 'content.dart' ;
12
10
import 'dialog.dart' ;
13
11
import 'emoji.dart' ;
14
12
import 'inset_shadow.dart' ;
15
- import 'profile .dart' ;
13
+ import 'reaction_users_sheet .dart' ;
16
14
import 'store.dart' ;
17
15
import 'text.dart' ;
18
16
import 'theme.dart' ;
@@ -130,7 +128,7 @@ class ReactionChipsList extends StatelessWidget {
130
128
context: context,
131
129
builder: (BuildContext context) => PerAccountStoreWidget (
132
130
accountId: store.accountId,
133
- child: _ReactionUsersSheet (
131
+ child: ReactionUsersSheet (
134
132
reactions: reactions,
135
133
initialSelectedReaction: selectedReaction,
136
134
store: store,
@@ -155,252 +153,6 @@ class ReactionChipsList extends StatelessWidget {
155
153
}
156
154
}
157
155
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
-
404
156
class ReactionChip extends StatelessWidget {
405
157
final bool showName;
406
158
final int messageId;
0 commit comments