Skip to content
Open
6 changes: 3 additions & 3 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1117,9 +1117,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": {
Expand Down
6 changes: 3 additions & 3 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1647,11 +1647,11 @@ abstract class ZulipLocalizations {
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.
///
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Share';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'My profile';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_de.dart
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Teilen';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'Mein Profil';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Share';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'My profile';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_fr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Share';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'My profile';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_it.dart
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Share';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'Il mio profilo';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
}

@override
String get sharePageTitle => '共有';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => '自分のプロフィール';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_nb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Share';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'My profile';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Udostępnij';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'Mój profil';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -959,7 +959,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Поделиться';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'Мой профиль';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_sk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Share';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'Môj profil';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_sl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -969,7 +969,7 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Deli';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'Moj profil';
Expand Down
2 changes: 1 addition & 1 deletion lib/generated/l10n/zulip_localizations_uk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Поділитися';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'Мій профіль';
Expand Down
8 changes: 1 addition & 7 deletions lib/generated/l10n/zulip_localizations_zh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
}

@override
String get sharePageTitle => 'Share';
String get shareChooseAccountLabel => 'Choose an account';

@override
String get mainMenuMyProfile => 'My profile';
Expand Down Expand Up @@ -2014,9 +2014,6 @@ class ZulipLocalizationsZhHansCn extends ZulipLocalizationsZh {
@override
String get channelsEmptyPlaceholder => '您还没有订阅任何频道。';

@override
String get sharePageTitle => '分享';

@override
String get mainMenuMyProfile => '个人资料';

Expand Down Expand Up @@ -3127,9 +3124,6 @@ class ZulipLocalizationsZhHantTw extends ZulipLocalizationsZh {
return '您尚未訂閱任何頻道。請前往 <z-link>$allChannelsPageTitle</z-link> 並加入一些頻道。';
}

@override
String get sharePageTitle => '分享';

@override
String get mainMenuMyProfile => '我的設定檔';

Expand Down
153 changes: 148 additions & 5 deletions lib/widgets/share.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:mime/mime.dart';

import '../api/core.dart';
import '../api/route/realm.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 'content.dart';
import 'dialog.dart';
import 'home.dart';
import 'icons.dart';
import 'message_list.dart';
import 'recent_dm_conversations.dart';
Expand Down Expand Up @@ -183,6 +187,7 @@ class ShareDialog 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);
Expand All @@ -205,15 +210,29 @@ class ShareDialog extends StatelessWidget {
maxLines: 1);
}

final hasMultipleAccounts =
List<int>.unmodifiable(globalStore.accountIds).length > 1;
Comment on lines +213 to +214
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be

Suggested change
final hasMultipleAccounts =
List<int>.unmodifiable(globalStore.accountIds).length > 1;
final hasMultipleAccounts = globalStore.accountIds.length > 1;

right?


return DefaultTabController(
length: 2,
child: Column(children: [
Row(children: [
SizedBox.square(
dimension: 42,
child: Padding(
padding: const EdgeInsets.all(7),
child: RealmContentNetworkImage(realmIconUrl))),
GestureDetector(
onTap: hasMultipleAccounts
? () {
ChooseAccountForShareDialog.show(
pageContext: context,
selectedAccountId: store.accountId,
sharedFiles: sharedFiles,
sharedText: sharedText);
}
Comment on lines +221 to +228
Copy link
Collaborator

@chrisbobbe chrisbobbe Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there's some way we can make it clearer that the org icon can be tapped to switch accounts. Like the chevron-down icon on the emoji selector on the set-status page:

image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno, at least as an Android user, I feel like one is pretty well trained that clicking on your avatar opens the account switcher.

: null,
Comment on lines +221 to +229
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
onTap: hasMultipleAccounts
? () {
ChooseAccountForShareDialog.show(
pageContext: context,
selectedAccountId: store.accountId,
sharedFiles: sharedFiles,
sharedText: sharedText);
}
: null,
onTap: hasMultipleAccounts
? () {
ChooseAccountForShareDialog.show(
pageContext: context,
selectedAccountId: store.accountId,
sharedFiles: sharedFiles,
sharedText: sharedText);
}
: null,

child: SizedBox.square(
dimension: 42,
child: Padding(
padding: const EdgeInsets.all(7),
child: RealmContentNetworkImage(realmIconUrl))),
),
Comment on lines +234 to +235
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
child: RealmContentNetworkImage(realmIconUrl))),
),
child: RealmContentNetworkImage(realmIconUrl)))),

