diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf
index df2c4ab947..84182be3bb 100644
Binary files a/assets/icons/ZulipIcons.ttf and b/assets/icons/ZulipIcons.ttf differ
diff --git a/assets/icons/remove.svg b/assets/icons/remove.svg
new file mode 100644
index 0000000000..e42b64d1ec
--- /dev/null
+++ b/assets/icons/remove.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb
index ea2e10cff3..10817cb656 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -415,6 +415,21 @@
"others": {"type": "String", "example": "Alice, Bob"}
}
},
+ "guestUserDmWarningOne": "{guestUser} is a guest in this organization.",
+ "@guestUserDmWarningOne": {
+ "description": "Warning shown when composing a DM to one guest user",
+ "placeholders": {
+ "guestUser": {"type": "String", "example": "Alice"}
+ }
+ },
+
+ "guestUserDmWarningMany": "{guestUsers} are guests in this organization.",
+ "@guestUserDmWarningMany": {
+ "description": "Warning shown when composing DMs to multiple guest users",
+ "placeholders": {
+ "guestUsers": {"type": "String", "example": "Alice, Bob, and Charlie"}
+ }
+ },
"messageListGroupYouWithYourself": "Messages with yourself",
"@messageListGroupYouWithYourself": {
"description": "Message list recipient header for a DM group that only includes yourself."
diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart
index 0479b0428f..b357d3a3dc 100644
--- a/lib/api/model/events.dart
+++ b/lib/api/model/events.dart
@@ -67,6 +67,11 @@ sealed class Event {
case 'submessage': return SubmessageEvent.fromJson(json);
case 'typing': return TypingEvent.fromJson(json);
case 'reaction': return ReactionEvent.fromJson(json);
+ case 'realm':
+ switch(json['op'] as String){
+ case 'update': return RealmUpdateEvent.fromJson(json);
+ default: return UnexpectedEvent.fromJson(json);
+ }
case 'heartbeat': return HeartbeatEvent.fromJson(json);
// TODO add many more event types
default: return UnexpectedEvent.fromJson(json);
@@ -1151,6 +1156,46 @@ enum ReactionOp {
remove,
}
+/// A Zulip event of type `realm`, with op `update`.
+///
+/// This is the simpler of two possible event types sent when realm configuration changes.
+/// It updates a single realm setting at a time.
+///
+/// See: https://zulip.com/api/get-events#realm-update
+@JsonSerializable(fieldRename: FieldRename.snake)
+class RealmUpdateEvent extends Event {
+ @override
+ @JsonKey(includeToJson: true)
+ String get type => 'realm';
+
+ @JsonKey(includeToJson: true)
+ String get op => 'update';
+
+ @JsonKey(unknownEnumValue: JsonKey.nullForUndefinedEnumValue)
+ final RealmPropertyName? property;
+
+ final dynamic value;
+
+ RealmUpdateEvent({
+ required super.id,
+ required this.property,
+ required this.value,
+ });
+
+ factory RealmUpdateEvent.fromJson(Map json) =>
+ _$RealmUpdateEventFromJson(json);
+
+ @override
+ Map toJson() => _$RealmUpdateEventToJson(this);
+}
+
+/// As in [RealmUpdateEvent.property].
+@JsonEnum(fieldRename: FieldRename.snake)
+enum RealmPropertyName {
+ @JsonValue('enable_guest_user_dm_warning')
+ realmEnableGuestUserDmWarning,
+}
+
/// A Zulip event of type `heartbeat`: https://zulip.com/api/get-events#heartbeat
@JsonSerializable(fieldRename: FieldRename.snake)
class HeartbeatEvent extends Event {
diff --git a/lib/api/model/events.g.dart b/lib/api/model/events.g.dart
index 35206d77b9..720aaae449 100644
--- a/lib/api/model/events.g.dart
+++ b/lib/api/model/events.g.dart
@@ -704,6 +704,31 @@ const _$ReactionTypeEnumMap = {
ReactionType.zulipExtraEmoji: 'zulip_extra_emoji',
};
+RealmUpdateEvent _$RealmUpdateEventFromJson(Map json) =>
+ RealmUpdateEvent(
+ id: (json['id'] as num).toInt(),
+ property: $enumDecodeNullable(
+ _$RealmPropertyNameEnumMap,
+ json['property'],
+ unknownValue: JsonKey.nullForUndefinedEnumValue,
+ ),
+ value: json['value'],
+ );
+
+Map _$RealmUpdateEventToJson(RealmUpdateEvent instance) =>
+ {
+ 'id': instance.id,
+ 'type': instance.type,
+ 'op': instance.op,
+ 'property': _$RealmPropertyNameEnumMap[instance.property],
+ 'value': instance.value,
+ };
+
+const _$RealmPropertyNameEnumMap = {
+ RealmPropertyName.realmEnableGuestUserDmWarning:
+ 'enable_guest_user_dm_warning',
+};
+
HeartbeatEvent _$HeartbeatEventFromJson(Map json) =>
HeartbeatEvent(id: (json['id'] as num).toInt());
diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart
index 054230a256..1df8a075df 100644
--- a/lib/api/model/initial_snapshot.dart
+++ b/lib/api/model/initial_snapshot.dart
@@ -86,6 +86,9 @@ class InitialSnapshot {
final int maxFileUploadSizeMib;
+ @JsonKey(defaultValue: false) // TODO(server-10): Remove default
+ final bool realmEnableGuestUserDmWarning;
+
final Uri? serverEmojiDataUrl; // TODO(server-6)
final String? realmEmptyTopicDisplayName; // TODO(server-10)
@@ -144,6 +147,7 @@ class InitialSnapshot {
required this.realmMessageContentEditLimitSeconds,
required this.realmDefaultExternalAccounts,
required this.maxFileUploadSizeMib,
+ required this.realmEnableGuestUserDmWarning,
required this.serverEmojiDataUrl,
required this.realmEmptyTopicDisplayName,
required this.realmUsers,
diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart
index 570d7c2bba..37c4fdf81c 100644
--- a/lib/api/model/initial_snapshot.g.dart
+++ b/lib/api/model/initial_snapshot.g.dart
@@ -84,6 +84,8 @@ InitialSnapshot _$InitialSnapshotFromJson(
),
),
maxFileUploadSizeMib: (json['max_file_upload_size_mib'] as num).toInt(),
+ realmEnableGuestUserDmWarning:
+ json['realm_enable_guest_user_dm_warning'] as bool? ?? false,
serverEmojiDataUrl:
json['server_emoji_data_url'] == null
? null
@@ -109,44 +111,46 @@ InitialSnapshot _$InitialSnapshotFromJson(
.toList(),
);
-Map _$InitialSnapshotToJson(InitialSnapshot instance) =>
- {
- 'queue_id': instance.queueId,
- 'last_event_id': instance.lastEventId,
- 'zulip_feature_level': instance.zulipFeatureLevel,
- 'zulip_version': instance.zulipVersion,
- 'zulip_merge_base': instance.zulipMergeBase,
- 'alert_words': instance.alertWords,
- 'custom_profile_fields': instance.customProfileFields,
- 'email_address_visibility':
- _$EmailAddressVisibilityEnumMap[instance.emailAddressVisibility],
- 'server_typing_started_expiry_period_milliseconds':
- instance.serverTypingStartedExpiryPeriodMilliseconds,
- 'server_typing_stopped_wait_period_milliseconds':
- instance.serverTypingStoppedWaitPeriodMilliseconds,
- 'server_typing_started_wait_period_milliseconds':
- instance.serverTypingStartedWaitPeriodMilliseconds,
- 'realm_emoji': instance.realmEmoji,
- 'recent_private_conversations': instance.recentPrivateConversations,
- 'subscriptions': instance.subscriptions,
- 'unread_msgs': instance.unreadMsgs,
- 'streams': instance.streams,
- 'user_settings': instance.userSettings,
- 'user_topics': instance.userTopics,
- 'realm_wildcard_mention_policy': instance.realmWildcardMentionPolicy,
- 'realm_mandatory_topics': instance.realmMandatoryTopics,
- 'realm_waiting_period_threshold': instance.realmWaitingPeriodThreshold,
- 'realm_allow_message_editing': instance.realmAllowMessageEditing,
- 'realm_message_content_edit_limit_seconds':
- instance.realmMessageContentEditLimitSeconds,
- 'realm_default_external_accounts': instance.realmDefaultExternalAccounts,
- 'max_file_upload_size_mib': instance.maxFileUploadSizeMib,
- 'server_emoji_data_url': instance.serverEmojiDataUrl?.toString(),
- 'realm_empty_topic_display_name': instance.realmEmptyTopicDisplayName,
- 'realm_users': instance.realmUsers,
- 'realm_non_active_users': instance.realmNonActiveUsers,
- 'cross_realm_bots': instance.crossRealmBots,
- };
+Map _$InitialSnapshotToJson(
+ InitialSnapshot instance,
+) => {
+ 'queue_id': instance.queueId,
+ 'last_event_id': instance.lastEventId,
+ 'zulip_feature_level': instance.zulipFeatureLevel,
+ 'zulip_version': instance.zulipVersion,
+ 'zulip_merge_base': instance.zulipMergeBase,
+ 'alert_words': instance.alertWords,
+ 'custom_profile_fields': instance.customProfileFields,
+ 'email_address_visibility':
+ _$EmailAddressVisibilityEnumMap[instance.emailAddressVisibility],
+ 'server_typing_started_expiry_period_milliseconds':
+ instance.serverTypingStartedExpiryPeriodMilliseconds,
+ 'server_typing_stopped_wait_period_milliseconds':
+ instance.serverTypingStoppedWaitPeriodMilliseconds,
+ 'server_typing_started_wait_period_milliseconds':
+ instance.serverTypingStartedWaitPeriodMilliseconds,
+ 'realm_emoji': instance.realmEmoji,
+ 'recent_private_conversations': instance.recentPrivateConversations,
+ 'subscriptions': instance.subscriptions,
+ 'unread_msgs': instance.unreadMsgs,
+ 'streams': instance.streams,
+ 'user_settings': instance.userSettings,
+ 'user_topics': instance.userTopics,
+ 'realm_wildcard_mention_policy': instance.realmWildcardMentionPolicy,
+ 'realm_mandatory_topics': instance.realmMandatoryTopics,
+ 'realm_waiting_period_threshold': instance.realmWaitingPeriodThreshold,
+ 'realm_allow_message_editing': instance.realmAllowMessageEditing,
+ 'realm_message_content_edit_limit_seconds':
+ instance.realmMessageContentEditLimitSeconds,
+ 'realm_default_external_accounts': instance.realmDefaultExternalAccounts,
+ 'max_file_upload_size_mib': instance.maxFileUploadSizeMib,
+ 'realm_enable_guest_user_dm_warning': instance.realmEnableGuestUserDmWarning,
+ 'server_emoji_data_url': instance.serverEmojiDataUrl?.toString(),
+ 'realm_empty_topic_display_name': instance.realmEmptyTopicDisplayName,
+ 'realm_users': instance.realmUsers,
+ 'realm_non_active_users': instance.realmNonActiveUsers,
+ 'cross_realm_bots': instance.crossRealmBots,
+};
const _$EmailAddressVisibilityEnumMap = {
EmailAddressVisibility.everyone: 1,
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index e8b15440e3..fc969b6238 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -647,6 +647,18 @@ abstract class ZulipLocalizations {
/// **'DMs with {others}'**
String dmsWithOthersPageTitle(String others);
+ /// Warning shown when composing a DM to one guest user
+ ///
+ /// In en, this message translates to:
+ /// **'{guestUser} is a guest in this organization.'**
+ String guestUserDmWarningOne(String guestUser);
+
+ /// Warning shown when composing DMs to multiple guest users
+ ///
+ /// In en, this message translates to:
+ /// **'{guestUsers} are guests in this organization.'**
+ String guestUserDmWarningMany(String guestUsers);
+
/// Message list recipient header for a DM group that only includes yourself.
///
/// 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 c2478f4613..1ec7dd8a88 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -324,6 +324,16 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
return 'DMs with $others';
}
+ @override
+ String guestUserDmWarningOne(String guestUser) {
+ return '$guestUser is a guest in this organization.';
+ }
+
+ @override
+ String guestUserDmWarningMany(String guestUsers) {
+ return '$guestUsers are guests in this organization.';
+ }
+
@override
String get messageListGroupYouWithYourself => 'Messages with yourself';
diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart
index 289ba33af2..b1ec47d212 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -324,6 +324,16 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
return 'DMs with $others';
}
+ @override
+ String guestUserDmWarningOne(String guestUser) {
+ return '$guestUser is a guest in this organization.';
+ }
+
+ @override
+ String guestUserDmWarningMany(String guestUsers) {
+ return '$guestUsers are guests in this organization.';
+ }
+
@override
String get messageListGroupYouWithYourself => 'Messages with yourself';
diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart
index 00537f73a2..041791b47a 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -324,6 +324,16 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
return 'DMs with $others';
}
+ @override
+ String guestUserDmWarningOne(String guestUser) {
+ return '$guestUser is a guest in this organization.';
+ }
+
+ @override
+ String guestUserDmWarningMany(String guestUsers) {
+ return '$guestUsers are guests in this organization.';
+ }
+
@override
String get messageListGroupYouWithYourself => 'Messages with yourself';
diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart
index 3c063e91da..43237cc5f3 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -324,6 +324,16 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
return 'DMs with $others';
}
+ @override
+ String guestUserDmWarningOne(String guestUser) {
+ return '$guestUser is a guest in this organization.';
+ }
+
+ @override
+ String guestUserDmWarningMany(String guestUsers) {
+ return '$guestUsers are guests in this organization.';
+ }
+
@override
String get messageListGroupYouWithYourself => 'Messages with yourself';
diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart
index d4c3a033d9..a3c2d78290 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -324,6 +324,16 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
return 'DM z $others';
}
+ @override
+ String guestUserDmWarningOne(String guestUser) {
+ return '$guestUser is a guest in this organization.';
+ }
+
+ @override
+ String guestUserDmWarningMany(String guestUsers) {
+ return '$guestUsers are guests in this organization.';
+ }
+
@override
String get messageListGroupYouWithYourself => 'Zapiski na własne konto';
diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart
index cb09ca516e..35ad545051 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -324,6 +324,16 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
return 'ЛС с $others';
}
+ @override
+ String guestUserDmWarningOne(String guestUser) {
+ return '$guestUser is a guest in this organization.';
+ }
+
+ @override
+ String guestUserDmWarningMany(String guestUsers) {
+ return '$guestUsers are guests in this organization.';
+ }
+
@override
String get messageListGroupYouWithYourself => 'Сообщения с собой';
diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart
index 0cb42c3a37..ca8f709b86 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -324,6 +324,16 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
return 'DMs with $others';
}
+ @override
+ String guestUserDmWarningOne(String guestUser) {
+ return '$guestUser is a guest in this organization.';
+ }
+
+ @override
+ String guestUserDmWarningMany(String guestUsers) {
+ return '$guestUsers are guests in this organization.';
+ }
+
@override
String get messageListGroupYouWithYourself => 'Messages with yourself';
diff --git a/lib/model/store.dart b/lib/model/store.dart
index 939120113e..2e6e6c7ed5 100644
--- a/lib/model/store.dart
+++ b/lib/model/store.dart
@@ -471,6 +471,7 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
realmMandatoryTopics: initialSnapshot.realmMandatoryTopics,
realmWaitingPeriodThreshold: initialSnapshot.realmWaitingPeriodThreshold,
maxFileUploadSizeMib: initialSnapshot.maxFileUploadSizeMib,
+ realmEnableGuestUserDmWarning: initialSnapshot.realmEnableGuestUserDmWarning,
realmEmptyTopicDisplayName: initialSnapshot.realmEmptyTopicDisplayName,
realmAllowMessageEditing: initialSnapshot.realmAllowMessageEditing,
realmMessageContentEditLimitSeconds: initialSnapshot.realmMessageContentEditLimitSeconds,
@@ -510,6 +511,7 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
required this.realmMandatoryTopics,
required this.realmWaitingPeriodThreshold,
required this.maxFileUploadSizeMib,
+ required this.realmEnableGuestUserDmWarning,
required String? realmEmptyTopicDisplayName,
required this.realmAllowMessageEditing,
required this.realmMessageContentEditLimitSeconds,
@@ -571,6 +573,13 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
final int? realmMessageContentEditLimitSeconds; // TODO(#668): update this realm setting
final int maxFileUploadSizeMib; // No event for this.
+ /// Whether to show a warning when composing a DM to a guest user.
+ ///
+ /// See: https://zulip.com/api/get-events-types
+ /// Changes: Added in Zulip 10.0 (feature level 348).
+ // TODO(server-10): Remove default
+ bool realmEnableGuestUserDmWarning = false;
+
/// The display name to use for empty topics.
///
/// This should only be accessed when FL >= 334, since topics cannot
@@ -899,6 +908,13 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
assert(debugLog("server event: reaction/${event.op}"));
_messages.handleReactionEvent(event);
+ case RealmUpdateEvent():
+ assert(debugLog("server event: realm/${event.op}"));
+ if (event.property == RealmPropertyName.realmEnableGuestUserDmWarning) {
+ realmEnableGuestUserDmWarning = event.value as bool;
+ notifyListeners();
+ }
+ break;
case UnexpectedEvent():
assert(debugLog("server event: ${jsonEncode(event.toJson())}")); // TODO log better
}
diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart
index 005321719d..480fe2446b 100644
--- a/lib/widgets/compose_box.dart
+++ b/lib/widgets/compose_box.dart
@@ -1484,6 +1484,42 @@ class _ErrorBanner extends _Banner {
}
}
+class _WarningBanner extends _Banner {
+ const _WarningBanner({
+ required this.label,
+ required this.onDismiss,
+ });
+
+ final String label;
+ final VoidCallback? onDismiss;
+
+ @override
+ String getLabel(ZulipLocalizations zulipLocalizations) => label;
+
+ @override
+ Color getLabelColor(DesignVariables designVariables) =>
+ designVariables.btnLabelAttMediumIntWarning;
+
+ @override
+ Color getBackgroundColor(DesignVariables designVariables) =>
+ designVariables.bannerBgIntWarning;
+
+ @override
+ bool get padEnd => false;
+
+ @override
+ Widget? buildTrailing(BuildContext context) {
+ final designVariables = DesignVariables.of(context);
+ return InkWell(
+ splashFactory: NoSplash.splashFactory,
+ onTap: onDismiss,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Icon(ZulipIcons.remove,
+ size: 24, color: designVariables.btnLabelAttLowIntWarning)));
+ }
+}
+
/// The compose box.
///
/// Takes the full screen width, covering the horizontal insets with its surface.
@@ -1521,6 +1557,8 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM
@override ComposeBoxController get controller => _controller!;
ComposeBoxController? _controller;
+ bool _isWarningBannerDismissed = false;
+
@override
void onNewStore() {
switch (widget.narrow) {
@@ -1547,6 +1585,12 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM
super.dispose();
}
+ void _dismissWarningBanner() {
+ setState(() {
+ _isWarningBannerDismissed = true;
+ });
+ }
+
/// An [_ErrorBanner] that replaces the compose box's text inputs.
Widget? _errorBannerComposingNotAllowed(BuildContext context) {
final store = PerAccountStoreWidget.of(context);
@@ -1576,11 +1620,62 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM
return null;
}
+ /// A [_WarningBanner] that goes at the top of the compose box.
+ Widget? _warningBanner(BuildContext context) {
+ if (_isWarningBannerDismissed) return null;
+
+ final store = PerAccountStoreWidget.of(context);
+ final zulipLocalizations = ZulipLocalizations.of(context);
+
+ if (store.connection.zulipFeatureLevel! < 348 ||
+ !store.realmEnableGuestUserDmWarning) {
+ return null;
+ }
+
+ switch (widget.narrow) {
+ case DmNarrow(:final otherRecipientIds):
+ final guestUsers = otherRecipientIds
+ .map((id) => store.getUser(id))
+ .where((user) => user?.role == UserRole.guest)
+ .toList();
+
+ if (guestUsers.isEmpty) return null;
+
+ final guestNames = guestUsers
+ .map((user) => user != null
+ ? store.userDisplayName(user.userId)
+ : zulipLocalizations.unknownUserName)
+ .toList();
+
+ final String formattedNames;
+ if (guestUsers.length == 1) {
+ formattedNames = guestNames[0];
+ } else {
+ final allButLast =
+ guestNames.sublist(0, guestNames.length - 1).join(', ');
+ formattedNames =
+ "$allButLast${guestUsers.length > 2 ? ',' : ''} and ${guestNames.last}";
+ }
+
+ final bannerText = guestUsers.length == 1
+ ? zulipLocalizations.guestUserDmWarningOne(guestNames.first)
+ : zulipLocalizations.guestUserDmWarningMany(formattedNames);
+
+ return _WarningBanner(label: bannerText,
+ onDismiss: _dismissWarningBanner);
+
+ default:
+ return null;
+ }
+ }
+
@override
Widget build(BuildContext context) {
final Widget? body;
final errorBanner = _errorBannerComposingNotAllowed(context);
+ final warningBanner = _warningBanner(context);
+
if (errorBanner != null) {
return _ComposeBoxContainer(body: null, banner: errorBanner);
}
@@ -1603,6 +1698,6 @@ class _ComposeBoxState extends State with PerAccountStoreAwareStateM
// errorBanner = _ErrorBanner(label:
// ZulipLocalizations.of(context).errorSendMessageTimeout);
// }
- return _ComposeBoxContainer(body: body, banner: null);
+ return _ComposeBoxContainer(body: body, banner: warningBanner);
}
}
diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart
index ff9b2f7794..a0963d94a5 100644
--- a/lib/widgets/icons.dart
+++ b/lib/widgets/icons.dart
@@ -114,38 +114,41 @@ abstract final class ZulipIcons {
/// The Zulip custom icon "read_receipts".
static const IconData read_receipts = IconData(0xf11e, fontFamily: "Zulip Icons");
+ /// The Zulip custom icon "remove".
+ static const IconData remove = IconData(0xf11f, fontFamily: "Zulip Icons");
+
/// The Zulip custom icon "send".
- static const IconData send = IconData(0xf11f, fontFamily: "Zulip Icons");
+ static const IconData send = IconData(0xf120, fontFamily: "Zulip Icons");
/// The Zulip custom icon "settings".
- static const IconData settings = IconData(0xf120, fontFamily: "Zulip Icons");
+ static const IconData settings = IconData(0xf121, fontFamily: "Zulip Icons");
/// The Zulip custom icon "share".
- static const IconData share = IconData(0xf121, fontFamily: "Zulip Icons");
+ static const IconData share = IconData(0xf122, fontFamily: "Zulip Icons");
/// The Zulip custom icon "share_ios".
- static const IconData share_ios = IconData(0xf122, fontFamily: "Zulip Icons");
+ static const IconData share_ios = IconData(0xf123, fontFamily: "Zulip Icons");
/// The Zulip custom icon "smile".
- static const IconData smile = IconData(0xf123, fontFamily: "Zulip Icons");
+ static const IconData smile = IconData(0xf124, fontFamily: "Zulip Icons");
/// The Zulip custom icon "star".
- static const IconData star = IconData(0xf124, fontFamily: "Zulip Icons");
+ static const IconData star = IconData(0xf125, fontFamily: "Zulip Icons");
/// The Zulip custom icon "star_filled".
- static const IconData star_filled = IconData(0xf125, fontFamily: "Zulip Icons");
+ static const IconData star_filled = IconData(0xf126, fontFamily: "Zulip Icons");
/// The Zulip custom icon "three_person".
- static const IconData three_person = IconData(0xf126, fontFamily: "Zulip Icons");
+ static const IconData three_person = IconData(0xf127, fontFamily: "Zulip Icons");
/// The Zulip custom icon "topic".
- static const IconData topic = IconData(0xf127, fontFamily: "Zulip Icons");
+ static const IconData topic = IconData(0xf128, fontFamily: "Zulip Icons");
/// The Zulip custom icon "unmute".
- static const IconData unmute = IconData(0xf128, fontFamily: "Zulip Icons");
+ static const IconData unmute = IconData(0xf129, fontFamily: "Zulip Icons");
/// The Zulip custom icon "user".
- static const IconData user = IconData(0xf129, fontFamily: "Zulip Icons");
+ static const IconData user = IconData(0xf12a, fontFamily: "Zulip Icons");
// END GENERATED ICON DATA
}
diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart
index eea9677045..12354d2fe9 100644
--- a/lib/widgets/theme.dart
+++ b/lib/widgets/theme.dart
@@ -130,6 +130,7 @@ class DesignVariables extends ThemeExtension {
static final light = DesignVariables._(
background: const Color(0xffffffff),
bannerBgIntDanger: const Color(0xfff2e4e4),
+ bannerBgIntWarning: const Color(0xfffaf5dc),
bgBotBar: const Color(0xfff6f6f6),
bgContextMenu: const Color(0xfff2f2f2),
bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.15),
@@ -144,8 +145,10 @@ class DesignVariables extends ThemeExtension {
btnBgAttMediumIntInfoNormal: const Color(0xff3c6bff).withValues(alpha: 0.12),
btnLabelAttHigh: const Color(0xffffffff),
btnLabelAttLowIntDanger: const Color(0xffc0070a),
+ btnLabelAttLowIntWarning: const Color(0xffa96a05),
btnLabelAttMediumIntDanger: const Color(0xffac0508),
btnLabelAttMediumIntInfo: const Color(0xff1027a6),
+ btnLabelAttMediumIntWarning: const Color(0xff764607),
btnShadowAttMed: const Color(0xff000000).withValues(alpha: 0.20),
composeBoxBg: const Color(0xffffffff),
contextMenuCancelText: const Color(0xff222222),
@@ -187,6 +190,7 @@ class DesignVariables extends ThemeExtension {
static final dark = DesignVariables._(
background: const Color(0xff000000),
bannerBgIntDanger: const Color(0xff461616),
+ bannerBgIntWarning: const Color(0xff332b00),
bgBotBar: const Color(0xff222222),
bgContextMenu: const Color(0xff262626),
bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.37),
@@ -201,8 +205,10 @@ class DesignVariables extends ThemeExtension {
btnBgAttMediumIntInfoNormal: const Color(0xff97b6fe).withValues(alpha: 0.12),
btnLabelAttHigh: const Color(0xffffffff).withValues(alpha: 0.85),
btnLabelAttLowIntDanger: const Color(0xffff8b7c),
+ btnLabelAttLowIntWarning: const Color(0xffeba002),
btnLabelAttMediumIntDanger: const Color(0xffff8b7c),
btnLabelAttMediumIntInfo: const Color(0xff97b6fe),
+ btnLabelAttMediumIntWarning: const Color(0xfff8b325),
btnShadowAttMed: const Color(0xffffffff).withValues(alpha: 0.21),
composeBoxBg: const Color(0xff0f0f0f),
contextMenuCancelText: const Color(0xffffffff).withValues(alpha: 0.75),
@@ -252,6 +258,7 @@ class DesignVariables extends ThemeExtension {
DesignVariables._({
required this.background,
required this.bannerBgIntDanger,
+ required this.bannerBgIntWarning,
required this.bgBotBar,
required this.bgContextMenu,
required this.bgCounterUnread,
@@ -266,8 +273,10 @@ class DesignVariables extends ThemeExtension {
required this.btnBgAttMediumIntInfoNormal,
required this.btnLabelAttHigh,
required this.btnLabelAttLowIntDanger,
+ required this.btnLabelAttLowIntWarning,
required this.btnLabelAttMediumIntDanger,
required this.btnLabelAttMediumIntInfo,
+ required this.btnLabelAttMediumIntWarning,
required this.btnShadowAttMed,
required this.composeBoxBg,
required this.contextMenuCancelText,
@@ -318,6 +327,7 @@ class DesignVariables extends ThemeExtension {
final Color background;
final Color bannerBgIntDanger;
+ final Color bannerBgIntWarning;
final Color bgBotBar;
final Color bgContextMenu;
final Color bgCounterUnread;
@@ -332,8 +342,10 @@ class DesignVariables extends ThemeExtension {
final Color btnBgAttMediumIntInfoNormal;
final Color btnLabelAttHigh;
final Color btnLabelAttLowIntDanger;
+ final Color btnLabelAttLowIntWarning;
final Color btnLabelAttMediumIntDanger;
final Color btnLabelAttMediumIntInfo;
+ final Color btnLabelAttMediumIntWarning;
final Color btnShadowAttMed;
final Color composeBoxBg;
final Color contextMenuCancelText;
@@ -379,6 +391,7 @@ class DesignVariables extends ThemeExtension {
DesignVariables copyWith({
Color? background,
Color? bannerBgIntDanger,
+ Color? bannerBgIntWarning,
Color? bgBotBar,
Color? bgContextMenu,
Color? bgCounterUnread,
@@ -393,8 +406,10 @@ class DesignVariables extends ThemeExtension {
Color? btnBgAttMediumIntInfoNormal,
Color? btnLabelAttHigh,
Color? btnLabelAttLowIntDanger,
+ Color? btnLabelAttLowIntWarning,
Color? btnLabelAttMediumIntDanger,
Color? btnLabelAttMediumIntInfo,
+ Color? btnLabelAttMediumIntWarning,
Color? btnShadowAttMed,
Color? composeBoxBg,
Color? contextMenuCancelText,
@@ -435,6 +450,7 @@ class DesignVariables extends ThemeExtension {
return DesignVariables._(
background: background ?? this.background,
bannerBgIntDanger: bannerBgIntDanger ?? this.bannerBgIntDanger,
+ bannerBgIntWarning: bannerBgIntWarning ?? this.bannerBgIntWarning,
bgBotBar: bgBotBar ?? this.bgBotBar,
bgContextMenu: bgContextMenu ?? this.bgContextMenu,
bgCounterUnread: bgCounterUnread ?? this.bgCounterUnread,
@@ -449,8 +465,10 @@ class DesignVariables extends ThemeExtension {
btnBgAttMediumIntInfoNormal: btnBgAttMediumIntInfoNormal ?? this.btnBgAttMediumIntInfoNormal,
btnLabelAttHigh: btnLabelAttHigh ?? this.btnLabelAttHigh,
btnLabelAttLowIntDanger: btnLabelAttLowIntDanger ?? this.btnLabelAttLowIntDanger,
+ btnLabelAttLowIntWarning: btnLabelAttLowIntWarning ?? this.btnLabelAttLowIntWarning,
btnLabelAttMediumIntDanger: btnLabelAttMediumIntDanger ?? this.btnLabelAttMediumIntDanger,
btnLabelAttMediumIntInfo: btnLabelAttMediumIntInfo ?? this.btnLabelAttMediumIntInfo,
+ btnLabelAttMediumIntWarning: btnLabelAttMediumIntWarning ?? this.btnLabelAttMediumIntWarning,
btnShadowAttMed: btnShadowAttMed ?? this.btnShadowAttMed,
composeBoxBg: composeBoxBg ?? this.composeBoxBg,
contextMenuCancelText: contextMenuCancelText ?? this.contextMenuCancelText,
@@ -498,6 +516,7 @@ class DesignVariables extends ThemeExtension {
return DesignVariables._(
background: Color.lerp(background, other.background, t)!,
bannerBgIntDanger: Color.lerp(bannerBgIntDanger, other.bannerBgIntDanger, t)!,
+ bannerBgIntWarning: Color.lerp(bannerBgIntWarning, other.bannerBgIntWarning, t)!,
bgBotBar: Color.lerp(bgBotBar, other.bgBotBar, t)!,
bgContextMenu: Color.lerp(bgContextMenu, other.bgContextMenu, t)!,
bgCounterUnread: Color.lerp(bgCounterUnread, other.bgCounterUnread, t)!,
@@ -512,8 +531,10 @@ class DesignVariables extends ThemeExtension {
btnBgAttMediumIntInfoNormal: Color.lerp(btnBgAttMediumIntInfoNormal, other.btnBgAttMediumIntInfoNormal, t)!,
btnLabelAttHigh: Color.lerp(btnLabelAttHigh, other.btnLabelAttHigh, t)!,
btnLabelAttLowIntDanger: Color.lerp(btnLabelAttLowIntDanger, other.btnLabelAttLowIntDanger, t)!,
+ btnLabelAttLowIntWarning: Color.lerp(btnLabelAttLowIntWarning, other.btnLabelAttLowIntWarning, t)!,
btnLabelAttMediumIntDanger: Color.lerp(btnLabelAttMediumIntDanger, other.btnLabelAttMediumIntDanger, t)!,
btnLabelAttMediumIntInfo: Color.lerp(btnLabelAttMediumIntInfo, other.btnLabelAttMediumIntInfo, t)!,
+ btnLabelAttMediumIntWarning: Color.lerp(btnLabelAttMediumIntWarning, other.btnLabelAttMediumIntWarning, t)!,
btnShadowAttMed: Color.lerp(btnShadowAttMed, other.btnShadowAttMed, t)!,
composeBoxBg: Color.lerp(composeBoxBg, other.composeBoxBg, t)!,
contextMenuCancelText: Color.lerp(contextMenuCancelText, other.contextMenuCancelText, t)!,
diff --git a/test/example_data.dart b/test/example_data.dart
index f31337d303..17be894a30 100644
--- a/test/example_data.dart
+++ b/test/example_data.dart
@@ -920,6 +920,7 @@ InitialSnapshot initialSnapshot({
int? realmMessageContentEditLimitSeconds,
Map? realmDefaultExternalAccounts,
int? maxFileUploadSizeMib,
+ bool? realmEnableGuestUserDmWarning,
Uri? serverEmojiDataUrl,
String? realmEmptyTopicDisplayName,
List? realmUsers,
@@ -959,6 +960,7 @@ InitialSnapshot initialSnapshot({
realmMessageContentEditLimitSeconds: realmMessageContentEditLimitSeconds ?? 600,
realmDefaultExternalAccounts: realmDefaultExternalAccounts ?? {},
maxFileUploadSizeMib: maxFileUploadSizeMib ?? 25,
+ realmEnableGuestUserDmWarning: realmEnableGuestUserDmWarning ?? false,
serverEmojiDataUrl: serverEmojiDataUrl
?? realmUrl.replace(path: '/static/emoji.json'),
realmEmptyTopicDisplayName: realmEmptyTopicDisplayName ?? defaultRealmEmptyTopicDisplayName,