From b8d36af2f60a4d93bd4ee9cf20a8f293e9998363 Mon Sep 17 00:00:00 2001 From: lakshya1goel Date: Sat, 1 Feb 2025 13:59:07 +0530 Subject: [PATCH 1/2] msglist: Update "Mark all messages as read" button icon Icon taken from the Figma design: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3512-7&t=FEydAZe5DNKVlKnV-0 Fixes: #1357 --- assets/icons/ZulipIcons.ttf | Bin 12464 -> 12720 bytes assets/icons/message_checked.svg | 4 ++++ lib/widgets/icons.dart | 29 ++++++++++++++++------------- lib/widgets/message_list.dart | 2 +- 4 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 assets/icons/message_checked.svg diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf index 0be670d5cb409c5a8b45cecef5c85e0fedfded51..a2d20d40cd286a3d8a63f48f44c32d3a362aa944 100644 GIT binary patch delta 1880 zcmb7FO-v(Y7=FL$50uWd1zKz=KP@fLmUh}sf2QT9EyQ4AL^q;F6C-v@f!(EmfGcc> zU6YL$_u%dv)+mX4Fvh3{vRpQsjq$J-_h95^%*O0N6SL97YGRBD=<^MOt4EnUedqh$ zpXYhsZ)Vu5?$w&ah{#SC$wa9qW}ly$xi#}X5i1ax-hO5_sl4#b>@6bmmxy|`)~K%S zyme}n$Qs7ppNq@e3+X>T_oKi^M8UnKdUgK%k9Te$_CAnb!bU4IPvZVI5MF9*Zhf%+ z*CCN501or=YHgx=_N#Rf1+YPv8r7{e_7dxX(~EUrrP`=_ZdMec#B)S~y0*Hpc|$q9 zOO(8fb@2=%yjucOg{@q6G!BHe9qVw<V!58p@eYnplaJquD*|dh^ayCUq~p{!-18ywG9rZVjwkeK2GFyJbyPnde%d7} zW33=lLcJLBc;*PAoBV_x18oGfDTE!Lr&lk?OY`T$g2#rZ^M^4;kJHoCjWbjsFJr{V zA>%|CpQj%J{_D?S%{@L;;nOQq5AMp-x+;F6Zk&@9jv`!K=`h?G*eE1Opm{XF49Ifm zprFU%9TDcW>*g{W*6y4Af|InM#jklSTbj#a*50)=lMnP)kLU;Vffo=1F`*M;4?uAf z>o)W+;F_Z|wEb{cD_KM7I4BDsNkK4( zZ>{^0TgWEVTq=#6d{K^#m!cV^JT_99(5fw$nxMp|1Ya%}|HWSgm@^QEts02Ho;H9l z9n}nEVdo7bVCx1{*aZVQ*hK>=*d+sL0{?v)JA_cM#ld8jYtQB_%sP-{LvZyo2EV)7nf5cK z!{_Tb`r6a8BDD*Ho*rM=>mEw`4}ba0=JPt-rfWwBQb@Wc`NHBhYnSpNpXqF;+ZXcn zx)0x35qsS`yuePkGU)l#X0dfi!aaXFb95kz?};IwcmX-S5bfXX|ILB_FJH(P1vaIX ak_RqlqrS0GU98X5mg=>0^?A*i`tWb{n+?za delta 1645 zcmb7EOK%%h7(Fwwoy3p04Jm{^Qj-)v;u+gBvB$2R*cJ;Ag$hJeRHTqHIB`=EHz5rr zK`2x4528UrLXlv>f(5Dqk*Wxl1yWXQC_5GiRaJ{3R3X>^iy}Dp`i8Q_qvLZQ=X>6n zyWQG$J!k+TxP~4So|!)P)R{k?+Xl1>&@=bKbl!gX*7WBm$M`{mW^1$Bp8mZ4j=rUTs{e=p!WcyrFG6Sa zX+9Z@BTLR9j{+LK8ilq@;3$$DJxWuPAsiXp!XpTxLaRJihB!7+W{?Js(8gpM8KxYh0kdxJv$P*Yu|G(=8ISaQ5j3G*On!z2~)MQ#VotijB z_ZhAT`w#}0d=*dQCCspsqE{(e9rVc)tukbon&0o~)okQI+xtTP5V((-iFw z@kueXXnKkVAX4g@hB+2Z6mFtSL{vN}yP06kQO?uyc*JQ>q0YHQ%NYG-XeTVksrJMP zJ;typhRsvfJ+AL%~B-QYaT0(3qXa0%1}_dj6VUD17H1wm#B + + + diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart index 82cb83704b..c3a68085c7 100644 --- a/lib/widgets/icons.dart +++ b/lib/widgets/icons.dart @@ -99,44 +99,47 @@ abstract final class ZulipIcons { /// The Zulip custom icon "menu". static const IconData menu = IconData(0xf119, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "message_checked". + static const IconData message_checked = IconData(0xf11a, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "message_feed". - static const IconData message_feed = IconData(0xf11a, fontFamily: "Zulip Icons"); + static const IconData message_feed = IconData(0xf11b, fontFamily: "Zulip Icons"); /// The Zulip custom icon "mute". - static const IconData mute = IconData(0xf11b, fontFamily: "Zulip Icons"); + static const IconData mute = IconData(0xf11c, fontFamily: "Zulip Icons"); /// The Zulip custom icon "read_receipts". - static const IconData read_receipts = IconData(0xf11c, fontFamily: "Zulip Icons"); + static const IconData read_receipts = IconData(0xf11d, fontFamily: "Zulip Icons"); /// The Zulip custom icon "send". - static const IconData send = IconData(0xf11d, fontFamily: "Zulip Icons"); + static const IconData send = IconData(0xf11e, fontFamily: "Zulip Icons"); /// The Zulip custom icon "share". - static const IconData share = IconData(0xf11e, fontFamily: "Zulip Icons"); + static const IconData share = IconData(0xf11f, fontFamily: "Zulip Icons"); /// The Zulip custom icon "share_ios". - static const IconData share_ios = IconData(0xf11f, fontFamily: "Zulip Icons"); + static const IconData share_ios = IconData(0xf120, fontFamily: "Zulip Icons"); /// The Zulip custom icon "smile". - static const IconData smile = IconData(0xf120, fontFamily: "Zulip Icons"); + static const IconData smile = IconData(0xf121, fontFamily: "Zulip Icons"); /// The Zulip custom icon "star". - static const IconData star = IconData(0xf121, fontFamily: "Zulip Icons"); + static const IconData star = IconData(0xf122, fontFamily: "Zulip Icons"); /// The Zulip custom icon "star_filled". - static const IconData star_filled = IconData(0xf122, fontFamily: "Zulip Icons"); + static const IconData star_filled = IconData(0xf123, fontFamily: "Zulip Icons"); /// The Zulip custom icon "three_person". - static const IconData three_person = IconData(0xf123, fontFamily: "Zulip Icons"); + static const IconData three_person = IconData(0xf124, fontFamily: "Zulip Icons"); /// The Zulip custom icon "topic". - static const IconData topic = IconData(0xf124, fontFamily: "Zulip Icons"); + static const IconData topic = IconData(0xf125, fontFamily: "Zulip Icons"); /// The Zulip custom icon "unmute". - static const IconData unmute = IconData(0xf125, fontFamily: "Zulip Icons"); + static const IconData unmute = IconData(0xf126, fontFamily: "Zulip Icons"); /// The Zulip custom icon "user". - static const IconData user = IconData(0xf126, fontFamily: "Zulip Icons"); + static const IconData user = IconData(0xf127, fontFamily: "Zulip Icons"); // END GENERATED ICON DATA } diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 48c0d70b59..26460781f2 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -852,7 +852,7 @@ class _MarkAsReadWidgetState extends State { backgroundColor: WidgetStatePropertyAll(messageListTheme.unreadMarker), ), onPressed: _loading ? null : () => _handlePress(context), - icon: const Icon(Icons.playlist_add_check), + icon: const Icon(ZulipIcons.message_checked), label: Text(zulipLocalizations.markAllAsReadLabel)))))); } } From 94e5ebee2ff768565030ea2ad27c7eb6e06f2789 Mon Sep 17 00:00:00 2001 From: lakshya1goel Date: Fri, 21 Feb 2025 22:27:26 +0530 Subject: [PATCH 2/2] action_sheet: Add "Mark Topic As Read" button Fixes: #1225 --- assets/l10n/app_en.arb | 4 ++ lib/generated/l10n/zulip_localizations.dart | 6 +++ .../l10n/zulip_localizations_ar.dart | 3 ++ .../l10n/zulip_localizations_en.dart | 3 ++ .../l10n/zulip_localizations_ja.dart | 3 ++ .../l10n/zulip_localizations_nb.dart | 3 ++ .../l10n/zulip_localizations_pl.dart | 3 ++ .../l10n/zulip_localizations_ru.dart | 3 ++ .../l10n/zulip_localizations_sk.dart | 3 ++ lib/widgets/action_sheet.dart | 31 ++++++++++++++ test/widgets/action_sheet_test.dart | 42 +++++++++++++++++++ 11 files changed, 104 insertions(+) diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 118ab83c70..9bc14daf7c 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -136,6 +136,10 @@ "@actionSheetOptionUnstarMessage": { "description": "Label for unstar button on action sheet." }, + "actionSheetOptionMarkTopicAsRead": "Mark topic as read", + "@actionSheetOptionMarkTopicAsRead": { + "description": "Option to mark a specific topic as read in the action sheet." + }, "errorWebAuthOperationalErrorTitle": "Something went wrong", "@errorWebAuthOperationalErrorTitle": { "description": "Error title when third-party authentication has an operational error (not necessarily caused by invalid credentials)." diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 9579683908..2756fe632e 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -309,6 +309,12 @@ abstract class ZulipLocalizations { /// **'Unstar message'** String get actionSheetOptionUnstarMessage; + /// Option to mark a specific topic as read in the action sheet. + /// + /// In en, this message translates to: + /// **'Mark topic as read'** + String get actionSheetOptionMarkTopicAsRead; + /// Error title when third-party authentication has an operational error (not necessarily caused by invalid credentials). /// /// In en, this message translates to: diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart index 71bf06d8ce..70d76b017f 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -112,6 +112,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get actionSheetOptionUnstarMessage => 'Unstar message'; + @override + String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read'; + @override String get errorWebAuthOperationalErrorTitle => 'Something went wrong'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 7a33e33567..d926cdb0c1 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -112,6 +112,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get actionSheetOptionUnstarMessage => 'Unstar message'; + @override + String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read'; + @override String get errorWebAuthOperationalErrorTitle => 'Something went wrong'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index 137883e5e9..c2f5692864 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -112,6 +112,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get actionSheetOptionUnstarMessage => 'Unstar message'; + @override + String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read'; + @override String get errorWebAuthOperationalErrorTitle => 'Something went wrong'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index 3dec7d9b5a..13fa7ea71a 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -112,6 +112,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get actionSheetOptionUnstarMessage => 'Unstar message'; + @override + String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read'; + @override String get errorWebAuthOperationalErrorTitle => 'Something went wrong'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index 83a777bfc4..f074d87acc 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -112,6 +112,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get actionSheetOptionUnstarMessage => 'Odbierz gwiazdkę'; + @override + String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read'; + @override String get errorWebAuthOperationalErrorTitle => 'Coś poszło nie tak'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 827aaf0155..848075052a 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -112,6 +112,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get actionSheetOptionUnstarMessage => 'Снять отметку с сообщения'; + @override + String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read'; + @override String get errorWebAuthOperationalErrorTitle => 'Что-то пошло не так'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 38a3f8a240..c013ab3886 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -112,6 +112,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get actionSheetOptionUnstarMessage => 'Odhviezdičkovať správu'; + @override + String get actionSheetOptionMarkTopicAsRead => 'Mark topic as read'; + @override String get errorWebAuthOperationalErrorTitle => 'Niečo sa pokazilo'; diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index e943d7eb36..fb10e8d185 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -255,6 +255,14 @@ void showTopicActionSheet(BuildContext context, { someMessageIdInTopic: someMessageIdInTopic)); } + final unreadCount = store.unreads.countInTopicNarrow(channelId, topic); + if (unreadCount > 0) { + optionButtons.add(MarkTopicAsReadButton( + channelId: channelId, + topic: topic, + pageContext: context)); + } + if (optionButtons.isEmpty) { // TODO(a11y): This case makes a no-op gesture handler; as a consequence, // we're presenting some UI (to people who use screen-reader software) as @@ -461,6 +469,29 @@ class ResolveUnresolveButton extends ActionSheetMenuItemButton { } } +class MarkTopicAsReadButton extends ActionSheetMenuItemButton { + const MarkTopicAsReadButton({ + super.key, + required this.channelId, + required this.topic, + required super.pageContext, + }); + + final int channelId; + final TopicName topic; + + @override IconData get icon => ZulipIcons.message_checked; + + @override + String label(ZulipLocalizations zulipLocalizations) { + return zulipLocalizations.actionSheetOptionMarkTopicAsRead; + } + + @override void onPressed() async { + await ZulipAction.markNarrowAsRead(pageContext, TopicNarrow(channelId, topic)); + } +} + /// Show a sheet of actions you can take on a message in the message list. /// /// Must have a [MessageListPage] ancestor. diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart index 5a4c22c604..286b26d3ee 100644 --- a/test/widgets/action_sheet_test.dart +++ b/test/widgets/action_sheet_test.dart @@ -10,6 +10,7 @@ import 'package:http/http.dart' as http; import 'package:zulip/api/model/events.dart'; import 'package:zulip/api/model/initial_snapshot.dart'; import 'package:zulip/api/model/model.dart'; +import 'package:zulip/api/model/narrow.dart'; import 'package:zulip/api/route/channels.dart'; import 'package:zulip/api/route/messages.dart'; import 'package:zulip/model/binding.dart'; @@ -562,6 +563,47 @@ void main() { expectedTitle: 'Failed to mark topic as unresolved'); }); }); + + group('MarkTopicAsReadButton', () { + testWidgets('visible if topic has unread messages', (tester) async { + await prepare(); + final message = eg.streamMessage(stream: someChannel, topic: someTopic, flags: []); + await store.addMessage(message); + await showFromAppBar(tester, messages: [message]); + check(find.text('Mark topic as read')).findsOne(); + }); + + testWidgets('not visible if topic has no unread messages', (tester) async { + await prepare(); + final message = eg.streamMessage(stream: someChannel, topic: someTopic, flags: [MessageFlag.read]); + await store.addMessage(message); + await showFromAppBar(tester, messages: [message]); + check(find.text('Mark topic as read')).findsNothing(); + }); + + testWidgets('marks topic as read when pressed', (tester) async { + await prepare(); + final message = eg.streamMessage(stream: someChannel, topic: someTopic); + await store.handleEvent(MessageEvent(id: 0, message: message)); + await showFromAppBar(tester, messages: [message]); + + connection.prepare(json: UpdateMessageFlagsForNarrowResult( + processedCount: 1, updatedCount: 1, + firstProcessedId: message.id, lastProcessedId: message.id, + foundOldest: true, foundNewest: true).toJson()); + await tester.tap(find.text('Mark topic as read')); + await tester.pumpAndSettle(); + + check(connection.lastRequest).isA() + ..url.path.equals('/api/v1/messages/flags/narrow') + ..bodyFields['narrow'].equals(jsonEncode([ + ...eg.topicNarrow(someChannel.streamId, someTopic).apiEncode(), + ApiNarrowIs(IsOperand.unread), + ])) + ..bodyFields['op'].equals('add') + ..bodyFields['flag'].equals('read'); + }); + }); }); group('message action sheet', () {