@@ -6,10 +6,14 @@ 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
+ import 'action_sheet.dart' ;
9
11
import 'color.dart' ;
12
+ import 'content.dart' ;
10
13
import 'dialog.dart' ;
11
14
import 'emoji.dart' ;
12
15
import 'inset_shadow.dart' ;
16
+ import 'profile.dart' ;
13
17
import 'store.dart' ;
14
18
import 'text.dart' ;
15
19
import 'theme.dart' ;
@@ -127,23 +131,36 @@ class ReactionChipsList extends StatelessWidget {
127
131
final showNames = displayEmojiReactionUsers && reactions.total <= 3 ;
128
132
129
133
return Wrap (spacing: 4 , runSpacing: 4 , crossAxisAlignment: WrapCrossAlignment .center,
130
- children: reactions.aggregated.map ((reactionVotes) => ReactionChip (
134
+ children: reactions.aggregated.map ((reactionVotes) {
135
+ final index = reactions.aggregated.indexOf (reactionVotes);
136
+ return ReactionChip (
131
137
showName: showNames,
132
- messageId: messageId, reactionWithVotes: reactionVotes),
133
- ).toList ());
138
+ messageId: messageId,
139
+ reactionWithVotes: reactionVotes,
140
+ onLongPress: (context){
141
+ showReactionListSheet (
142
+ context,
143
+ reactionList: reactions.aggregated,
144
+ initialTabIndex: index,
145
+ );
146
+ }
147
+ );
148
+ }).toList ());
134
149
}
135
150
}
136
151
137
152
class ReactionChip extends StatelessWidget {
138
153
final bool showName;
139
154
final int messageId;
140
155
final ReactionWithVotes reactionWithVotes;
156
+ final void Function (BuildContext context)? onLongPress;
141
157
142
158
const ReactionChip ({
143
159
super .key,
144
160
required this .showName,
145
161
required this .messageId,
146
162
required this .reactionWithVotes,
163
+ this .onLongPress,
147
164
});
148
165
149
166
@override
@@ -206,6 +223,11 @@ class ReactionChip extends StatelessWidget {
206
223
customBorder: shape,
207
224
splashColor: splashColor,
208
225
highlightColor: highlightColor,
226
+ onLongPress: (){
227
+ if (onLongPress != null ) {
228
+ onLongPress !(context);
229
+ }
230
+ },
209
231
onTap: () {
210
232
(selfVoted ? removeReaction : addReaction).call (store.connection,
211
233
messageId: messageId,
@@ -266,6 +288,179 @@ class ReactionChip extends StatelessWidget {
266
288
}
267
289
}
268
290
291
+ void showReactionListSheet (
292
+ BuildContext context, {
293
+ required List <ReactionWithVotes >? reactionList,
294
+ int initialTabIndex = 0 ,
295
+ }) {
296
+ final store = PerAccountStoreWidget .of (context);
297
+
298
+ if (reactionList == null || reactionList.isEmpty) return ;
299
+
300
+ showModalBottomSheet <void >(
301
+ context: context,
302
+ clipBehavior: Clip .antiAlias,
303
+ useSafeArea: true ,
304
+ isScrollControlled: true ,
305
+ builder: (BuildContext modalContext) {
306
+ return ConstrainedBox (
307
+ constraints: BoxConstraints (
308
+ maxHeight: MediaQuery .of (context).size.height * 0.7 ,
309
+ ),
310
+ child: SafeArea (
311
+ minimum: const EdgeInsets .only (bottom: 16 ),
312
+ child: Padding (
313
+ padding: const EdgeInsets .fromLTRB (16 , 0 , 16 , 0 ),
314
+ child: Column (
315
+ crossAxisAlignment: CrossAxisAlignment .stretch,
316
+ mainAxisSize: MainAxisSize .min,
317
+ children: [
318
+ Flexible (
319
+ child: InsetShadowBox (
320
+ top: 8 ,
321
+ bottom: 8 ,
322
+ color: DesignVariables .of (context).bgContextMenu,
323
+ child: PerAccountStoreWidget (
324
+ accountId: store.accountId,
325
+ child: ReactionListContent (
326
+ store: store,
327
+ reactionList: reactionList,
328
+ initialTabIndex: initialTabIndex
329
+ ),
330
+ ),
331
+ ),
332
+ ),
333
+ const ActionSheetCancelButton (),
334
+ ],
335
+ ),
336
+ ),
337
+ ),
338
+ );
339
+ },
340
+ );
341
+ }
342
+ class ReactionListContent extends StatelessWidget {
343
+ final PerAccountStore store;
344
+ final List <ReactionWithVotes > reactionList;
345
+ final int initialTabIndex;
346
+
347
+ const ReactionListContent ({
348
+ super .key,
349
+ required this .store,
350
+ required this .reactionList,
351
+ this .initialTabIndex = 0 ,
352
+ });
353
+
354
+ @override
355
+ Widget build (BuildContext context) {
356
+ final designVariables = DesignVariables .of (context);
357
+
358
+ final tabs = reactionList.map ((reaction) {
359
+ final emojiDisplay = store.emojiDisplayFor (
360
+ emojiType: reaction.reactionType,
361
+ emojiCode: reaction.emojiCode,
362
+ emojiName: reaction.emojiName,
363
+ ).resolve (store.userSettings);
364
+
365
+ final emoji = switch (emojiDisplay) {
366
+ UnicodeEmojiDisplay () => _UnicodeEmoji (emojiDisplay: emojiDisplay),
367
+ ImageEmojiDisplay () => _ImageEmoji (
368
+ emojiDisplay: emojiDisplay,
369
+ emojiName: reaction.emojiName,
370
+ selected: reaction.userIds.contains (store.selfUserId),
371
+ ),
372
+ TextEmojiDisplay () => _TextEmoji (
373
+ emojiDisplay: emojiDisplay,
374
+ selected: reaction.userIds.contains (store.selfUserId),
375
+ ),
376
+ };
377
+
378
+ return Tab (
379
+ child: Row (
380
+ mainAxisSize: MainAxisSize .min,
381
+ mainAxisAlignment: MainAxisAlignment .center,
382
+ children: [
383
+ emoji,
384
+ const SizedBox (width: 4 ),
385
+ Text (
386
+ '${reaction .userIds .length }' ,
387
+ style: const TextStyle ()
388
+ .merge (weightVariableTextStyle (context, wght: 600 )),
389
+ ),
390
+ ],
391
+ ),
392
+ );
393
+ }).toList ();
394
+
395
+ final tabViews = reactionList.map ((reaction) {
396
+ return ListView .builder (
397
+ padding: EdgeInsets .zero,
398
+ itemCount: reaction.userIds.length,
399
+ itemBuilder: (context, index) {
400
+ final userId = reaction.userIds.elementAt (index);
401
+ return Container (
402
+ decoration: BoxDecoration (
403
+ color: designVariables.contextMenuItemBg.withFadedAlpha (0.20 ),
404
+ borderRadius: BorderRadius .circular (8 ),
405
+ ),
406
+ margin: const EdgeInsets .symmetric (vertical: 2 ),
407
+ child: ListTile (
408
+ leading: Avatar (userId: userId, size: 36.0 , borderRadius: 4 ),
409
+ title: Text (
410
+ userId == store.selfUserId
411
+ ? 'You'
412
+ : store.users[userId]? .fullName ?? '(unknown user)' ,
413
+ style: TextStyle (
414
+ color: designVariables.contextMenuItemText,
415
+ fontSize: 18 ,
416
+ ).merge (weightVariableTextStyle (context, wght: 600 )),
417
+ ),
418
+ onTap: () {
419
+ Navigator .push (
420
+ context,
421
+ ProfilePage .buildRoute (context: context, userId: userId),
422
+ );
423
+ },
424
+ ),
425
+ );
426
+ },
427
+ );
428
+ }).toList ();
429
+
430
+ return DefaultTabController (
431
+ length: tabs.length,
432
+ initialIndex: initialTabIndex,
433
+ child: Column (
434
+ mainAxisSize: MainAxisSize .min,
435
+ children: [
436
+ Padding (
437
+ padding: const EdgeInsets .only (top: 16.0 ), // Add space above the TabBar
438
+ child: TabBar (
439
+ isScrollable: true ,
440
+ indicator: BoxDecoration (
441
+ color: designVariables.contextMenuItemBg.withFadedAlpha (0.1 ),
442
+ borderRadius: BorderRadius .circular (8 ),
443
+ ),
444
+ splashFactory: NoSplash .splashFactory,
445
+ indicatorSize: TabBarIndicatorSize .tab,
446
+ labelColor: designVariables.contextMenuItemText,
447
+ unselectedLabelColor: designVariables.contextMenuItemText,
448
+ labelStyle: const TextStyle (fontSize: 14 )
449
+ .merge (weightVariableTextStyle (context, wght: 700 )),
450
+ unselectedLabelStyle: const TextStyle (fontSize: 14 )
451
+ .merge (weightVariableTextStyle (context, wght: 400 )),
452
+ tabs: tabs,
453
+ ),
454
+ ),
455
+ const SizedBox (height: 8 ), // Space between TabBar and TabBarView
456
+ Flexible (
457
+ child: TabBarView (children: tabViews),
458
+ ),
459
+ ],
460
+ ),
461
+ );
462
+ }
463
+ }
269
464
/// The size of a square emoji (Unicode or image).
270
465
///
271
466
/// Should be scaled by [_emojiTextScalerClamped] .
0 commit comments