Expanded(child: TabBar(
labelStyle: labelStyle,
labelColor: designVariables.iconSelected,
Expand Down Expand Up @@ -266,3 +285,127 @@ class ShareDialog extends StatelessWidget {
]));
}
}

class ChooseAccountForShareDialog extends StatefulWidget {
const ChooseAccountForShareDialog({
super.key,
required this.sharedFiles,
required this.sharedText,
});

final Iterable<FileToUpload>? sharedFiles;
final String? sharedText;

static void show({
required BuildContext pageContext,
required int selectedAccountId,
required Iterable<FileToUpload>? sharedFiles,
required String? sharedText,
}) async {
unawaited(showModalBottomSheet<void>(
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<ChooseAccountForShareDialog> createState() => _ChooseAccountForShareDialogState();
}

class _ChooseAccountForShareDialogState extends State<ChooseAccountForShareDialog> {
late List<int> accountIds;
bool _hasUpdatedAccountsOnce = false;

@override
void didChangeDependencies() {
super.didChangeDependencies();

final globalStore = GlobalStoreWidget.of(context);
accountIds = List.unmodifiable(globalStore.accountIds);

if (_hasUpdatedAccountsOnce) return;
_hasUpdatedAccountsOnce = true;

for (final accountId in accountIds) {
final account = globalStore.getAccount(accountId);
if (account == null) continue;

unawaited(() async {
final GetServerSettingsResult serverSettings;
final connection = globalStore.apiConnection(
realmUrl: account.realmUrl,
zulipFeatureLevel: null);
try {
serverSettings = await getServerSettings(connection);
} catch (_) {
return;
} finally {
connection.close();
}

if (globalStore.getAccount(accountId) != null) {
await globalStore.updateRealmData(
accountId,
realmName: serverSettings.realmName,
realmIcon: serverSettings.realmIcon);
}
}());
Comment on lines +344 to +363
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal is to make sure the realm name and icon are present and up-to-date, right?

I think my ideal way to do this would be more centralized and a bit more rigorous:

  • The choose-account page/dialog doesn't yet show the name and icon, but it will, and it'll be equally useful there to refresh them as here.
  • We've written careful "refuse-to-connect-to-ancient-servers" logic that should eliminate ancient-server behavior as a possible source of bugs, which should help simplify debugging. This revision puts a small hole in that logic, which we could fix by checking ZulipVersionData.isUnsupported before interpreting realmName and realmIcon. Like we do around the other getServerSettings call.
    • I don't think any ancient servers format realmName and realmIcon in weird, interfering ways…but the point is being confident that it wouldn't matter even if they did.
  • The realm name and icon are also provided via the event system, and we should defer to that: if a PerAccountStore finishes loading before we've received and recorded the data from get-server-settings, we should abort instead of clobbering the store with this data that arrived out-of-band.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more centralized

I'm not sure yet where's the most helpful place to put this. Maybe in a stateful widget that gets passed to PerAccountStoreWidget.placeholder? I'd be happy to think about that when I revisit my draft commits in #1816, and in the meantime perhaps you can just address my second and third bullet points above, which are correctness issues.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a good home for this would be as a method on GlobalStore, perhaps named like ensureFreshAccountData. (Or ensureFreshRealmMetadata? The point is it's the data (a) that we store in the Account record but (b) about the realm as a whole.)

Then that method will have access to _perAccountStoresLoading as well as _perAccountStores. It can look at those in order to ensure that it doesn't do anything if there's already a PerAccountStore or one being loaded.

}
}

@override
Widget build(BuildContext context) {
final globalStore = GlobalStoreWidget.of(context);
final zulipLocalizations = ZulipLocalizations.of(context);

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();
}

return ListTile(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a TODO(#1038) for aligning this dialog with other choose-account dialogs (link: #1038)

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.
ShareDialog.show(
pageContext: context,
initialAccountId: accountId,
sharedFiles: widget.sharedFiles,
sharedText: widget.sharedText);
},
leading: AspectRatio(
aspectRatio: 1,
child: account.realmIcon != null
? Image.network(
account.realmUrl.resolveUri(account.realmIcon!).toString(),
headers: userAgentHeader())
: null),
title: Text(account.realmName ?? account.realmUrl.toString()),
subtitle: Text(account.email));
});

return DraggableScrollableModalBottomSheet(
header: Padding(
padding: const EdgeInsets.only(top: 8),
child: BottomSheetHeader(title: zulipLocalizations.shareChooseAccountLabel)),
contentSliver: SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: content));
}
}