diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index ee7e96c35f..8e1538185a 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -23,6 +23,10 @@ "@switchAccountButton": { "description": "Label for main-menu button leading to the choose-account page." }, + "organizationsButtonLabel": "Organizations", + "@organizationsButtonLabel": { + "description": "Button text to view and switch between different organizations." + }, "tryAnotherAccountMessage": "Your account at {url} is taking a while to load.", "@tryAnotherAccountMessage": { "description": "Message that appears on the loading screen after waiting for some time.", diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index 1adc44196f..14702a04fa 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -71,6 +71,7 @@ class InitialSnapshot { final bool realmMandatoryTopics; + final String realmName; /// The number of days until a user's account is treated as a full member. /// /// Search for "realm_waiting_period_threshold" in https://zulip.com/api/register-queue. @@ -81,6 +82,8 @@ class InitialSnapshot { final Map realmDefaultExternalAccounts; + final String realmIconUrl; + final int maxFileUploadSizeMib; final Uri? serverEmojiDataUrl; // TODO(server-6) @@ -134,8 +137,10 @@ class InitialSnapshot { required this.userTopics, required this.realmWildcardMentionPolicy, required this.realmMandatoryTopics, + required this.realmName, required this.realmWaitingPeriodThreshold, required this.realmDefaultExternalAccounts, + required this.realmIconUrl, required this.maxFileUploadSizeMib, required this.serverEmojiDataUrl, required this.realmUsers, diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index a69b6ebafe..4f2b457cdc 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -62,6 +62,7 @@ InitialSnapshot _$InitialSnapshotFromJson(Map json) => _$RealmWildcardMentionPolicyEnumMap, json['realm_wildcard_mention_policy']), realmMandatoryTopics: json['realm_mandatory_topics'] as bool, + realmName: json['realm_name'] as String, realmWaitingPeriodThreshold: (json['realm_waiting_period_threshold'] as num).toInt(), realmDefaultExternalAccounts: @@ -69,6 +70,7 @@ InitialSnapshot _$InitialSnapshotFromJson(Map json) => (k, e) => MapEntry( k, RealmDefaultExternalAccount.fromJson(e as Map)), ), + realmIconUrl: json['realm_icon_url'] as String, maxFileUploadSizeMib: (json['max_file_upload_size_mib'] as num).toInt(), serverEmojiDataUrl: json['server_emoji_data_url'] == null ? null @@ -114,8 +116,10 @@ Map _$InitialSnapshotToJson(InitialSnapshot instance) => 'user_topics': instance.userTopics, 'realm_wildcard_mention_policy': instance.realmWildcardMentionPolicy, 'realm_mandatory_topics': instance.realmMandatoryTopics, + 'realm_name': instance.realmName, 'realm_waiting_period_threshold': instance.realmWaitingPeriodThreshold, 'realm_default_external_accounts': instance.realmDefaultExternalAccounts, + 'realm_icon_url': instance.realmIconUrl, 'max_file_upload_size_mib': instance.maxFileUploadSizeMib, 'server_emoji_data_url': instance.serverEmojiDataUrl?.toString(), 'realm_users': instance.realmUsers, diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index b6fbb70769..979e3e586a 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -141,6 +141,12 @@ abstract class ZulipLocalizations { /// **'Switch account'** String get switchAccountButton; + /// Button text to view and switch between different organizations. + /// + /// In en, this message translates to: + /// **'Organizations'** + String get organizationsButtonLabel; + /// Message that appears on the loading screen after waiting for some time. /// /// 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 025b4b1444..01bfacb57a 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -26,6 +26,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get switchAccountButton => 'Switch account'; + @override + String get organizationsButtonLabel => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index 9467d33428..37b867b59d 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -26,6 +26,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get switchAccountButton => 'Switch account'; + @override + String get organizationsButtonLabel => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index f363ee0043..37ca37e540 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -26,6 +26,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get switchAccountButton => 'Switch account'; + @override + String get organizationsButtonLabel => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index 35b3e86fe5..8d734b95c2 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -26,6 +26,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get switchAccountButton => 'Switch account'; + @override + String get organizationsButtonLabel => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Your account at $url is taking a while to load.'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index 0594722d31..fe0702c4f7 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -26,6 +26,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get switchAccountButton => 'Przełącz konto'; + @override + String get organizationsButtonLabel => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Twoje konto na $url wymaga jeszcze chwili na załadowanie.'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 879559fed4..a0b01d655d 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -26,6 +26,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get switchAccountButton => 'Сменить учетную запись'; + @override + String get organizationsButtonLabel => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Ваша учетная запись на $url загружается медленно.'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index af87dfd949..637851152a 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -26,6 +26,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get switchAccountButton => 'Zmeniť účet'; + @override + String get organizationsButtonLabel => 'Organizations'; + @override String tryAnotherAccountMessage(Object url) { return 'Načítavanie vášho konta na adrese $url chvílu trvá.'; diff --git a/lib/model/store.dart b/lib/model/store.dart index 7603c7f452..5ec343ce3e 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -263,6 +263,8 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess assert(connection.zulipFeatureLevel == account.zulipFeatureLevel); final realmUrl = account.realmUrl; + final realmName = initialSnapshot.realmName; + final realmIcon = initialSnapshot.realmIconUrl; final channels = ChannelStoreImpl(initialSnapshot: initialSnapshot); return PerAccountStore._( globalStore: globalStore, @@ -270,6 +272,8 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess realmUrl: realmUrl, realmWildcardMentionPolicy: initialSnapshot.realmWildcardMentionPolicy, realmMandatoryTopics: initialSnapshot.realmMandatoryTopics, + realmName: realmName, + realmIcon: realmIcon, realmWaitingPeriodThreshold: initialSnapshot.realmWaitingPeriodThreshold, maxFileUploadSizeMib: initialSnapshot.maxFileUploadSizeMib, realmDefaultExternalAccounts: initialSnapshot.realmDefaultExternalAccounts, @@ -315,6 +319,8 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess required this.realmUrl, required this.realmWildcardMentionPolicy, required this.realmMandatoryTopics, + required this.realmName, + required this.realmIcon, required this.realmWaitingPeriodThreshold, required this.maxFileUploadSizeMib, required this.realmDefaultExternalAccounts, @@ -373,6 +379,9 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess /// Always equal to `account.realmUrl` and `connection.realmUrl`. final Uri realmUrl; + final String realmName; + final String realmIcon; + /// Resolve [reference] as a URL relative to [realmUrl]. /// /// This returns null if [reference] fails to parse as a URL. diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart index ad70b57c32..bb708e8c26 100644 --- a/lib/widgets/home.dart +++ b/lib/widgets/home.dart @@ -310,6 +310,7 @@ void _showMainMenu(BuildContext context, { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ + _OrganizationHeader(), Flexible(child: InsetShadowBox( top: 8, bottom: 8, color: designVariables.bgBotBar, @@ -326,6 +327,84 @@ void _showMainMenu(BuildContext context, { }); } +class _OrganizationHeader extends StatelessWidget { + @override + Widget build(BuildContext context) { + final store = PerAccountStoreWidget.of(context); + final designVariables = DesignVariables.of(context); + final zulipLocalizations = ZulipLocalizations.of(context); + + String organizationName = store.realmName; + Uri? organizationIcon = store.tryResolveUrl(store.realmIcon); + final buttonStyle = TextButton.styleFrom( + splashFactory: NoSplash.splashFactory, + overlayColor: Colors.transparent + ); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Image.network( + organizationIcon.toString(), + width: 28, + height: 28, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const SizedBox( + width: 28, + height: 28, + child: Placeholder(), + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return const SizedBox( + width: 28, + height: 28, + child: CircularProgressIndicator(), + ); + }, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + organizationName, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + TextButton( + onPressed: () { + Navigator.of(context).push(MaterialWidgetRoute(page: const ChooseAccountPage())); + }, + style: buttonStyle, + child: Text( + zulipLocalizations.organizationsButtonLabel, + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w500, + color: designVariables.icon, + ), + ), + ), + ], + ), + ); + } +} + abstract class _MenuButton extends StatelessWidget { const _MenuButton(); diff --git a/test/example_data.dart b/test/example_data.dart index 6b84bf185c..68da830a82 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -856,10 +856,12 @@ InitialSnapshot initialSnapshot({ List? streams, UserSettings? userSettings, List? userTopics, + String? realmName, RealmWildcardMentionPolicy? realmWildcardMentionPolicy, bool? realmMandatoryTopics, int? realmWaitingPeriodThreshold, Map? realmDefaultExternalAccounts, + String? realmIconUrl, int? maxFileUploadSizeMib, Uri? serverEmojiDataUrl, List? realmUsers, @@ -892,10 +894,12 @@ InitialSnapshot initialSnapshot({ emojiset: Emojiset.google, ), userTopics: userTopics, + realmName: realmName ?? "Example Name", realmWildcardMentionPolicy: realmWildcardMentionPolicy ?? RealmWildcardMentionPolicy.everyone, realmMandatoryTopics: realmMandatoryTopics ?? true, realmWaitingPeriodThreshold: realmWaitingPeriodThreshold ?? 0, realmDefaultExternalAccounts: realmDefaultExternalAccounts ?? {}, + realmIconUrl: realmIconUrl ?? "https://example.com/image.png", maxFileUploadSizeMib: maxFileUploadSizeMib ?? 25, serverEmojiDataUrl: serverEmojiDataUrl ?? realmUrl.replace(path: '/static/emoji.json'), diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 5bb789727f..b369242c21 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -248,6 +248,24 @@ void main () { check(find.byType(BottomSheet)).findsNothing(); }); + testWidgets('organization header shows realm info and navigation works', (tester) async { + await prepare(tester); + final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); + await tapOpenMenu(tester); + + check(find.text(store.realmName)).findsOne(); + check(find.byType(Image)).findsOne(); + + final organizationsButton = find.text('Organizations'); + check(organizationsButton).findsOne(); + + await tester.tap(organizationsButton); + await tester.pump(Duration.zero); + await tester.pump(const Duration(milliseconds: 250)); // wait for animation + + check(find.byType(ChooseAccountPage)).findsOne(); + }); + testWidgets('_MyProfileButton', (tester) async { await prepare(tester); await tapOpenMenu(tester);