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,