Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reactions: Add sheet to view who reracted to a message. #1243

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/icons/ZulipIcons.ttf
Binary file not shown.
3 changes: 3 additions & 0 deletions assets/icons/reactions.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@
"@actionSheetOptionShare": {
"description": "Label for share button on action sheet."
},
"actionSheetOptionViewReactions": "View Reactions",
"@actionSheetOptionViewReactions": {
"description": "Label for View Reactions button on action sheet."
},
"actionSheetOptionQuoteAndReply": "Quote and reply",
"@actionSheetOptionQuoteAndReply": {
"description": "Label for Quote and reply button on action sheet."
Expand Down
6 changes: 6 additions & 0 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ abstract class ZulipLocalizations {
/// **'Share'**
String get actionSheetOptionShare;

/// Label for View Reactions button on action sheet.
///
/// In en, this message translates to:
/// **'View Reactions'**
String get actionSheetOptionViewReactions;

/// Label for Quote and reply button on action sheet.
///
/// In en, this message translates to:
Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_fr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Share';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Quote and reply';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Udostępnij';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Odpowiedz cytując';

Expand Down
3 changes: 3 additions & 0 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get actionSheetOptionShare => 'Поделиться';

@override
String get actionSheetOptionViewReactions => 'View Reactions';

@override
String get actionSheetOptionQuoteAndReply => 'Ответить с цитированием';

Expand Down
20 changes: 20 additions & 0 deletions lib/widgets/action_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ void showMessageActionSheet({required BuildContext context, required Message mes

final optionButtons = [
ReactionButtons(message: message, pageContext: context),
if((message.reactions?.total ?? 0) > 0)
ViewReactionsButton(message: message, pageContext: context),
StarButton(message: message, pageContext: context),
if (isComposeBoxOffered)
QuoteAndReplyButton(message: message, pageContext: context),
Expand Down Expand Up @@ -688,6 +690,24 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
}
}

class ViewReactionsButton extends MessageActionSheetMenuItemButton {
ViewReactionsButton({super.key, required super.message, required super.pageContext});

@override IconData get icon => ZulipIcons.reactions;

@override
String label(ZulipLocalizations zulipLocalizations) {
return zulipLocalizations.actionSheetOptionViewReactions;
}

@override void onPressed() async {
showReactionListSheet(
pageContext,
reactionList: message.reactions?.aggregated,
);
}
}

class MarkAsUnreadButton extends MessageActionSheetMenuItemButton {
MarkAsUnreadButton({super.key, required super.message, required super.pageContext});

Expand Down
224 changes: 221 additions & 3 deletions lib/widgets/emoji_reaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import '../api/route/messages.dart';
import '../generated/l10n/zulip_localizations.dart';
import '../model/autocomplete.dart';
import '../model/emoji.dart';
import '../model/store.dart';
import 'color.dart';
import 'content.dart';
import 'dialog.dart';
import 'emoji.dart';
import 'inset_shadow.dart';
import 'profile.dart';
import 'store.dart';
import 'text.dart';
import 'theme.dart';
Expand Down Expand Up @@ -127,23 +130,36 @@ class ReactionChipsList extends StatelessWidget {
final showNames = displayEmojiReactionUsers && reactions.total <= 3;

return Wrap(spacing: 4, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center,
children: reactions.aggregated.map((reactionVotes) => ReactionChip(
children: reactions.aggregated.map((reactionVotes) {
final index = reactions.aggregated.indexOf(reactionVotes);
return ReactionChip(
showName: showNames,
messageId: messageId, reactionWithVotes: reactionVotes),
).toList());
messageId: messageId,
reactionWithVotes: reactionVotes,
onLongPress:(context){
showReactionListSheet(
context,
reactionList: reactions.aggregated,
initialTabIndex: index,
);
}
);
}).toList());
}
}

class ReactionChip extends StatelessWidget {
final bool showName;
final int messageId;
final ReactionWithVotes reactionWithVotes;
final void Function(BuildContext context)? onLongPress;

const ReactionChip({
super.key,
required this.showName,
required this.messageId,
required this.reactionWithVotes,
this.onLongPress,
});

@override
Expand Down Expand Up @@ -206,6 +222,11 @@ class ReactionChip extends StatelessWidget {
customBorder: shape,
splashColor: splashColor,
highlightColor: highlightColor,
onLongPress: (){
if (onLongPress != null) {
onLongPress!(context);
}
},
onTap: () {
(selfVoted ? removeReaction : addReaction).call(store.connection,
messageId: messageId,
Expand Down Expand Up @@ -266,6 +287,203 @@ class ReactionChip extends StatelessWidget {
}
}

void showReactionListSheet(
BuildContext context, {
required List<ReactionWithVotes>? reactionList,
int initialTabIndex = 0,
}) {
final store = PerAccountStoreWidget.of(context);

if (reactionList == null || reactionList.isEmpty) return;

showModalBottomSheet<void>(
context: context,
clipBehavior: Clip.antiAlias,
useSafeArea: true,
isScrollControlled: true,
builder: (BuildContext modalContext) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
child: SafeArea(
minimum: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: InsetShadowBox(
top: 8,
bottom: 8,
color: DesignVariables.of(context).bgContextMenu,
child: PerAccountStoreWidget(
accountId: store.accountId,
child: ReactionListContent(
store: store,
reactionList: reactionList,
initialTabIndex: initialTabIndex
),
),
),
),
const ReactionSheetCloseButton(),
],
),
),
),
);
},
);
}
class ReactionListContent extends StatelessWidget {
final PerAccountStore store;
final List<ReactionWithVotes> reactionList;
final int initialTabIndex;

const ReactionListContent({
super.key,
required this.store,
required this.reactionList,
this.initialTabIndex = 0,
});

@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);

