From 153bc52510134539f3240253e962d0b6a57a4f13 Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Thu, 11 Dec 2025 21:52:31 +0530 Subject: [PATCH 1/3] store: Add refreshRealmMetadata A helper that attempts to update the realm metadata (name and icon) for each account. --- lib/model/store.dart | 58 ++++++++++++++++++++++++++ test/model/store_test.dart | 84 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/lib/model/store.dart b/lib/model/store.dart index b051e75b72..e1784ba36e 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -345,6 +345,64 @@ abstract class GlobalStore extends ChangeNotifier { )); } + /// Attempt to refresh the realm metadata for each account. + /// + /// Fetches server settings for each account and then calls [updateRealmData] + /// to update the realm metadata (name and icon). + /// + /// The updation for some accounts may be skipped if: + /// - The [PerAccountStore] for the account is already loaded or is loading. + /// - The version of corresponding realm server is unsupported. + /// + /// Returns immediately; the fetches and updates are done asynchronously. + void refreshRealmMetadata() { + for (final accountId in accountIds) { + // Avoid updating realm metadata if per account store has already + // loaded or is being loaded. It should be updated with data from + // the initial snapshot or realm update events. + if (_perAccountStoresLoading.containsKey(accountId)) continue; + if (_perAccountStores.containsKey(accountId)) continue; + + final account = getAccount(accountId); + if (account == null) continue; + + // Fetch the server settings and update the realm data without awaiting. + // This allows fetching server settings of all the accounts parallelly. + unawaited(() async { + final GetServerSettingsResult serverSettings; + final connection = apiConnection( + realmUrl: account.realmUrl, + zulipFeatureLevel: null); + try { + serverSettings = await getServerSettings(connection); + final zulipVersionData = ZulipVersionData.fromServerSettings(serverSettings); + if (zulipVersionData.isUnsupported) { + throw ServerVersionUnsupportedException(zulipVersionData); + } + } on MalformedServerResponseException catch (e) { + final zulipVersionData = ZulipVersionData.fromMalformedServerResponseException(e); + if (zulipVersionData != null && zulipVersionData.isUnsupported) { + throw ServerVersionUnsupportedException(zulipVersionData); + } + rethrow; + } finally { + connection.close(); + } + + // Account got logged out while fetching server settings. + if (getAccount(accountId) == null) return; + + if (_perAccountStoresLoading.containsKey(accountId)) return; + if (_perAccountStores.containsKey(accountId)) return; + + await updateRealmData( + accountId, + realmName: serverSettings.realmName, + realmIcon: serverSettings.realmIcon); + }()); + } + } + /// Update an account in the underlying data store. Future doUpdateAccount(int accountId, AccountsCompanion data); diff --git a/test/model/store_test.dart b/test/model/store_test.dart index c0ab76e1bb..52d7dd2d88 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -391,6 +391,90 @@ void main() { realmIcon: Value(Uri.parse('/image-b.png')))); }); + group('GlobalStore.refreshRealmMetadata', () { + test('smoke; populates/updates realm data', () => awaitFakeAsync((async) async { + final account1 = eg.selfAccount.copyWith( + realmUrl: Uri.parse('https://realm1.example.com'), + realmName: const Value.absent(), // account without realm metadata + realmIcon: const Value.absent()); + final account2 = eg.otherAccount.copyWith( + realmUrl: Uri.parse('https://realm2.example.com'), + realmName: Value('Old realm 2 name'), // account with old realm metadata + realmIcon: Value(Uri.parse('/old-realm-2-image.png'))); + + final globalStore = eg.globalStore(accounts: [account1, account2]); + globalStore.useCachedApiConnections = true; + + final connection1 = globalStore.apiConnection( + realmUrl: account1.realmUrl, zulipFeatureLevel: null); + final serverSettings1 = eg.serverSettings( + realmUrl: account1.realmUrl, + realmName: 'Realm 1 name', + realmIcon: Uri.parse('/realm-1-image.png')); + connection1.prepare(json: serverSettings1.toJson()); + + final connection2 = globalStore.apiConnection( + realmUrl: account2.realmUrl, zulipFeatureLevel: null); + final serverSettings2 = eg.serverSettings( + realmUrl: account2.realmUrl, + realmName: 'New realm 2 name', + realmIcon: Uri.parse('/new-realm-2-image.png')); + connection2.prepare(json: serverSettings2.toJson()); + + globalStore.refreshRealmMetadata(); + async.elapse(Duration.zero); + + check(globalStore.getAccount(account1.id)).isNotNull() + ..realmName.equals('Realm 1 name') + ..realmIcon.equals(Uri.parse('/realm-1-image.png')); + check(globalStore.getAccount(account2.id)).isNotNull() + ..realmName.equals('New realm 2 name') + ..realmIcon.equals(Uri.parse('/new-realm-2-image.png')); + })); + + test('ignores per account store loaded account', () => awaitFakeAsync((async) async { + final account1 = eg.selfAccount.copyWith( + realmUrl: Uri.parse('https://realm1.example.com'), + realmName: Value('Old realm 1 name'), + realmIcon: Value(Uri.parse('/old-realm-1-image.png'))); + final account2 = eg.otherAccount.copyWith( + realmUrl: Uri.parse('https://realm2.example.com'), + realmName: Value('Old realm 2 name'), + realmIcon: Value(Uri.parse('/old-realm-2-image.png'))); + + final globalStore = eg.globalStore(); + await globalStore.add(account1, eg.initialSnapshot( + realmName: account1.realmName, + realmIconUrl: account1.realmIcon, + realmUsers: [eg.selfUser])); + await globalStore.add(account2, eg.initialSnapshot( + realmName: account2.realmName, + realmIconUrl: account2.realmIcon, + realmUsers: [eg.otherUser])); + globalStore.useCachedApiConnections = true; + + final connection1 = globalStore.apiConnection( + realmUrl: account1.realmUrl, zulipFeatureLevel: null); + final serverSettings1 = eg.serverSettings( + realmUrl: account1.realmUrl, + realmName: 'New realm 1 name', + realmIcon: Uri.parse('/new-realm-1-image.png')); + connection1.prepare(json: serverSettings1.toJson()); + + await globalStore.perAccount(account2.id); + + globalStore.refreshRealmMetadata(); + async.elapse(Duration.zero); + + check(globalStore.getAccount(account1.id)).isNotNull() + ..realmName.equals('New realm 1 name') + ..realmIcon.equals(Uri.parse('/new-realm-1-image.png')); + check(globalStore.getAccount(account2.id)).isNotNull() + ..realmName.equals('Old realm 2 name') + ..realmIcon.equals(Uri.parse('/old-realm-2-image.png')); + })); + }); + group('GlobalStore.removeAccount', () { void checkGlobalStore(GlobalStore store, int accountId, { required bool expectAccount, From 47c5a8f494145a6cb53800e9e1516b6feb46e6c2 Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Mon, 29 Sep 2025 23:18:36 +0530 Subject: [PATCH 2/3] share: Re-design from page to a dialog Figma: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=12853-76543&p=f&t=oBRXWxFjbkz1yeI7-0 --- assets/l10n/app_de.arb | 4 - assets/l10n/app_en.arb | 4 - assets/l10n/app_fr.arb | 4 - assets/l10n/app_ja.arb | 4 - assets/l10n/app_pl.arb | 4 - assets/l10n/app_ru.arb | 4 - assets/l10n/app_sl.arb | 4 - assets/l10n/app_uk.arb | 4 - assets/l10n/app_zh_Hans_CN.arb | 4 - assets/l10n/app_zh_Hant_TW.arb | 4 - lib/generated/l10n/zulip_localizations.dart | 6 - .../l10n/zulip_localizations_ar.dart | 3 - .../l10n/zulip_localizations_de.dart | 3 - .../l10n/zulip_localizations_el.dart | 3 - .../l10n/zulip_localizations_en.dart | 3 - .../l10n/zulip_localizations_es.dart | 3 - .../l10n/zulip_localizations_fr.dart | 3 - .../l10n/zulip_localizations_he.dart | 3 - .../l10n/zulip_localizations_hu.dart | 3 - .../l10n/zulip_localizations_it.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 - .../l10n/zulip_localizations_sl.dart | 3 - .../l10n/zulip_localizations_uk.dart | 3 - .../l10n/zulip_localizations_zh.dart | 9 -- lib/widgets/share.dart | 132 ++++++++++++++---- 29 files changed, 104 insertions(+), 131 deletions(-) diff --git a/assets/l10n/app_de.arb b/assets/l10n/app_de.arb index da489ff68e..18e84e2e42 100644 --- a/assets/l10n/app_de.arb +++ b/assets/l10n/app_de.arb @@ -961,9 +961,6 @@ "@settingsPageTitle": { "description": "Title for the settings page." }, - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "@signInWithFoo": { "description": "Button to use {method} to sign in to the app.", "placeholders": { @@ -1469,7 +1466,6 @@ "serverUrlValidationErrorUnsupportedScheme": "Die Server-URL muss mit http:// oder https:// beginnen.", "setStatusPageTitle": "Status setzen", "settingsPageTitle": "Einstellungen", - "sharePageTitle": "Teilen", "signInWithFoo": "Anmelden mit {method}", "snackBarDetails": "Details", "spoilerDefaultHeaderText": "Spoiler", diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index f142c14077..ddae45df1b 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -1220,10 +1220,6 @@ "allChannelsPageTitle": {"type": "String", "example": "All channels"} } }, - "sharePageTitle": "Share", - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "mainMenuMyProfile": "My profile", "@mainMenuMyProfile": { "description": "Label for main-menu button leading to the user's own profile." diff --git a/assets/l10n/app_fr.arb b/assets/l10n/app_fr.arb index 8a4fa23021..58cb6e17b1 100644 --- a/assets/l10n/app_fr.arb +++ b/assets/l10n/app_fr.arb @@ -490,9 +490,6 @@ "@settingsPageTitle": { "description": "Title for the settings page." }, - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "@starredMessagesPageTitle": { "description": "Page title for the 'Starred messages' message view." }, @@ -730,7 +727,6 @@ "seeWhoReactedSheetUserListLabel": "Votes pour {emojiName} ({num})", "setStatusPageTitle": "Définir statut", "settingsPageTitle": "Paramètres", - "sharePageTitle": "Partager", "starredMessagesPageTitle": "Messages favoris", "statusButtonLabelStatusSet": "Statut", "statusButtonLabelStatusUnset": "Définir mon statut", diff --git a/assets/l10n/app_ja.arb b/assets/l10n/app_ja.arb index 5a270b00e5..836dbc3ccd 100644 --- a/assets/l10n/app_ja.arb +++ b/assets/l10n/app_ja.arb @@ -925,9 +925,6 @@ "@settingsPageTitle": { "description": "Title for the settings page." }, - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "@signInWithFoo": { "description": "Button to use {method} to sign in to the app.", "placeholders": { @@ -1408,7 +1405,6 @@ "serverUrlValidationErrorUnsupportedScheme": "サーバーURLは http:// または https:// で始まる必要があります。", "setStatusPageTitle": "ステータスの設定", "settingsPageTitle": "設定", - "sharePageTitle": "共有", "signInWithFoo": "{method}でログイン", "snackBarDetails": "詳細", "spoilerDefaultHeaderText": "内容を隠す", diff --git a/assets/l10n/app_pl.arb b/assets/l10n/app_pl.arb index 1f3fba52c2..dc710448b3 100644 --- a/assets/l10n/app_pl.arb +++ b/assets/l10n/app_pl.arb @@ -991,9 +991,6 @@ "@settingsPageTitle": { "description": "Title for the settings page." }, - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "@signInWithFoo": { "description": "Button to use {method} to sign in to the app.", "placeholders": { @@ -1507,7 +1504,6 @@ "serverUrlValidationErrorUnsupportedScheme": "Adres URL serwera musi zaczynać się od http:// or https://.", "setStatusPageTitle": "Ustaw stan", "settingsPageTitle": "Ustawienia", - "sharePageTitle": "Udostępnij", "signInWithFoo": "Logowanie z {method}", "snackBarDetails": "Szczegóły", "spoilerDefaultHeaderText": "Spoiler", diff --git a/assets/l10n/app_ru.arb b/assets/l10n/app_ru.arb index 3a5e39b010..5525c13bb9 100644 --- a/assets/l10n/app_ru.arb +++ b/assets/l10n/app_ru.arb @@ -991,9 +991,6 @@ "@settingsPageTitle": { "description": "Title for the settings page." }, - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "@signInWithFoo": { "description": "Button to use {method} to sign in to the app.", "placeholders": { @@ -1507,7 +1504,6 @@ "serverUrlValidationErrorUnsupportedScheme": "URL-адрес сервера должен начинаться с http:// или https://.", "setStatusPageTitle": "Установить статус", "settingsPageTitle": "Настройки", - "sharePageTitle": "Поделиться", "signInWithFoo": "Войти с помощью {method}", "snackBarDetails": "Подробности", "spoilerDefaultHeaderText": "Спойлер", diff --git a/assets/l10n/app_sl.arb b/assets/l10n/app_sl.arb index bdf9190eac..3631386a16 100644 --- a/assets/l10n/app_sl.arb +++ b/assets/l10n/app_sl.arb @@ -991,9 +991,6 @@ "@settingsPageTitle": { "description": "Title for the settings page." }, - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "@signInWithFoo": { "description": "Button to use {method} to sign in to the app.", "placeholders": { @@ -1507,7 +1504,6 @@ "serverUrlValidationErrorUnsupportedScheme": "URL strežnika se mora začeti s http:// ali https://.", "setStatusPageTitle": "Nastavi status", "settingsPageTitle": "Nastavitve", - "sharePageTitle": "Deli", "signInWithFoo": "Prijava z {method}", "snackBarDetails": "Podrobnosti", "spoilerDefaultHeaderText": "Skrito", diff --git a/assets/l10n/app_uk.arb b/assets/l10n/app_uk.arb index 3be6b66351..e0af51be74 100644 --- a/assets/l10n/app_uk.arb +++ b/assets/l10n/app_uk.arb @@ -991,9 +991,6 @@ "@settingsPageTitle": { "description": "Title for the settings page." }, - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "@signInWithFoo": { "description": "Button to use {method} to sign in to the app.", "placeholders": { @@ -1507,7 +1504,6 @@ "serverUrlValidationErrorUnsupportedScheme": "URL-адреса сервера має починатися з http:// або https://.", "setStatusPageTitle": "Встановити статус", "settingsPageTitle": "Налаштування", - "sharePageTitle": "Поділитися", "signInWithFoo": "Увійти з {method}", "snackBarDetails": "Деталі", "spoilerDefaultHeaderText": "Спойлер", diff --git a/assets/l10n/app_zh_Hans_CN.arb b/assets/l10n/app_zh_Hans_CN.arb index d3aed361f8..5ec095deda 100644 --- a/assets/l10n/app_zh_Hans_CN.arb +++ b/assets/l10n/app_zh_Hans_CN.arb @@ -950,9 +950,6 @@ "description": "Title for the 'Set status' page." }, "@settingsPageTitle": {}, - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "@signInWithFoo": { "description": "Button to use {method} to sign in to the app.", "placeholders": { @@ -1452,7 +1449,6 @@ "serverUrlValidationErrorUnsupportedScheme": "服务器网址必须以 http:// 或 https:// 开头。", "setStatusPageTitle": "设定状态", "settingsPageTitle": "设置", - "sharePageTitle": "分享", "signInWithFoo": "使用{method}登入", "snackBarDetails": "详情", "spoilerDefaultHeaderText": "剧透", diff --git a/assets/l10n/app_zh_Hant_TW.arb b/assets/l10n/app_zh_Hant_TW.arb index 8639296119..d2f804dfc4 100644 --- a/assets/l10n/app_zh_Hant_TW.arb +++ b/assets/l10n/app_zh_Hant_TW.arb @@ -962,9 +962,6 @@ "description": "Title for the 'Set status' page." }, "@settingsPageTitle": {}, - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." - }, "@signInWithFoo": { "description": "Button to use {method} to sign in to the app.", "placeholders": { @@ -1471,7 +1468,6 @@ "serverUrlValidationErrorUnsupportedScheme": "伺服器 URL 必須以 http:// 或 https:// 開頭。", "setStatusPageTitle": "設定狀態", "settingsPageTitle": "設定", - "sharePageTitle": "分享", "signInWithFoo": "使用 {method} 登入", "snackBarDetails": "詳細資訊", "spoilerDefaultHeaderText": "劇透", diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index e4c469d7c3..8a225eb3b7 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -1785,12 +1785,6 @@ abstract class ZulipLocalizations { /// **'Try going to {allChannelsPageTitle} and joining some of them.'** String channelsEmptyPlaceholderMessage(String allChannelsPageTitle); - /// Title for the page about sharing content received from other apps. - /// - /// In en, this message translates to: - /// **'Share'** - String get sharePageTitle; - /// Label for main-menu button leading to the user's own profile. /// /// 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 a2afe093b0..ec0e099da0 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -1025,9 +1025,6 @@ class ZulipLocalizationsAr extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index fcc5c8d3fa..a1e911ef47 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -1044,9 +1044,6 @@ class ZulipLocalizationsDe extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Teilen'; - @override String get mainMenuMyProfile => 'Mein Profil'; diff --git a/lib/generated/l10n/zulip_localizations_el.dart b/lib/generated/l10n/zulip_localizations_el.dart index 754866cb02..0a241959ef 100644 --- a/lib/generated/l10n/zulip_localizations_el.dart +++ b/lib/generated/l10n/zulip_localizations_el.dart @@ -1025,9 +1025,6 @@ class ZulipLocalizationsEl extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index f5809fc10d..6e8c670916 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -1025,9 +1025,6 @@ class ZulipLocalizationsEn extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_es.dart b/lib/generated/l10n/zulip_localizations_es.dart index 6bdd3973c3..dda88b329d 100644 --- a/lib/generated/l10n/zulip_localizations_es.dart +++ b/lib/generated/l10n/zulip_localizations_es.dart @@ -1025,9 +1025,6 @@ class ZulipLocalizationsEs extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index 7adbcb8303..b7815c99bf 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -1041,9 +1041,6 @@ class ZulipLocalizationsFr extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Partager'; - @override String get mainMenuMyProfile => 'Mon profil'; diff --git a/lib/generated/l10n/zulip_localizations_he.dart b/lib/generated/l10n/zulip_localizations_he.dart index c28f06c4e5..55b9c286b8 100644 --- a/lib/generated/l10n/zulip_localizations_he.dart +++ b/lib/generated/l10n/zulip_localizations_he.dart @@ -1025,9 +1025,6 @@ class ZulipLocalizationsHe extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_hu.dart b/lib/generated/l10n/zulip_localizations_hu.dart index 43ac20a859..53bc8e6bcf 100644 --- a/lib/generated/l10n/zulip_localizations_hu.dart +++ b/lib/generated/l10n/zulip_localizations_hu.dart @@ -1025,9 +1025,6 @@ class ZulipLocalizationsHu extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index 5b45be5f62..d3ed6a98f6 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -1038,9 +1038,6 @@ class ZulipLocalizationsIt extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'Il mio profilo'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index a54686e873..f0f30e7980 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -1000,9 +1000,6 @@ class ZulipLocalizationsJa extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => '共有'; - @override String get mainMenuMyProfile => '自分のプロフィール'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index cd13ec9ab0..38e142fcd0 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -1025,9 +1025,6 @@ class ZulipLocalizationsNb extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index 036846caf4..d64f57e0bd 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -1041,9 +1041,6 @@ class ZulipLocalizationsPl extends ZulipLocalizations { return 'Spróbuj skorzystać z $allChannelsPageTitle i dołączyć do nich.'; } - @override - String get sharePageTitle => 'Udostępnij'; - @override String get mainMenuMyProfile => 'Mój profil'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 5f54d02d32..e77e313a66 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -1052,9 +1052,6 @@ class ZulipLocalizationsRu extends ZulipLocalizations { return 'Вы можете просмотреть $allChannelsPageTitle и присоединиться к некоторым из них.'; } - @override - String get sharePageTitle => 'Поделиться'; - @override String get mainMenuMyProfile => 'Мой профиль'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 39dca858c8..09113358e8 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -1027,9 +1027,6 @@ class ZulipLocalizationsSk extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'Môj profil'; diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index abcfb90899..6b8c80801a 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -1060,9 +1060,6 @@ class ZulipLocalizationsSl extends ZulipLocalizations { return 'Poskusite odpreti $allChannelsPageTitle in se jim pridružiti.'; } - @override - String get sharePageTitle => 'Deli'; - @override String get mainMenuMyProfile => 'Moj profil'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index 0a70fc9b7f..cc6c0d2370 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -1042,9 +1042,6 @@ class ZulipLocalizationsUk extends ZulipLocalizations { return 'Спробуйте перейти за посиланням $allChannelsPageTitle та приєднатися до деяких із них.'; } - @override - String get sharePageTitle => 'Поділитися'; - @override String get mainMenuMyProfile => 'Мій профіль'; diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index bac342c0c1..7337340375 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -1025,9 +1025,6 @@ class ZulipLocalizationsZh extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } - @override - String get sharePageTitle => 'Share'; - @override String get mainMenuMyProfile => 'My profile'; @@ -2122,9 +2119,6 @@ class ZulipLocalizationsZhHansCn extends ZulipLocalizationsZh { @override String get channelsPageTitle => '频道'; - @override - String get sharePageTitle => '分享'; - @override String get mainMenuMyProfile => '个人资料'; @@ -3230,9 +3224,6 @@ class ZulipLocalizationsZhHantTw extends ZulipLocalizationsZh { @override String get channelsPageTitle => '頻道'; - @override - String get sharePageTitle => '分享'; - @override String get mainMenuMyProfile => '我的設定檔'; diff --git a/lib/widgets/share.dart b/lib/widgets/share.dart index df776e4d1e..1c50ae40a0 100644 --- a/lib/widgets/share.dart +++ b/lib/widgets/share.dart @@ -11,15 +11,18 @@ import '../log.dart'; import '../model/binding.dart'; import '../model/narrow.dart'; import 'app.dart'; -import 'color.dart'; import 'compose_box.dart'; import 'dialog.dart'; +import 'icons.dart'; +import 'image.dart'; import 'message_list.dart'; import 'page.dart'; import 'recent_dm_conversations.dart'; import 'store.dart'; import 'subscription_list.dart'; +import 'text.dart'; import 'theme.dart'; +import 'user.dart'; // Responds to receiving shared content from other apps. class ShareService { @@ -99,16 +102,20 @@ class ShareService { mimeType: mimeType); }); - unawaited(navigator.push( - SharePage.buildRoute( - accountId: accountId, - sharedFiles: sharedFiles, - sharedText: intentSendEvent.extraText))); + ShareSheet.show( + pageContext: context, + initialAccountId: accountId, + sharedFiles: sharedFiles, + sharedText: intentSendEvent.extraText); } } -class SharePage extends StatelessWidget { - const SharePage({ +/// The Share-to-Zulip sheet. +/// +/// Figma link: +/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=12853-76543&p=f&t=oBRXWxFjbkz1yeI7-0 +class ShareSheet extends StatelessWidget { + const ShareSheet({ super.key, required this.sharedFiles, required this.sharedText, @@ -117,16 +124,34 @@ class SharePage extends StatelessWidget { final Iterable? sharedFiles; final String? sharedText; - static AccountRoute buildRoute({ - required int accountId, + static void show({ + required BuildContext pageContext, + required int initialAccountId, required Iterable? sharedFiles, required String? sharedText, - }) { - return MaterialAccountWidgetRoute( - accountId: accountId, - page: SharePage( - sharedFiles: sharedFiles, - sharedText: sharedText)); + }) async { + unawaited(showModalBottomSheet( + context: pageContext, + // Clip.hardEdge looks bad; Clip.antiAliasWithSaveLayer looks pixel-perfect + // on my iPhone 13 Pro but is marked as "much slower": + // https://api.flutter.dev/flutter/dart-ui/Clip.html + clipBehavior: Clip.antiAlias, + useSafeArea: true, + isScrollControlled: true, + // The Figma uses designVariables.mainBackground, which we could set + // here with backgroundColor. Shrug; instead, accept the background color + // from BottomSheetThemeData, which is similar to that (as of 2025-10-07), + // for consistency with other bottom sheets. + builder: (_) { + return PerAccountStoreWidget( + accountId: initialAccountId, + // PageRoot goes under PerAccountStoreWidget, so the provided context + // can be used for PerAccountStoreWidget.of. + child: PageRoot( + child: ShareSheet( + sharedFiles: sharedFiles, + sharedText: sharedText))); + })); } void _handleNarrowSelect(BuildContext context, Narrow narrow) { @@ -171,24 +196,74 @@ class SharePage extends StatelessWidget { @override Widget build(BuildContext context) { - final zulipLocalizations = ZulipLocalizations.of(context); + final store = PerAccountStoreWidget.of(context); final designVariables = DesignVariables.of(context); + final zulipLocalizations = ZulipLocalizations.of(context); + + Widget mkTabLabel({required String text, required IconData icon}) { + return ConstrainedBox( + constraints: BoxConstraints(minHeight: 42), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Icon(size: 24, icon), + Flexible( + child: Text( + text, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + fontSize: 18, + height: 24 / 18, + ).merge(weightVariableTextStyle(context, wght: 500)))), + ])); + } return DefaultTabController( length: 2, - child: Scaffold( - appBar: AppBar( - title: Text(zulipLocalizations.sharePageTitle), - bottom: TabBar( - indicatorColor: designVariables.icon, - labelColor: designVariables.foreground, - unselectedLabelColor: designVariables.foreground.withFadedAlpha(0.7), + child: Column(children: [ + Row(children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 7, horizontal: 11), + child: AvatarShape( + size: 28, + borderRadius: 4, + child: RealmContentNetworkImage( + store.resolvedRealmIcon, + filterQuality: FilterQuality.medium, + fit: BoxFit.cover))), + Expanded(child: TabBar( + padding: EdgeInsets.zero, + labelPadding: EdgeInsets.symmetric(horizontal: 4), + labelColor: designVariables.iconSelected, + unselectedLabelColor: designVariables.icon, + // TODO(upstream): The documentation for `indicatorWeight` states + // that it is ignored if `indicator` is specified. But that + // doesn't seem to be the case in practice, this value affects + // the size of the tab label, making the tab label 2px larger + // which is also the default value for this argument. See: + // https://github.com/flutter/flutter/issues/171951 + // As a workaround passing a value of zero appears to be working + // fine, so use that. + indicatorWeight: 0, + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + color: designVariables.iconSelected, + width: 4.0)), + dividerHeight: 0, splashFactory: NoSplash.splashFactory, + overlayColor: WidgetStatePropertyAll(Colors.transparent), tabs: [ - Tab(text: zulipLocalizations.channelsPageTitle), - Tab(text: zulipLocalizations.recentDmConversationsPageTitle), + mkTabLabel( + text: zulipLocalizations.channelsPageTitle, + icon: ZulipIcons.hash_italic), + mkTabLabel( + text: zulipLocalizations.recentDmConversationsPageTitle, + icon: ZulipIcons.two_person), ])), - body: TabBarView(children: [ + ]), + Expanded(child: TabBarView(children: [ SubscriptionListPageBody( showTopicListButtonInActionSheet: false, hideChannelsIfUserCantSendMessage: true, @@ -204,6 +279,7 @@ class SharePage extends StatelessWidget { RecentDmConversationsPageBody( hideDmsIfUserCantPost: true, onDmSelect: (narrow) => _handleNarrowSelect(context, narrow)), - ]))); + ])), + ])); } } From 5d559aeb0e3409480743c90644128361bc385f7a Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Tue, 30 Sep 2025 22:34:47 +0530 Subject: [PATCH 3/3] share: Support switching accounts Co-Authored-By: Chris Bobbe Fixes: #1779 --- assets/l10n/app_en.arb | 4 + lib/generated/l10n/zulip_localizations.dart | 6 + .../l10n/zulip_localizations_ar.dart | 3 + .../l10n/zulip_localizations_de.dart | 3 + .../l10n/zulip_localizations_el.dart | 3 + .../l10n/zulip_localizations_en.dart | 3 + .../l10n/zulip_localizations_es.dart | 3 + .../l10n/zulip_localizations_fr.dart | 3 + .../l10n/zulip_localizations_he.dart | 3 + .../l10n/zulip_localizations_hu.dart | 3 + .../l10n/zulip_localizations_it.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 + .../l10n/zulip_localizations_sl.dart | 3 + .../l10n/zulip_localizations_uk.dart | 3 + .../l10n/zulip_localizations_zh.dart | 3 + lib/widgets/share.dart | 142 ++++++++++++++++-- 20 files changed, 194 insertions(+), 9 deletions(-) diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index ddae45df1b..8ffabbccfb 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -1220,6 +1220,10 @@ "allChannelsPageTitle": {"type": "String", "example": "All channels"} } }, + "shareChooseAccountLabel": "Choose an account", + "@shareChooseAccountLabel": { + "description": "Label for the page about selecting an account to share content received from other apps." + }, "mainMenuMyProfile": "My profile", "@mainMenuMyProfile": { "description": "Label for main-menu button leading to the user's own profile." diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 8a225eb3b7..f037b86ddf 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -1785,6 +1785,12 @@ abstract class ZulipLocalizations { /// **'Try going to {allChannelsPageTitle} and joining some of them.'** String channelsEmptyPlaceholderMessage(String allChannelsPageTitle); + /// Label for the page about selecting an account to share content received from other apps. + /// + /// In en, this message translates to: + /// **'Choose an account'** + String get shareChooseAccountLabel; + /// Label for main-menu button leading to the user's own profile. /// /// 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 ec0e099da0..9b2ef31aae 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -1025,6 +1025,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index a1e911ef47..1a96b3b38c 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -1044,6 +1044,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'Mein Profil'; diff --git a/lib/generated/l10n/zulip_localizations_el.dart b/lib/generated/l10n/zulip_localizations_el.dart index 0a241959ef..f6242993e4 100644 --- a/lib/generated/l10n/zulip_localizations_el.dart +++ b/lib/generated/l10n/zulip_localizations_el.dart @@ -1025,6 +1025,9 @@ class ZulipLocalizationsEl extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 6e8c670916..de42f675f9 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -1025,6 +1025,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_es.dart b/lib/generated/l10n/zulip_localizations_es.dart index dda88b329d..8bf2121d7c 100644 --- a/lib/generated/l10n/zulip_localizations_es.dart +++ b/lib/generated/l10n/zulip_localizations_es.dart @@ -1025,6 +1025,9 @@ class ZulipLocalizationsEs extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index b7815c99bf..cce82fa772 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -1041,6 +1041,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'Mon profil'; diff --git a/lib/generated/l10n/zulip_localizations_he.dart b/lib/generated/l10n/zulip_localizations_he.dart index 55b9c286b8..9c4301a890 100644 --- a/lib/generated/l10n/zulip_localizations_he.dart +++ b/lib/generated/l10n/zulip_localizations_he.dart @@ -1025,6 +1025,9 @@ class ZulipLocalizationsHe extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_hu.dart b/lib/generated/l10n/zulip_localizations_hu.dart index 53bc8e6bcf..3bb6136612 100644 --- a/lib/generated/l10n/zulip_localizations_hu.dart +++ b/lib/generated/l10n/zulip_localizations_hu.dart @@ -1025,6 +1025,9 @@ class ZulipLocalizationsHu extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index d3ed6a98f6..cf8933e340 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -1038,6 +1038,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'Il mio profilo'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index f0f30e7980..4268f51991 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -1000,6 +1000,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => '自分のプロフィール'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index 38e142fcd0..326d57e4e5 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -1025,6 +1025,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index d64f57e0bd..c6eb24dbba 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -1041,6 +1041,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations { return 'Spróbuj skorzystać z $allChannelsPageTitle i dołączyć do nich.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'Mój profil'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index e77e313a66..62fee1cd91 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -1052,6 +1052,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations { return 'Вы можете просмотреть $allChannelsPageTitle и присоединиться к некоторым из них.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'Мой профиль'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 09113358e8..12dbb8cca9 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -1027,6 +1027,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'Môj profil'; diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index 6b8c80801a..fa25aff067 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -1060,6 +1060,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations { return 'Poskusite odpreti $allChannelsPageTitle in se jim pridružiti.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'Moj profil'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index cc6c0d2370..54455e6af5 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -1042,6 +1042,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations { return 'Спробуйте перейти за посиланням $allChannelsPageTitle та приєднатися до деяких із них.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'Мій профіль'; diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index 7337340375..7b1a465bf0 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -1025,6 +1025,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations { return 'Try going to $allChannelsPageTitle and joining some of them.'; } + @override + String get shareChooseAccountLabel => 'Choose an account'; + @override String get mainMenuMyProfile => 'My profile'; diff --git a/lib/widgets/share.dart b/lib/widgets/share.dart index 1c50ae40a0..b8753a0480 100644 --- a/lib/widgets/share.dart +++ b/lib/widgets/share.dart @@ -5,14 +5,17 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:mime/mime.dart'; +import '../api/core.dart'; import '../generated/l10n/zulip_localizations.dart'; import '../host/android_intents.dart'; import '../log.dart'; import '../model/binding.dart'; import '../model/narrow.dart'; +import 'action_sheet.dart'; import 'app.dart'; import 'compose_box.dart'; import 'dialog.dart'; +import 'home.dart'; import 'icons.dart'; import 'image.dart'; import 'message_list.dart'; @@ -196,10 +199,13 @@ class ShareSheet extends StatelessWidget { @override Widget build(BuildContext context) { + final globalStore = GlobalStoreWidget.of(context); final store = PerAccountStoreWidget.of(context); final designVariables = DesignVariables.of(context); final zulipLocalizations = ZulipLocalizations.of(context); + final hasMultipleAccounts = globalStore.accountIds.length > 1; + Widget mkTabLabel({required String text, required IconData icon}) { return ConstrainedBox( constraints: BoxConstraints(minHeight: 42), @@ -224,15 +230,26 @@ class ShareSheet extends StatelessWidget { length: 2, child: Column(children: [ Row(children: [ - Padding( - padding: EdgeInsets.symmetric(vertical: 7, horizontal: 11), - child: AvatarShape( - size: 28, - borderRadius: 4, - child: RealmContentNetworkImage( - store.resolvedRealmIcon, - filterQuality: FilterQuality.medium, - fit: BoxFit.cover))), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: !hasMultipleAccounts + ? null + : () { + ChooseAccountForShareDialog.show( + pageContext: context, + selectedAccountId: store.accountId, + sharedFiles: sharedFiles, + sharedText: sharedText); + }, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 7, horizontal: 11), + child: AvatarShape( + size: 28, + borderRadius: 4, + child: RealmContentNetworkImage( + store.resolvedRealmIcon, + filterQuality: FilterQuality.medium, + fit: BoxFit.cover)))), Expanded(child: TabBar( padding: EdgeInsets.zero, labelPadding: EdgeInsets.symmetric(horizontal: 4), @@ -283,3 +300,110 @@ class ShareSheet extends StatelessWidget { ])); } } + +class ChooseAccountForShareDialog extends StatefulWidget { + const ChooseAccountForShareDialog({ + super.key, + required this.sharedFiles, + required this.sharedText, + }); + + final Iterable? sharedFiles; + final String? sharedText; + + static void show({ + required BuildContext pageContext, + required int selectedAccountId, + required Iterable? sharedFiles, + required String? sharedText, + }) async { + unawaited(showModalBottomSheet( + context: pageContext, + // Clip.hardEdge looks bad; Clip.antiAliasWithSaveLayer looks pixel-perfect + // on my iPhone 13 Pro but is marked as "much slower": + // https://api.flutter.dev/flutter/dart-ui/Clip.html + clipBehavior: Clip.antiAlias, + useSafeArea: true, + isScrollControlled: true, + builder: (_) { + return SafeArea( + minimum: const EdgeInsets.only(bottom: 16), + child: ChooseAccountForShareDialog( + sharedFiles: sharedFiles, + sharedText: sharedText)); + })); + } + + @override + State createState() => _ChooseAccountForShareDialogState(); +} + +class _ChooseAccountForShareDialogState extends State { + bool _hasUpdatedAccountsOnce = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final globalStore = GlobalStoreWidget.of(context); + + if (_hasUpdatedAccountsOnce) return; + _hasUpdatedAccountsOnce = true; + + globalStore.refreshRealmMetadata(); + } + + @override + Widget build(BuildContext context) { + final globalStore = GlobalStoreWidget.of(context); + final zulipLocalizations = ZulipLocalizations.of(context); + + final accountIds = List.unmodifiable(globalStore.accountIds); + + // TODO(#1038) align the design of this dialog to other + // choose account dialogs + final content = SliverList.builder( + itemCount: accountIds.length, + itemBuilder: (context, index) { + final accountId = accountIds[index]; + final account = globalStore.getAccount(accountId); + if (account == null) return const SizedBox.shrink(); + + final resolvedRealmIconUrl = + account.realmIcon == null + ? null + : account.realmUrl.resolveUri(account.realmIcon!); + + return ListTile( + onTap: () { + // First change home page account to the selected account. + HomePage.navigate(context, accountId: accountId); + // Then push a new share dialog for the selected account. + ShareSheet.show( + pageContext: context, + initialAccountId: accountId, + sharedFiles: widget.sharedFiles, + sharedText: widget.sharedText); + }, + splashColor: Colors.transparent, + leading: AvatarShape( + size: 56, + borderRadius: 4, + child: resolvedRealmIconUrl == null + ? const SizedBox.shrink() + : Image.network( + resolvedRealmIconUrl.toString(), + headers: userAgentHeader(), + filterQuality: FilterQuality.medium, + fit: BoxFit.cover)), + title: Text(account.realmName ?? account.realmUrl.toString()), + subtitle: Text(account.email)); + }); + + return DraggableScrollableModalBottomSheet( + header: BottomSheetHeader( + title: zulipLocalizations.shareChooseAccountLabel, + outerVerticalPadding: true), + contentSliver: content); + } +}