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..8ffabbccfb 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -1220,9 +1220,9 @@ "allChannelsPageTitle": {"type": "String", "example": "All channels"} } }, - "sharePageTitle": "Share", - "@sharePageTitle": { - "description": "Title for the page about sharing content received from other apps." + "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": { 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..f037b86ddf 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -1785,11 +1785,11 @@ 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. + /// Label for the page about selecting an account to share content received from other apps. /// /// In en, this message translates to: - /// **'Share'** - String get sharePageTitle; + /// **'Choose an account'** + String get shareChooseAccountLabel; /// Label for main-menu button leading to the user's own profile. /// diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart index a2afe093b0..9b2ef31aae 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -1026,7 +1026,7 @@ class ZulipLocalizationsAr extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + 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 fcc5c8d3fa..1a96b3b38c 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -1045,7 +1045,7 @@ class ZulipLocalizationsDe extends ZulipLocalizations { } @override - String get sharePageTitle => 'Teilen'; + 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 754866cb02..f6242993e4 100644 --- a/lib/generated/l10n/zulip_localizations_el.dart +++ b/lib/generated/l10n/zulip_localizations_el.dart @@ -1026,7 +1026,7 @@ class ZulipLocalizationsEl extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + 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 f5809fc10d..de42f675f9 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -1026,7 +1026,7 @@ class ZulipLocalizationsEn extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + 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 6bdd3973c3..8bf2121d7c 100644 --- a/lib/generated/l10n/zulip_localizations_es.dart +++ b/lib/generated/l10n/zulip_localizations_es.dart @@ -1026,7 +1026,7 @@ class ZulipLocalizationsEs extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + 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 7adbcb8303..cce82fa772 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -1042,7 +1042,7 @@ class ZulipLocalizationsFr extends ZulipLocalizations { } @override - String get sharePageTitle => 'Partager'; + 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 c28f06c4e5..9c4301a890 100644 --- a/lib/generated/l10n/zulip_localizations_he.dart +++ b/lib/generated/l10n/zulip_localizations_he.dart @@ -1026,7 +1026,7 @@ class ZulipLocalizationsHe extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + 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 43ac20a859..3bb6136612 100644 --- a/lib/generated/l10n/zulip_localizations_hu.dart +++ b/lib/generated/l10n/zulip_localizations_hu.dart @@ -1026,7 +1026,7 @@ class ZulipLocalizationsHu extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + 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 5b45be5f62..cf8933e340 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -1039,7 +1039,7 @@ class ZulipLocalizationsIt extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + 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 a54686e873..4268f51991 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -1001,7 +1001,7 @@ class ZulipLocalizationsJa extends ZulipLocalizations { } @override - String get sharePageTitle => '共有'; + 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 cd13ec9ab0..326d57e4e5 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -1026,7 +1026,7 @@ class ZulipLocalizationsNb extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + 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 036846caf4..c6eb24dbba 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -1042,7 +1042,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations { } @override - String get sharePageTitle => 'Udostępnij'; + 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 5f54d02d32..62fee1cd91 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -1053,7 +1053,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { } @override - String get sharePageTitle => 'Поделиться'; + 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 39dca858c8..12dbb8cca9 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -1028,7 +1028,7 @@ class ZulipLocalizationsSk extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + 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 abcfb90899..fa25aff067 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -1061,7 +1061,7 @@ class ZulipLocalizationsSl extends ZulipLocalizations { } @override - String get sharePageTitle => 'Deli'; + 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 0a70fc9b7f..54455e6af5 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -1043,7 +1043,7 @@ class ZulipLocalizationsUk extends ZulipLocalizations { } @override - String get sharePageTitle => 'Поділитися'; + 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 bac342c0c1..7b1a465bf0 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -1026,7 +1026,7 @@ class ZulipLocalizationsZh extends ZulipLocalizations { } @override - String get sharePageTitle => 'Share'; + String get shareChooseAccountLabel => 'Choose an account'; @override String get mainMenuMyProfile => 'My profile'; @@ -2122,9 +2122,6 @@ class ZulipLocalizationsZhHansCn extends ZulipLocalizationsZh { @override String get channelsPageTitle => '频道'; - @override - String get sharePageTitle => '分享'; - @override String get mainMenuMyProfile => '个人资料'; @@ -3230,9 +3227,6 @@ class ZulipLocalizationsZhHantTw extends ZulipLocalizationsZh { @override String get channelsPageTitle => '頻道'; - @override - String get sharePageTitle => '分享'; - @override String get mainMenuMyProfile => '我的設定檔'; 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/lib/widgets/share.dart b/lib/widgets/share.dart index df776e4d1e..b8753a0480 100644 --- a/lib/widgets/share.dart +++ b/lib/widgets/share.dart @@ -5,21 +5,27 @@ 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 'color.dart'; import 'compose_box.dart'; import 'dialog.dart'; +import 'home.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 +105,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 +127,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 +199,88 @@ class SharePage extends StatelessWidget { @override Widget build(BuildContext context) { - final zulipLocalizations = ZulipLocalizations.of(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), + 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: [ + 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), + 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 +296,114 @@ class SharePage extends StatelessWidget { RecentDmConversationsPageBody( hideDmsIfUserCantPost: true, onDmSelect: (narrow) => _handleNarrowSelect(context, narrow)), - ]))); + ])), + ])); + } +} + +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); } } 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,