final tabs = reactionList.map((reaction) {
final emojiDisplay = store.emojiDisplayFor(
emojiType: reaction.reactionType,
emojiCode: reaction.emojiCode,
emojiName: reaction.emojiName,
).resolve(store.userSettings);

final emoji = switch (emojiDisplay) {
UnicodeEmojiDisplay() => _UnicodeEmoji(emojiDisplay: emojiDisplay),
ImageEmojiDisplay() => _ImageEmoji(
emojiDisplay: emojiDisplay,
emojiName: reaction.emojiName,
selected: reaction.userIds.contains(store.selfUserId),
),
TextEmojiDisplay() => _TextEmoji(
emojiDisplay: emojiDisplay,
selected: reaction.userIds.contains(store.selfUserId),
),
};

return Tab(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
emoji,
const SizedBox(height: 4),
Text(
'${reaction.userIds.length}',
style: const TextStyle()
.merge(weightVariableTextStyle(context, wght: 600)),
),
],
),
);
}).toList();

final tabViews = reactionList.map((reaction) {
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: reaction.userIds.length,
itemBuilder: (context, index) {
final userId = reaction.userIds.elementAt(index);
return ListTile(
leading: Avatar(userId: userId, size: 32.0, borderRadius: 3),
title: Text(
userId == store.selfUserId
? 'You'
: store.users[userId]?.fullName ?? '(unknown user)',
style: TextStyle(
color: designVariables.foreground.withFadedAlpha(0.80),
fontSize: 17,
).merge(weightVariableTextStyle(context, wght: 500)),
),
onTap: () {
Navigator.push(
context,
ProfilePage.buildRoute(context: context, userId: userId),
);
},
);
},
);
}).toList();

return DefaultTabController(
length: tabs.length,
initialIndex: initialTabIndex,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
dividerColor: Colors.transparent,
indicator: BoxDecoration(
color: designVariables.background,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: designVariables.foreground.withFadedAlpha(0.2),
width:1
)
),
splashFactory: NoSplash.splashFactory,
indicatorSize: TabBarIndicatorSize.tab,
labelColor: designVariables.foreground,
unselectedLabelColor: designVariables.foreground,
labelStyle: const TextStyle(fontSize: 14)
.merge(weightVariableTextStyle(context, wght: 400)),
unselectedLabelStyle: const TextStyle(fontSize: 14)
.merge(weightVariableTextStyle(context, wght: 400)),
tabs: tabs,
),
),
const SizedBox(height: 8),
Flexible(
child: TabBarView(children: tabViews),
),
],
),
);
}
}
class ReactionSheetCloseButton extends StatelessWidget {
const ReactionSheetCloseButton({super.key});

@override
Widget build(BuildContext context) {
final designVariables = DesignVariables.of(context);
return TextButton(
style: TextButton.styleFrom(
minimumSize: const Size.fromHeight(44),
padding: const EdgeInsets.all(10),
foregroundColor: designVariables.contextMenuCancelText,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
splashFactory: NoSplash.splashFactory,
).copyWith(backgroundColor: WidgetStateColor.fromMap({
WidgetState.pressed: designVariables.contextMenuCancelPressedBg,
~WidgetState.pressed: designVariables.contextMenuCancelBg,
})),
onPressed: () {
Navigator.pop(context);
},
child: Text(ZulipLocalizations.of(context).dialogClose,
style: const TextStyle(fontSize: 20, height: 24 / 20)
.merge(weightVariableTextStyle(context, wght: 600))));
}
}
/// The size of a square emoji (Unicode or image).
///
/// Should be scaled by [_emojiTextScalerClamped].
Expand Down
Loading