From 41e1a3510e41964c6830c9dbdd503aacf1e36d1e Mon Sep 17 00:00:00 2001 From: chimnayajith <chinmayajith30@gmail.com> Date: Wed, 23 Apr 2025 01:46:27 +0530 Subject: [PATCH 1/3] icons: Add remove icon Taken from the "Icons" page in Figma: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=4031-32875&t=6FcBjNTASZoepny4-4 --- assets/icons/ZulipIcons.ttf | Bin 13840 -> 13980 bytes assets/icons/remove.svg | 3 +++ lib/widgets/icons.dart | 25 ++++++++++++++----------- 3 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 assets/icons/remove.svg diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf index df2c4ab94724dc1d2f78fde5515534a5c5dad87a..84182be3bbe0d49a1a45a6bd566855e4deac20cf 100644 GIT binary patch delta 1818 zcmb7FOKcle6g}f_;@Gj1eyH0L0&yVqZ#*9VCbkpXNr9>oRHUG)s@j^=jtg;P(hw2} zF^EMa5UN^D7c3$n1Zu?&H9}$$v0}vn6^RuqWC06=s&qkBRUkNT{L&V7`031jpZh)c z&G^dVM%#n}U;yvJiPUp*FV~JAdg&uTDFDvzUYt|YuiTou1-SdDb#}g8?-YMM9tJ#X z+>0zMt~XmpP8<gO6TtX(t5H9<`pY*rsrwU=YjLAbam{kRMMPTdm9-m3{{D-$Eyf5f zF3p$gpa1cRhZ$^8;e&R4t)rY$ay;C}_2`9qyRql1N*YLW?Z|hQF0P#X)bs(FV_YA- zx-}M<wJTs1rOdUDul}wnv(|{RU^WrJC_cjvxT{2zW6Foh7fRQ0%JH*f+xa})@FR(5 zaT+UdAcQzaf;%eG(2+rwvp90dqrkb71U=MvxbNnEmLrBq>Z!EL+4c&2<|76v+qDK4 zytZS*cC0b52Tx&yiRyHbvP0^0rSp86ng*VrhK>^LrznN~K8`+WYfP<1ndg3(o)fey z+Zv+?vN$ur1WHs8l~k#k;7rFLT_l)en$M(Ee}?J>YU>;-1M8d{Jl8Nmo)X;2(YXsg z?8noze2isiv=DkZdfiPe5lN)7hjR@@Buva;FO4*kl8~7XF>^t4jV@KzTO#ppJC!0$ zMRg+>CZ#ck7jz2eF`kR?yU_n&e}_LW?I-Bv1)M|^9VRE*5n~J$X0I2bn<<RZxL_ZM z3Uf@sz>`cyr+SuaL8if0xF2Xm?3#Mr9Az0gby67TtQSCpj`RMUVBlrELv8{#BU5&P z8dTB$o$J$Y6D&GgW_UsLPDlMrLJ-}%LZahdvJm6BAUDQSky6icKyoF#1el0uOoeyf zMGGfaS<ZZjVwQXyfElEj!O`oB%HTVW1S1ET(!b9$T#J+abR(**;c?DtblYpqZX9AD zjj8qaqC7Qt7H0|po7=Sg-t>w#1{ttuD+4aas`aO9T8UaE_m*|V?XvE<O?BITZ-L*K zz4!*jDfwb6+jRUj*FIM7;5dWxSicjsu6xEbm4S-%oMPY%n^qvlW!{hy6BB$?>BV5m zHNG!N-oT1=-!tcq(yPZ(JoN*#FHWZPOuj(7bV*B0SK~&uHkF*ISl@f!vqB!X>1S*> zGvdkL3~@$6rL0TDC|{K*QqD_EQl67YQ8pxW%BF<yy&#dN<hRfUzqvSXiU+}OAYPX! zQ!YyITZpzqiSmMkM!6)BpzKIYQ@$ZlrCgSXQ(lxXC|4xPSx(-Rs8Oy;Oi{iik)-55 zfDOUWx<qC3a{q0`cZZA%AZoomP(Avs|L)-B;NIYC!TX`pp^c&D(9Pkq!}tFG05)Cz zVz@8njPV9zm-S=lV7BXz_`7_B1Kn;Q{K(un68P_yJf@$*_?LtHr?{<Oawvy4S2Dwj Q*R$McFReDLOWF7T0bx50L;wH) delta 1706 zcmb7^OKcle6o$|El_qhVXG<TD8qz>wPwes7u_tz%*bZtzR3L~b<)H?Xrb$KIm^73` zsu;uq!7kOHuDSrx1uGV0Q&mwnR0t3YgjllZrWFemsbT|_5PWxhLtu$Vzt5d}&*MM; zy>qWMHkVy7M5I&Rlb~29W?!tmF!y|1#BfA{x1O6#+oyh*{Z=IOJ6^BPH|ou&j=t=P zM9zwY4=gTkoO|t$XZl5=A(7$DrG@(0OLuR7iSHedUt(gP5t?DV4UR1}*496J`maBU zJxz`1^2&U<el_^%1sGh$;loCKy=lB^<XJq(d;CJZv9SL}#TLnYED|U*S1zvIeem1Q zBH8bGuU*+5>YDL8po^ErZhd~`*Nida4H}E?De029T$kH&&loT&#%1FZ<Hx|sz)yiY zK}W(8m6RNpGqNTD>5(LlDPzoK#Fnh&7$s#~@={<NM4&y7z4<WnIi7?};Flt9+>bY; z!~Iu4+D~msNZS0wj-S|qcSr`IX_FxB7qm%avpR*J1F?u4<k`o%1(hl`&-x+aOgtxj zkGOQxj7hx`>xy^<PuFB<ON<mIoH9NIl`t-k<6gks=1Eb?X6&$T$rI>eGBZx{kaWml zd~JCM+7=PYZ=9?Hc&So~%6`U{6p5<I0b(pPFcmEq-n}qavQ|l%qQeqGhkYtVLKT@o z=|jQ<<&}IYd4hFSzF*q^ncvl`jrb9|epX(QbJCPyU;YF&cBNOFmzKwfbqj>69#vYC z0&&RL6v*+e#B}6Q$n1Iz`8~CE?<jrP_@w0+BSk5R0zG6oE~hEHD(|9Am#;|KZ_uL3 z-l=F;`nsf0JERtDIya?^jY2{R-8wRAkK0fo!MYMR!K$LvitN-{sVrR(Q5!SupP2{; zQf;r=qJ%t&9!EF>HrX8B)wZO>RVT8QNJ;WArT^=Y<z1~D*4<X+5k^(gwzg&fsUuJ= z>A6u>9oCXi=%P%LjZ)u9>l<-sw&?qER|J~IrWXsBjZyDpc-xCc-1LtBk&65Rbv{+- z2O%%U4q4UJ&_U|~o;f)$8}~=On~|YRiYi6&rm32xtpc?ssak@S5~BhK2L@H79pn5> zd$G1z(nq}BNc{-W#fh|?%@>Hulq_?ql5}#_@>p%!yWh6%#UtHrl*$quQUCmEU``{2 zt!pH(Z)g;;^BNOau0bDZ?1Bcj6{n<yEOt?YTTPZU3fS|m`UB(OO^r$HvPK5m&?sTK zCVg1g6%7;H)R@A)rBT7IY9z52HMrNg@>(e87`&}f#a_}VW8cvj!>(&6g*G&%x30JE z7~Zwc>WQyoXJYrdi{0P#q<WTn^S$r)S$*I9|7U9?S~|4#)$kWFuJF~|2U`zveTLVc I|HzQP0mKdIWB>pF 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 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166 4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166 19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289 17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" fill="black"/> +</svg> \ No newline at end of file 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 } From 1ee4f1ff55eb9361bb25d63f412f50f0067a2a27 Mon Sep 17 00:00:00 2001 From: chimnayajith <chinmayajith30@gmail.com> Date: Wed, 23 Apr 2025 02:26:32 +0530 Subject: [PATCH 2/3] api: Add RealmUpdateEvent and guest DM warning flag - Add realmEnableGuestUserDmWarning to PerAccountStore - Handle RealmUpdateEvent to keep setting in sync with server - Support feature level 348+ for realm configuration --- lib/api/model/events.dart | 45 +++++++++++++++ lib/api/model/events.g.dart | 25 +++++++++ lib/api/model/initial_snapshot.dart | 4 ++ lib/api/model/initial_snapshot.g.dart | 80 ++++++++++++++------------- lib/model/store.dart | 16 ++++++ test/example_data.dart | 2 + 6 files changed, 134 insertions(+), 38 deletions(-) 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<String, dynamic> json) => + _$RealmUpdateEventFromJson(json); + + @override + Map<String, dynamic> 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<String, dynamic> json) => + RealmUpdateEvent( + id: (json['id'] as num).toInt(), + property: $enumDecodeNullable( + _$RealmPropertyNameEnumMap, + json['property'], + unknownValue: JsonKey.nullForUndefinedEnumValue, + ), + value: json['value'], + ); + +Map<String, dynamic> _$RealmUpdateEventToJson(RealmUpdateEvent instance) => + <String, dynamic>{ + '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<String, dynamic> 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<String, dynamic> _$InitialSnapshotToJson(InitialSnapshot instance) => - <String, dynamic>{ - '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<String, dynamic> _$InitialSnapshotToJson( + InitialSnapshot instance, +) => <String, dynamic>{ + '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/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/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<String, RealmDefaultExternalAccount>? realmDefaultExternalAccounts, int? maxFileUploadSizeMib, + bool? realmEnableGuestUserDmWarning, Uri? serverEmojiDataUrl, String? realmEmptyTopicDisplayName, List<User>? 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, From b3edf00a54d252e20dd8afae9bd9756c41f32e5f Mon Sep 17 00:00:00 2001 From: chimnayajith <chinmayajith30@gmail.com> Date: Wed, 23 Apr 2025 02:27:32 +0530 Subject: [PATCH 3/3] compose: Implement guest user DM warning banner --- assets/l10n/app_en.arb | 15 +++ lib/generated/l10n/zulip_localizations.dart | 12 +++ .../l10n/zulip_localizations_ar.dart | 10 ++ .../l10n/zulip_localizations_en.dart | 10 ++ .../l10n/zulip_localizations_ja.dart | 10 ++ .../l10n/zulip_localizations_nb.dart | 10 ++ .../l10n/zulip_localizations_pl.dart | 10 ++ .../l10n/zulip_localizations_ru.dart | 10 ++ .../l10n/zulip_localizations_sk.dart | 10 ++ lib/widgets/compose_box.dart | 97 ++++++++++++++++++- lib/widgets/theme.dart | 21 ++++ 11 files changed, 214 insertions(+), 1 deletion(-) 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/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/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<ComposeBox> 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<ComposeBox> 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<ComposeBox> 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<ComposeBox> 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/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<DesignVariables> { 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<DesignVariables> { 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<DesignVariables> { 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<DesignVariables> { 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> { 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<DesignVariables> { 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<DesignVariables> { 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<DesignVariables> { 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> { DesignVariables copyWith({ Color? background, Color? bannerBgIntDanger, + Color? bannerBgIntWarning, Color? bgBotBar, Color? bgContextMenu, Color? bgCounterUnread, @@ -393,8 +406,10 @@ class DesignVariables extends ThemeExtension<DesignVariables> { 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<DesignVariables> { 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<DesignVariables> { 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<DesignVariables> { 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<DesignVariables> { 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)!,