diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf index 85f393019a..612d8a04fa 100644 Binary files a/assets/icons/ZulipIcons.ttf and b/assets/icons/ZulipIcons.ttf differ diff --git a/assets/icons/link.svg b/assets/icons/link.svg new file mode 100644 index 0000000000..0d560f15ed --- /dev/null +++ b/assets/icons/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index c24f23dce9..1d529f95f0 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -100,6 +100,10 @@ "@actionSheetOptionMarkChannelAsRead": { "description": "Label for marking a channel as read." }, + "actionSheetOptionCopyChannelLink": "Copy link to channel", + "@actionSheetOptionCopyChannelLink": { + "description": "Label for copy channel link button on action sheet." + }, "actionSheetOptionListOfTopics": "List of topics", "@actionSheetOptionListOfTopics": { "description": "Label for navigating to a channel's topic-list page." @@ -357,6 +361,10 @@ "@successMessageLinkCopied": { "description": "Message when link of a message was copied to the user's system clipboard." }, + "successChannelLinkCopied": "Channel link copied", + "@successChannelLinkCopied": { + "description": "Message when link of a channel was copied to the user's system clipboard." + }, "errorBannerDeactivatedDmLabel": "You cannot send messages to deactivated users.", "@errorBannerDeactivatedDmLabel": { "description": "Label text for error banner when sending a message to one or multiple deactivated users." diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 9668b50f26..907e29d538 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -281,6 +281,12 @@ abstract class ZulipLocalizations { /// **'Mark channel as read'** String get actionSheetOptionMarkChannelAsRead; + /// Label for copy channel link button on action sheet. + /// + /// In en, this message translates to: + /// **'Copy link to channel'** + String get actionSheetOptionCopyChannelLink; + /// Label for navigating to a channel's topic-list page. /// /// In en, this message translates to: @@ -619,6 +625,12 @@ abstract class ZulipLocalizations { /// **'Message link copied'** String get successMessageLinkCopied; + /// Message when link of a channel was copied to the user's system clipboard. + /// + /// In en, this message translates to: + /// **'Channel link copied'** + String get successChannelLinkCopied; + /// Label text for error banner when sending a message to one or multiple deactivated users. /// /// 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 110b0dbe24..96cafc3cc9 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -90,6 +90,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'List of topics'; @@ -300,6 +303,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Message link copied'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index f3b1bdad67..a016576322 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -92,6 +92,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations { String get actionSheetOptionMarkChannelAsRead => 'Kanal als gelesen markieren'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'Themenliste'; @@ -313,6 +316,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Nachrichtenlink kopiert'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'Du kannst keine Nachrichten an deaktivierte Nutzer:innen senden.'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index f99c386087..fa76f8decc 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -90,6 +90,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'List of topics'; @@ -300,6 +303,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Message link copied'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index cbc18b6d35..a0cff72bff 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -90,6 +90,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'List of topics'; @@ -300,6 +303,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Message link copied'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index 2d7d35e23e..b93ddcdb0e 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -91,6 +91,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations { @override String get actionSheetOptionMarkChannelAsRead => 'Segna il canale come letto'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'Elenco degli argomenti'; @@ -310,6 +313,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Collegamento messaggio copiato'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'Non è possibile inviare messaggi agli utenti disattivati.'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index edf5c759f9..9e5eba47f8 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -89,6 +89,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get actionSheetOptionMarkChannelAsRead => 'チャンネルを既読にする'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'トピック一覧'; @@ -297,6 +300,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Message link copied'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index 0568bc0ae7..73b5a00f0e 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -90,6 +90,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'List of topics'; @@ -300,6 +303,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Message link copied'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index c96ab24679..2861c82c8c 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -92,6 +92,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get actionSheetOptionMarkChannelAsRead => 'Oznacz kanał jako przeczytany'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'Lista wątków'; @@ -308,6 +311,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Skopiowano odnośnik wiadomości'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'Nie można wysyłać wiadomości do dezaktywowanych użytkowników.'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index be5de60e97..5fdf3c879c 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -92,6 +92,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get actionSheetOptionMarkChannelAsRead => 'Отметить канал как прочитанный'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'Список тем'; @@ -309,6 +312,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Ссылка на сообщение скопирована'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'Нельзя отправить сообщение отключенным пользователям.'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 33b4465eb6..e79d81866f 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -90,6 +90,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'List of topics'; @@ -300,6 +303,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Message link copied'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index 8d587b9085..df7ba7377e 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -90,6 +90,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations { @override String get actionSheetOptionMarkChannelAsRead => 'Označi kanal kot prebran'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'Seznam tem'; @@ -320,6 +323,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations { String get successMessageLinkCopied => 'Povezava do sporočila je bila kopirana'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'Deaktiviranim uporabnikom ne morete pošiljati sporočil.'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index 6799942531..169af2fa7e 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -93,6 +93,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get actionSheetOptionMarkChannelAsRead => 'Позначити канал як прочитаний'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'Список тем'; @@ -311,6 +314,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get successMessageLinkCopied => 'Посилання на повідомлення скопійовано'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'Ви не можете надсилати повідомлення деактивованим користувачам.'; diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index c44852c041..4b9bc3759b 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -90,6 +90,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get actionSheetOptionMarkChannelAsRead => 'Mark channel as read'; + @override + String get actionSheetOptionCopyChannelLink => 'Copy link to channel'; + @override String get actionSheetOptionListOfTopics => 'List of topics'; @@ -300,6 +303,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get successMessageLinkCopied => 'Message link copied'; + @override + String get successChannelLinkCopied => 'Channel link copied'; + @override String get errorBannerDeactivatedDmLabel => 'You cannot send messages to deactivated users.'; diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index 6b280df6ee..b77c73a55d 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -205,6 +205,9 @@ void showChannelActionSheet(BuildContext context, { MarkChannelAsReadButton(pageContext: pageContext, channelId: channelId)); } + optionButtons.add( + CopyChannelLinkButton(channelId: channelId, pageContext: pageContext)); + _showActionSheet(pageContext, optionButtons: optionButtons); } @@ -256,6 +259,34 @@ class MarkChannelAsReadButton extends ActionSheetMenuItemButton { } } +class CopyChannelLinkButton extends ActionSheetMenuItemButton { + const CopyChannelLinkButton({ + super.key, + required this.channelId, + required super.pageContext, + }); + + final int channelId; + + @override + IconData get icon => ZulipIcons.link; + + @override + String label(ZulipLocalizations zulipLocalizations) { + return zulipLocalizations.actionSheetOptionCopyChannelLink; + } + + @override + void onPressed() async { + final localizations = ZulipLocalizations.of(pageContext); + final store = PerAccountStoreWidget.of(pageContext); + + PlatformActions.copyWithPopup(context: pageContext, + successContent: Text(localizations.successChannelLinkCopied), + data: ClipboardData(text: narrowLink(store, ChannelNarrow(channelId)).toString())); + } +} + /// Show a sheet of actions you can take on a topic. /// /// Needs a [PageRoot] ancestor. @@ -1023,7 +1054,7 @@ class CopyMessageTextButton extends MessageActionSheetMenuItemButton { class CopyMessageLinkButton extends MessageActionSheetMenuItemButton { CopyMessageLinkButton({super.key, required super.message, required super.pageContext}); - @override IconData get icon => Icons.link; + @override IconData get icon => ZulipIcons.link; @override String label(ZulipLocalizations zulipLocalizations) { diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart index 1b5c424b0b..2392e054c5 100644 --- a/lib/widgets/icons.dart +++ b/lib/widgets/icons.dart @@ -111,71 +111,74 @@ abstract final class ZulipIcons { /// The Zulip custom icon "language". static const IconData language = IconData(0xf11d, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "link". + static const IconData link = IconData(0xf11e, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "lock". - static const IconData lock = IconData(0xf11e, fontFamily: "Zulip Icons"); + static const IconData lock = IconData(0xf11f, fontFamily: "Zulip Icons"); /// The Zulip custom icon "menu". - static const IconData menu = IconData(0xf11f, fontFamily: "Zulip Icons"); + static const IconData menu = IconData(0xf120, fontFamily: "Zulip Icons"); /// The Zulip custom icon "message_checked". - static const IconData message_checked = IconData(0xf120, fontFamily: "Zulip Icons"); + static const IconData message_checked = IconData(0xf121, fontFamily: "Zulip Icons"); /// The Zulip custom icon "message_feed". - static const IconData message_feed = IconData(0xf121, fontFamily: "Zulip Icons"); + static const IconData message_feed = IconData(0xf122, fontFamily: "Zulip Icons"); /// The Zulip custom icon "mute". - static const IconData mute = IconData(0xf122, fontFamily: "Zulip Icons"); + static const IconData mute = IconData(0xf123, fontFamily: "Zulip Icons"); /// The Zulip custom icon "person". - static const IconData person = IconData(0xf123, fontFamily: "Zulip Icons"); + static const IconData person = IconData(0xf124, fontFamily: "Zulip Icons"); /// The Zulip custom icon "plus". - static const IconData plus = IconData(0xf124, fontFamily: "Zulip Icons"); + static const IconData plus = IconData(0xf125, fontFamily: "Zulip Icons"); /// The Zulip custom icon "read_receipts". - static const IconData read_receipts = IconData(0xf125, fontFamily: "Zulip Icons"); + static const IconData read_receipts = IconData(0xf126, fontFamily: "Zulip Icons"); /// The Zulip custom icon "remove". - static const IconData remove = IconData(0xf126, fontFamily: "Zulip Icons"); + static const IconData remove = IconData(0xf127, fontFamily: "Zulip Icons"); /// The Zulip custom icon "search". - static const IconData search = IconData(0xf127, fontFamily: "Zulip Icons"); + static const IconData search = IconData(0xf128, fontFamily: "Zulip Icons"); /// The Zulip custom icon "send". - static const IconData send = IconData(0xf128, fontFamily: "Zulip Icons"); + static const IconData send = IconData(0xf129, fontFamily: "Zulip Icons"); /// The Zulip custom icon "settings". - static const IconData settings = IconData(0xf129, fontFamily: "Zulip Icons"); + static const IconData settings = IconData(0xf12a, fontFamily: "Zulip Icons"); /// The Zulip custom icon "share". - static const IconData share = IconData(0xf12a, fontFamily: "Zulip Icons"); + static const IconData share = IconData(0xf12b, fontFamily: "Zulip Icons"); /// The Zulip custom icon "share_ios". - static const IconData share_ios = IconData(0xf12b, fontFamily: "Zulip Icons"); + static const IconData share_ios = IconData(0xf12c, fontFamily: "Zulip Icons"); /// The Zulip custom icon "smile". - static const IconData smile = IconData(0xf12c, fontFamily: "Zulip Icons"); + static const IconData smile = IconData(0xf12d, fontFamily: "Zulip Icons"); /// The Zulip custom icon "star". - static const IconData star = IconData(0xf12d, fontFamily: "Zulip Icons"); + static const IconData star = IconData(0xf12e, fontFamily: "Zulip Icons"); /// The Zulip custom icon "star_filled". - static const IconData star_filled = IconData(0xf12e, fontFamily: "Zulip Icons"); + static const IconData star_filled = IconData(0xf12f, fontFamily: "Zulip Icons"); /// The Zulip custom icon "three_person". - static const IconData three_person = IconData(0xf12f, fontFamily: "Zulip Icons"); + static const IconData three_person = IconData(0xf130, fontFamily: "Zulip Icons"); /// The Zulip custom icon "topic". - static const IconData topic = IconData(0xf130, fontFamily: "Zulip Icons"); + static const IconData topic = IconData(0xf131, fontFamily: "Zulip Icons"); /// The Zulip custom icon "topics". - static const IconData topics = IconData(0xf131, fontFamily: "Zulip Icons"); + static const IconData topics = IconData(0xf132, fontFamily: "Zulip Icons"); /// The Zulip custom icon "two_person". - static const IconData two_person = IconData(0xf132, fontFamily: "Zulip Icons"); + static const IconData two_person = IconData(0xf133, fontFamily: "Zulip Icons"); /// The Zulip custom icon "unmute". - static const IconData unmute = IconData(0xf133, fontFamily: "Zulip Icons"); + static const IconData unmute = IconData(0xf134, fontFamily: "Zulip Icons"); // END GENERATED ICON DATA } diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart index 631856efde..28faa69ff6 100644 --- a/test/widgets/action_sheet_test.dart +++ b/test/widgets/action_sheet_test.dart @@ -243,6 +243,7 @@ void main() { check(actionSheetFinder).findsOne(); checkButton('List of topics'); checkButton('Mark channel as read'); + checkButton('Copy link to channel'); } testWidgets('show from inbox', (tester) async { @@ -342,6 +343,32 @@ void main() { expectedTitle: "Mark as read failed"); }); }); + + group('CopyChannelLinkButton', () { + setUp(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + MockClipboard().handleMethodCall, + ); + }); + + Future tapCopyChannelLinkButton(WidgetTester tester) async { + await tester.ensureVisible(find.byIcon(ZulipIcons.link, skipOffstage: false)); + await tester.tap(find.byIcon(ZulipIcons.link)); + await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e + } + + testWidgets('copies channel link to clipboard', (tester) async { + await prepare(); + final narrow = ChannelNarrow(someChannel.streamId); + await showFromAppBar(tester, narrow: narrow); + + await tapCopyChannelLinkButton(tester); + await tester.pump(Duration.zero); + final expectedLink = narrowLink(store, narrow).toString(); + check(await Clipboard.getData('text/plain')).isNotNull().text.equals(expectedLink); + }); + }); }); group('topic action sheet', () { @@ -1573,8 +1600,8 @@ void main() { }); Future tapCopyMessageLinkButton(WidgetTester tester) async { - await tester.ensureVisible(find.byIcon(Icons.link, skipOffstage: false)); - await tester.tap(find.byIcon(Icons.link)); + await tester.ensureVisible(find.byIcon(ZulipIcons.link, skipOffstage: false)); + await tester.tap(find.byIcon(ZulipIcons.link)); await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e }