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