From 0832ed410c820bbc53be7d7e97c556f21683b8b9 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Fri, 17 Oct 2025 08:06:32 +0530 Subject: [PATCH 01/11] icons: Add 'video', from the Figma Figma: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3704-41538&t=udaDEYxIvoxfLwqQ-4 --- assets/icons/ZulipIcons.ttf | Bin 17968 -> 18180 bytes assets/icons/video.svg | 8 ++++++++ lib/widgets/icons.dart | 3 +++ 3 files changed, 11 insertions(+) create mode 100644 assets/icons/video.svg diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf index 4fa457ba54b862cf9e1a21f1b00a440eeede421b..f6641a2a6ec13b656806a05a3d1189e91475a79e 100644 GIT binary patch delta 2293 zcmbW3OKenS6vxjuFQ!9jJ41O?acF01@qvX7v^?hh?o6jl+v$uXMxj8VMEXDq78VSN ztdx*6lNdFc7&XRVVvIFLg%vTbTxeokutJR+<3bV=H(33BckVcK;l{c7-}Bw`KIc2% zz4P%kb@jTciV~6aaz(7NJ6kFzF8_ShDWdj^*pC%Uy8@MOCwGcC-bCx@@!6?`_j*0| zMVyoHx=)?Cc(U*MXV*pAdqo0QXQro4T=?ayPtd&!hGt+mm94`17U-RsU0nM0i@*NF z_5m>tpP4@%o_as^`pY71izs|FJGHc+=F||vtvq+nP0dbk`aBj8iGLts*|#u%Zt>gg*L;B=~+)+*yP)F5mwQBKMCaq%Y zw;i;7YWv#ugV?22`XwXJ$)Z@$4|7E%Dlth)O43MTaI!KiIo4L%)YNG5?eH^PAsIn0 zhFw;YRU9aBtucrjt`lODb%x`-;W$BHhxp_P*?>hz;zr06u2RS+(9Fw|XryEm`!T3h zobr(LN;fnl36d*9hmrK)I}7I!{;D?AM_6k0tuQtbwuDj)ewu3|%5l^)(ut0i=tR0@ z3q6UD_9#wu{gb$luu92h1i60N&pK?l&5@vsO8X=r+etn_2<<_qybUd#psxB6)(I)#VO+e} zBq%N-Rb97shQr$yYysCN{e-Kd~|sn+olV!atKW;hQa*Qq=#E&uQvo7F%fQN?t`Xj>cuJs=_2=5P=oMKS{f$QgWXoW(RRyGS^N zc7|uYw|Ut~l6o@2_{+&|(r?%Vj4m*2$or{0g+*NUuwqy-s9mvbuj;1hIf!x38cwu& zjE*D>hjpn=$3<=brL99ZHQN|g{XB+0y~%4&NvkmnjhM_qb8{T|w)hpfc0v|(c9DT6{Q z4KBz@S>Q6#Mo&#;_H+QEfJ6IJ7 ztlthcIY=T2a&K~iq?E-Xg=0OQo$?s>=E->+t0ek)>^qF9XuPWGucCQ6C+}Q2s*zs)0rdJL*z3`6}q{TG$l`UMMfjac`Qk}_Pz7GJxCk}k*Budp)e9p zrH6A^CPx#IiOFz2Qyhzqm&zzyuKm8wSu466)mEJxzcM&tA_ko@5rQ5wQGgydF#|J!2vXoi#BEoimYu&YOrp7fk3~IBSBh zCNGXU!q9Uj@=*N&)X0GI^pu;>NRbOB#-J~nh(ec4j6*M);L9kNOq5q%UH_%Ub&uw2 zNU%26wzR)1@qaFnl?QD<4pgo9VLq%CJ~jiq_hqB6O>J^_dexquKtzSr7O#q^LHFK% z)q~gdwuP1!&znEJY8L$ReiUH zKeU!Lvr<>zJ>zp(++L4baeLh@&vwHtqgv4h>V~H)zxu}6<&`_JXDs{&7)su-sHa!% TCbw7|_6w&^OwZT;NFVqc!!JxC delta 2108 zcmb7_OKenS6vxkd7kf{QW6 z1q(ug6OBvaN{u0eE_{TAA?)0^MB}2GXk2t*_Z9t}JKxy4aN*qi@AlzJ2A5J z3p1hI%_kpD<6sPhFY>vSMJsRhB5Yybb}5&i-St(pMly_PQyz0;-tpxOeEzdbc%I+;5#P{2~p~A)m?<%V!<2PFvqvo1Qk$gy*TZ%iHID z;C(EWQY){?fSi_P@kpZtc|sDFh{Pl=2}!aFA|I3#>k68*rLo1YgrDRI$PjuF>;}|c z`&zxtAnLld#Vb2p$BygRCa_Oll!H=7$bdxMka1kak&mP4$g60?C5`CLfes>h0p9~~M)2pALI@`#u6hfMU4)%LDT12>&oih*QBO)UIx5jb+GGzs36XXh zr&|9c?nA8NvKx7b%3{33ZtptL8)Z(&T?s3=E zIN^t=uNP+_ltI+k4ms~{@X85tve>=pxUJJOxGC+ zW^}wI()zTVlSM9@p0LAH8oBL>Xn#79>z&c==>+JIW=r*JHZge#Xa9DP zV6Ud_gw%7($N^Rv0>|An`boqle2Jrj+AJ^g-nv7tpdUl-px;20pnH7!(JFngb~?0c z*mW_^m(=U|vUt}QvHDlne15CAQFXC+%HMdn32S~G`MFa$ko z5QWYf#G&U5643JogHZm{T%@2E3i<*A;G)3@blxBa%^Relmkey^f* + + diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart index 0e17f584ce..d16eb967b3 100644 --- a/lib/widgets/icons.dart +++ b/lib/widgets/icons.dart @@ -202,6 +202,9 @@ abstract final class ZulipIcons { /// The Zulip custom icon "unmute". static const IconData unmute = IconData(0xf13a, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "video". + static const IconData video = IconData(0xf13b, fontFamily: "Zulip Icons"); + // END GENERATED ICON DATA } From 6a478c40ce4d4665840e6a67552bb8eb537f8eb3 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Sun, 23 Nov 2025 11:04:18 +0530 Subject: [PATCH 02/11] api: Add realmVideoChatProvider to InitialSnapshot --- lib/api/model/initial_snapshot.dart | 3 +++ lib/api/model/initial_snapshot.g.dart | 14 ++++++++++++++ lib/api/model/model.dart | 22 ++++++++++++++++++++++ test/example_data.dart | 2 ++ 4 files changed, 41 insertions(+) diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index 2f0257eaac..64c204a9f6 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -96,6 +96,8 @@ class InitialSnapshot { final String realmName; + final RealmVideoChatProvider realmVideoChatProvider; + /// 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. @@ -188,6 +190,7 @@ class InitialSnapshot { required this.realmWildcardMentionPolicy, required this.realmMandatoryTopics, required this.realmName, + required this.realmVideoChatProvider, required this.realmWaitingPeriodThreshold, required this.realmMessageContentDeleteLimitSeconds, required this.realmAllowMessageEditing, diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index dac7c141f7..e2d5b6337a 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -104,6 +104,10 @@ InitialSnapshot _$InitialSnapshotFromJson( ), realmMandatoryTopics: json['realm_mandatory_topics'] as bool, realmName: json['realm_name'] as String, + realmVideoChatProvider: $enumDecode( + _$RealmVideoChatProviderEnumMap, + json['realm_video_chat_provider'], + ), realmWaitingPeriodThreshold: (json['realm_waiting_period_threshold'] as num) .toInt(), realmMessageContentDeleteLimitSeconds: @@ -185,6 +189,7 @@ Map _$InitialSnapshotToJson( 'realm_wildcard_mention_policy': instance.realmWildcardMentionPolicy, 'realm_mandatory_topics': instance.realmMandatoryTopics, 'realm_name': instance.realmName, + 'realm_video_chat_provider': instance.realmVideoChatProvider, 'realm_waiting_period_threshold': instance.realmWaitingPeriodThreshold, 'realm_message_content_delete_limit_seconds': instance.realmMessageContentDeleteLimitSeconds, @@ -220,6 +225,15 @@ const _$RealmWildcardMentionPolicyEnumMap = { RealmWildcardMentionPolicy.moderators: 7, }; +const _$RealmVideoChatProviderEnumMap = { + RealmVideoChatProvider.none: 0, + RealmVideoChatProvider.jitsiMeet: 1, + RealmVideoChatProvider.zoomUserOAuth: 3, + RealmVideoChatProvider.bigBlueButton: 4, + RealmVideoChatProvider.zoomServerToServerOAuth: 5, + RealmVideoChatProvider.unknown: null, +}; + RealmDefaultExternalAccount _$RealmDefaultExternalAccountFromJson( Map json, ) => RealmDefaultExternalAccount( diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index c7ae2fb6ac..51dc25ed94 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -406,6 +406,28 @@ enum Emojiset { String toJson() => _$EmojisetEnumMap[this]!; } +/// As in [InitialSnapshot.realmVideoChatProvider]. +/// +/// For docs, search for "realm_video_chat_provider:" +/// in . +@JsonEnum(fieldRename: FieldRename.snake, valueField: "apiValue") +enum RealmVideoChatProvider { + none(apiValue: 0), + jitsiMeet(apiValue: 1), + zoomUserOAuth(apiValue: 3), + bigBlueButton(apiValue: 4), + zoomServerToServerOAuth(apiValue: 5), + unknown(apiValue: null); + + const RealmVideoChatProvider({ + required this.apiValue, + }); + + final int? apiValue; + + int? toJson() => apiValue; +} + /// As in [InitialSnapshot.realmUserGroups] or [UserGroupAddEvent]. @JsonSerializable(fieldRename: FieldRename.snake) class UserGroup { diff --git a/test/example_data.dart b/test/example_data.dart index c9beaef72f..5e32f82851 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -1358,6 +1358,7 @@ InitialSnapshot initialSnapshot({ RealmWildcardMentionPolicy? realmWildcardMentionPolicy, bool? realmMandatoryTopics, String? realmName, + RealmVideoChatProvider? realmVideoChatProvider, int? realmWaitingPeriodThreshold, int? realmMessageContentDeleteLimitSeconds, bool? realmAllowMessageEditing, @@ -1418,6 +1419,7 @@ InitialSnapshot initialSnapshot({ realmWildcardMentionPolicy: realmWildcardMentionPolicy ?? RealmWildcardMentionPolicy.everyone, realmMandatoryTopics: realmMandatoryTopics ?? true, realmName: realmName ?? 'Example Zulip organization', + realmVideoChatProvider: realmVideoChatProvider ?? RealmVideoChatProvider.jitsiMeet, realmWaitingPeriodThreshold: realmWaitingPeriodThreshold ?? 0, realmMessageContentDeleteLimitSeconds: realmMessageContentDeleteLimitSeconds, realmAllowMessageEditing: realmAllowMessageEditing ?? true, From bcb152d719fecc4261235ab6e74a5c73fa18fb05 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Mon, 1 Dec 2025 14:56:40 +0530 Subject: [PATCH 03/11] api: Add realmAvailableVideoChatProviders to InitialSnapshot --- lib/api/model/initial_snapshot.dart | 3 +++ lib/api/model/initial_snapshot.g.dart | 12 ++++++++++++ lib/api/model/model.dart | 20 ++++++++++++++++++++ lib/api/model/model.g.dart | 11 +++++++++++ test/example_data.dart | 2 ++ 5 files changed, 48 insertions(+) diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index 64c204a9f6..34c2162bd4 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -115,6 +115,8 @@ class InitialSnapshot { final Uri realmIconUrl; + final Map realmAvailableVideoChatProviders; + final bool realmPresenceDisabled; final Map realmDefaultExternalAccounts; @@ -197,6 +199,7 @@ class InitialSnapshot { required this.realmMessageContentEditLimitSeconds, required this.realmEnableReadReceipts, required this.realmIconUrl, + required this.realmAvailableVideoChatProviders, required this.realmPresenceDisabled, required this.realmDefaultExternalAccounts, required this.maxFileUploadSizeMib, diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index e2d5b6337a..54f018deb7 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -117,6 +117,16 @@ InitialSnapshot _$InitialSnapshotFromJson( (json['realm_message_content_edit_limit_seconds'] as num?)?.toInt(), realmEnableReadReceipts: json['realm_enable_read_receipts'] as bool, realmIconUrl: Uri.parse(json['realm_icon_url'] as String), + realmAvailableVideoChatProviders: + (json['realm_available_video_chat_providers'] as Map) + .map( + (k, e) => MapEntry( + k, + RealmAvailableVideoChatProviders.fromJson( + e as Map, + ), + ), + ), realmPresenceDisabled: json['realm_presence_disabled'] as bool, realmDefaultExternalAccounts: (json['realm_default_external_accounts'] as Map).map( @@ -198,6 +208,8 @@ Map _$InitialSnapshotToJson( instance.realmMessageContentEditLimitSeconds, 'realm_enable_read_receipts': instance.realmEnableReadReceipts, 'realm_icon_url': instance.realmIconUrl.toString(), + 'realm_available_video_chat_providers': + instance.realmAvailableVideoChatProviders, 'realm_presence_disabled': instance.realmPresenceDisabled, 'realm_default_external_accounts': instance.realmDefaultExternalAccounts, 'max_file_upload_size_mib': instance.maxFileUploadSizeMib, diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index 51dc25ed94..e84124555d 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -56,6 +56,26 @@ class GroupSettingValueNameless extends GroupSettingValue { Map toJson() => _$GroupSettingValueNamelessToJson(this); } +/// As in [InitialSnapshot.realmAvailableVideoChatProviders]. +/// +/// For docs, search for "realm_available_video_chat_providers:" +/// in . +@JsonSerializable(fieldRename: FieldRename.snake) +class RealmAvailableVideoChatProviders { + final String name; + final int id; + + RealmAvailableVideoChatProviders({ + required this.name, + required this.id, + }); + + factory RealmAvailableVideoChatProviders.fromJson(Map json) => + _$RealmAvailableVideoChatProvidersFromJson(json); + + Map toJson() => _$RealmAvailableVideoChatProvidersToJson(this); +} + /// As in [InitialSnapshot.customProfileFields]. /// /// For docs, search for "custom_profile_fields:" diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart index 95c5588392..3163d33eb6 100644 --- a/lib/api/model/model.g.dart +++ b/lib/api/model/model.g.dart @@ -26,6 +26,17 @@ Map _$GroupSettingValueNamelessToJson( 'direct_subgroups': instance.directSubgroups, }; +RealmAvailableVideoChatProviders _$RealmAvailableVideoChatProvidersFromJson( + Map json, +) => RealmAvailableVideoChatProviders( + name: json['name'] as String, + id: (json['id'] as num).toInt(), +); + +Map _$RealmAvailableVideoChatProvidersToJson( + RealmAvailableVideoChatProviders instance, +) => {'name': instance.name, 'id': instance.id}; + CustomProfileField _$CustomProfileFieldFromJson(Map json) => CustomProfileField( id: (json['id'] as num).toInt(), diff --git a/test/example_data.dart b/test/example_data.dart index 5e32f82851..ff4ef146a6 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -1365,6 +1365,7 @@ InitialSnapshot initialSnapshot({ int? realmMessageContentEditLimitSeconds, bool? realmEnableReadReceipts, Uri? realmIconUrl, + Map? realmAvailableVideoChatProviders, bool? realmPresenceDisabled, Map? realmDefaultExternalAccounts, int? maxFileUploadSizeMib, @@ -1426,6 +1427,7 @@ InitialSnapshot initialSnapshot({ realmMessageContentEditLimitSeconds: realmMessageContentEditLimitSeconds, realmEnableReadReceipts: realmEnableReadReceipts ?? true, realmIconUrl: realmIconUrl ?? _realmIcon, + realmAvailableVideoChatProviders: realmAvailableVideoChatProviders ?? {}, realmPresenceDisabled: realmPresenceDisabled ?? false, realmDefaultExternalAccounts: realmDefaultExternalAccounts ?? {}, maxFileUploadSizeMib: maxFileUploadSizeMib ?? 25, From 6bafa8c8561b982064b1a434a84ef4395f9e5756 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Mon, 1 Dec 2025 15:12:53 +0530 Subject: [PATCH 04/11] api: Add hasZoomToken to InitialSnapshot --- lib/api/model/initial_snapshot.dart | 3 +++ lib/api/model/initial_snapshot.g.dart | 2 ++ test/example_data.dart | 2 ++ 3 files changed, 7 insertions(+) diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index 34c2162bd4..d1c8de0d7e 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -76,6 +76,8 @@ class InitialSnapshot { final List userTopics; + final bool hasZoomToken; + final GroupSettingValue? realmCanDeleteAnyMessageGroup; // TODO(server-10) final GroupSettingValue? realmCanDeleteOwnMessageGroup; // TODO(server-10) @@ -186,6 +188,7 @@ class InitialSnapshot { required this.userStatuses, required this.userSettings, required this.userTopics, + required this.hasZoomToken, required this.realmCanDeleteAnyMessageGroup, required this.realmCanDeleteOwnMessageGroup, required this.realmDeleteOwnMessagePolicy, diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index 54f018deb7..a1c69e7f75 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -86,6 +86,7 @@ InitialSnapshot _$InitialSnapshotFromJson( userTopics: (json['user_topics'] as List) .map((e) => UserTopicItem.fromJson(e as Map)) .toList(), + hasZoomToken: json['has_zoom_token'] as bool, realmCanDeleteAnyMessageGroup: json['realm_can_delete_any_message_group'] == null ? null @@ -193,6 +194,7 @@ Map _$InitialSnapshotToJson( 'user_status': instance.userStatuses.map((k, e) => MapEntry(k.toString(), e)), 'user_settings': instance.userSettings, 'user_topics': instance.userTopics, + 'has_zoom_token': instance.hasZoomToken, 'realm_can_delete_any_message_group': instance.realmCanDeleteAnyMessageGroup, 'realm_can_delete_own_message_group': instance.realmCanDeleteOwnMessageGroup, 'realm_delete_own_message_policy': instance.realmDeleteOwnMessagePolicy, diff --git a/test/example_data.dart b/test/example_data.dart index ff4ef146a6..39952c2969 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -1352,6 +1352,7 @@ InitialSnapshot initialSnapshot({ Map? userStatuses, UserSettings? userSettings, List? userTopics, + bool? hasZoomToken, GroupSettingValue? realmCanDeleteAnyMessageGroup, GroupSettingValue? realmCanDeleteOwnMessageGroup, RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy, @@ -1414,6 +1415,7 @@ InitialSnapshot initialSnapshot({ userSettings: userSettings ?? _userSettings(), userTopics: userTopics ?? [], // no default; allow `null` to simulate servers without this + hasZoomToken: hasZoomToken ?? false, realmCanDeleteAnyMessageGroup: realmCanDeleteAnyMessageGroup, realmCanDeleteOwnMessageGroup: realmCanDeleteOwnMessageGroup, realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy, From b34d6fb929e7f51a25ceb532663aa318954b953d Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Mon, 1 Dec 2025 16:03:04 +0530 Subject: [PATCH 05/11] api: Add jitsi server url configurations to InitialSnapshot # Conflicts: # lib/api/model/initial_snapshot.g.dart # test/example_data.dart --- lib/api/model/initial_snapshot.dart | 9 +++++++++ lib/api/model/initial_snapshot.g.dart | 6 ++++++ test/example_data.dart | 6 ++++++ 3 files changed, 21 insertions(+) diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index d1c8de0d7e..b4bfd34445 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -100,6 +100,8 @@ class InitialSnapshot { final RealmVideoChatProvider realmVideoChatProvider; + final String? realmJitsiServerUrl; // TODO(server-8) + /// 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. @@ -123,10 +125,14 @@ class InitialSnapshot { final Map realmDefaultExternalAccounts; + final String? jitsiServerUrl; // TODO(server-8): Can ignore this deprecated field + final int maxFileUploadSizeMib; final Uri serverEmojiDataUrl; + final String? serverJitsiServerUrl; // TODO(server-8) + final String? realmEmptyTopicDisplayName; // TODO(server-10) @JsonKey(readValue: _readUsersIsActiveFallbackTrue) @@ -196,6 +202,7 @@ class InitialSnapshot { required this.realmMandatoryTopics, required this.realmName, required this.realmVideoChatProvider, + required this.realmJitsiServerUrl, required this.realmWaitingPeriodThreshold, required this.realmMessageContentDeleteLimitSeconds, required this.realmAllowMessageEditing, @@ -205,8 +212,10 @@ class InitialSnapshot { required this.realmAvailableVideoChatProviders, required this.realmPresenceDisabled, required this.realmDefaultExternalAccounts, + required this.jitsiServerUrl, required this.maxFileUploadSizeMib, required this.serverEmojiDataUrl, + required this.serverJitsiServerUrl, required this.realmEmptyTopicDisplayName, required this.realmUsers, required this.realmNonActiveUsers, diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index a1c69e7f75..9288769af8 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -109,6 +109,7 @@ InitialSnapshot _$InitialSnapshotFromJson( _$RealmVideoChatProviderEnumMap, json['realm_video_chat_provider'], ), + realmJitsiServerUrl: json['realm_jitsi_server_url'] as String?, realmWaitingPeriodThreshold: (json['realm_waiting_period_threshold'] as num) .toInt(), realmMessageContentDeleteLimitSeconds: @@ -136,8 +137,10 @@ InitialSnapshot _$InitialSnapshotFromJson( RealmDefaultExternalAccount.fromJson(e as Map), ), ), + jitsiServerUrl: json['jitsi_server_url'] as String?, maxFileUploadSizeMib: (json['max_file_upload_size_mib'] as num).toInt(), serverEmojiDataUrl: Uri.parse(json['server_emoji_data_url'] as String), + serverJitsiServerUrl: json['server_jitsi_server_url'] as String?, realmEmptyTopicDisplayName: json['realm_empty_topic_display_name'] as String?, realmUsers: (InitialSnapshot._readUsersIsActiveFallbackTrue(json, 'realm_users') @@ -202,6 +205,7 @@ Map _$InitialSnapshotToJson( 'realm_mandatory_topics': instance.realmMandatoryTopics, 'realm_name': instance.realmName, 'realm_video_chat_provider': instance.realmVideoChatProvider, + 'realm_jitsi_server_url': instance.realmJitsiServerUrl, 'realm_waiting_period_threshold': instance.realmWaitingPeriodThreshold, 'realm_message_content_delete_limit_seconds': instance.realmMessageContentDeleteLimitSeconds, @@ -214,8 +218,10 @@ Map _$InitialSnapshotToJson( instance.realmAvailableVideoChatProviders, 'realm_presence_disabled': instance.realmPresenceDisabled, 'realm_default_external_accounts': instance.realmDefaultExternalAccounts, + 'jitsi_server_url': instance.jitsiServerUrl, 'max_file_upload_size_mib': instance.maxFileUploadSizeMib, 'server_emoji_data_url': instance.serverEmojiDataUrl.toString(), + 'server_jitsi_server_url': instance.serverJitsiServerUrl, 'realm_empty_topic_display_name': instance.realmEmptyTopicDisplayName, 'realm_users': instance.realmUsers, 'realm_non_active_users': instance.realmNonActiveUsers, diff --git a/test/example_data.dart b/test/example_data.dart index 39952c2969..6ea5b55b8f 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -1360,6 +1360,7 @@ InitialSnapshot initialSnapshot({ bool? realmMandatoryTopics, String? realmName, RealmVideoChatProvider? realmVideoChatProvider, + String? realmJitsiServerUrl, int? realmWaitingPeriodThreshold, int? realmMessageContentDeleteLimitSeconds, bool? realmAllowMessageEditing, @@ -1369,8 +1370,10 @@ InitialSnapshot initialSnapshot({ Map? realmAvailableVideoChatProviders, bool? realmPresenceDisabled, Map? realmDefaultExternalAccounts, + String? jitsiServerUrl, int? maxFileUploadSizeMib, Uri? serverEmojiDataUrl, + String? serverJitsiServerUrl, String? realmEmptyTopicDisplayName, List? realmUsers, List? realmNonActiveUsers, @@ -1423,6 +1426,7 @@ InitialSnapshot initialSnapshot({ realmMandatoryTopics: realmMandatoryTopics ?? true, realmName: realmName ?? 'Example Zulip organization', realmVideoChatProvider: realmVideoChatProvider ?? RealmVideoChatProvider.jitsiMeet, + realmJitsiServerUrl: realmJitsiServerUrl, realmWaitingPeriodThreshold: realmWaitingPeriodThreshold ?? 0, realmMessageContentDeleteLimitSeconds: realmMessageContentDeleteLimitSeconds, realmAllowMessageEditing: realmAllowMessageEditing ?? true, @@ -1432,9 +1436,11 @@ InitialSnapshot initialSnapshot({ realmAvailableVideoChatProviders: realmAvailableVideoChatProviders ?? {}, realmPresenceDisabled: realmPresenceDisabled ?? false, realmDefaultExternalAccounts: realmDefaultExternalAccounts ?? {}, + jitsiServerUrl: jitsiServerUrl ?? 'https://meet.jit.si', maxFileUploadSizeMib: maxFileUploadSizeMib ?? 25, serverEmojiDataUrl: serverEmojiDataUrl ?? realmUrl.replace(path: '/static/emoji.json'), + serverJitsiServerUrl: serverJitsiServerUrl, realmEmptyTopicDisplayName: realmEmptyTopicDisplayName ?? defaultRealmEmptyTopicDisplayName, realmUsers: realmUsers ?? [selfUser], realmNonActiveUsers: realmNonActiveUsers ?? [], From db667eddf48ed28250dcdb00cbb2e4312e97e48e Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Mon, 1 Dec 2025 16:04:09 +0530 Subject: [PATCH 06/11] realm: Add RealmStore.realmAvailableVideoChatProviders --- lib/model/realm.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/model/realm.dart b/lib/model/realm.dart index 9acfbda757..a2e6849e29 100644 --- a/lib/model/realm.dart +++ b/lib/model/realm.dart @@ -74,6 +74,8 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore { // TODO(server-10) simplify this String get realmEmptyTopicDisplayName; + Map get realmAvailableVideoChatProviders; + Map get realmDefaultExternalAccounts; int get maxChannelNameLength; @@ -193,6 +195,8 @@ mixin ProxyRealmStore on RealmStore { @override String get realmEmptyTopicDisplayName => realmStore.realmEmptyTopicDisplayName; @override + Map get realmAvailableVideoChatProviders => realmStore.realmAvailableVideoChatProviders; + @override Map get realmDefaultExternalAccounts => realmStore.realmDefaultExternalAccounts; @override int get maxChannelNameLength => realmStore.maxChannelNameLength; @@ -246,6 +250,7 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { realmWildcardMentionPolicy = initialSnapshot.realmWildcardMentionPolicy, realmDeleteOwnMessagePolicy = initialSnapshot.realmDeleteOwnMessagePolicy, _realmEmptyTopicDisplayName = initialSnapshot.realmEmptyTopicDisplayName, + realmAvailableVideoChatProviders = initialSnapshot.realmAvailableVideoChatProviders, realmDefaultExternalAccounts = initialSnapshot.realmDefaultExternalAccounts, maxChannelNameLength = initialSnapshot.maxChannelNameLength, maxTopicLength = initialSnapshot.maxTopicLength, @@ -412,6 +417,9 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { } final String? _realmEmptyTopicDisplayName; + @override + final Map realmAvailableVideoChatProviders; + @override final Map realmDefaultExternalAccounts; From 205a2d199b283e651bb0ad4bfaa41ebe649be91d Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Fri, 19 Dec 2025 17:52:14 +0530 Subject: [PATCH 07/11] realm: Add realmStore.jitsiServerUrl and realmStore.realmVideoChatProvider Rearranged `realmEnableReadReceipts` in proper order under class `RealmStoreImpl` which was out of order previously. --- lib/model/realm.dart | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/model/realm.dart b/lib/model/realm.dart index a2e6849e29..b82f0f0139 100644 --- a/lib/model/realm.dart +++ b/lib/model/realm.dart @@ -32,6 +32,8 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore { Duration get serverTypingStartedWaitPeriod => Duration(milliseconds: serverTypingStartedWaitPeriodMilliseconds); int get serverTypingStartedWaitPeriodMilliseconds; + String? get serverJitsiServerUrl; + //|////////////////////////////////////////////////////////////// // Realm settings. @@ -46,6 +48,7 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore { GroupSettingValue? get realmCanDeleteAnyMessageGroup; // TODO(server-10) GroupSettingValue? get realmCanDeleteOwnMessageGroup; // TODO(server-10) bool get realmEnableReadReceipts; + String? get realmJitsiServerUrl; bool get realmMandatoryTopics; int get maxFileUploadSizeMib; int? get realmMessageContentDeleteLimitSeconds; @@ -54,6 +57,7 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore { : Duration(seconds: realmMessageContentEditLimitSeconds!); int? get realmMessageContentEditLimitSeconds; bool get realmPresenceDisabled; + RealmVideoChatProvider get realmVideoChatProvider; int get realmWaitingPeriodThreshold; //|////////////////////////////// @@ -81,6 +85,8 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore { int get maxChannelNameLength; int get maxTopicLength; + String? get jitsiServerUrl; + //|////////////////////////////// // Realm settings with their own events. @@ -169,6 +175,8 @@ mixin ProxyRealmStore on RealmStore { @override int get serverTypingStartedWaitPeriodMilliseconds => realmStore.serverTypingStartedWaitPeriodMilliseconds; @override + String? get serverJitsiServerUrl => realmStore.serverJitsiServerUrl; + @override bool get realmAllowMessageEditing => realmStore.realmAllowMessageEditing; @override GroupSettingValue? get realmCanDeleteAnyMessageGroup => realmStore.realmCanDeleteAnyMessageGroup; @@ -177,6 +185,8 @@ mixin ProxyRealmStore on RealmStore { @override bool get realmEnableReadReceipts => realmStore.realmEnableReadReceipts; @override + String? get realmJitsiServerUrl => realmStore.realmJitsiServerUrl; + @override bool get realmMandatoryTopics => realmStore.realmMandatoryTopics; @override int get maxFileUploadSizeMib => realmStore.maxFileUploadSizeMib; @@ -187,6 +197,8 @@ mixin ProxyRealmStore on RealmStore { @override bool get realmPresenceDisabled => realmStore.realmPresenceDisabled; @override + RealmVideoChatProvider get realmVideoChatProvider => realmStore.realmVideoChatProvider; + @override int get realmWaitingPeriodThreshold => realmStore.realmWaitingPeriodThreshold; @override RealmWildcardMentionPolicy get realmWildcardMentionPolicy => realmStore.realmWildcardMentionPolicy; @@ -203,6 +215,8 @@ mixin ProxyRealmStore on RealmStore { @override int get maxTopicLength => realmStore.maxTopicLength; @override + String? get jitsiServerUrl => realmStore.jitsiServerUrl; + @override List get customProfileFields => realmStore.customProfileFields; @override bool selfHasPassedWaitingPeriod({required DateTime byDate}) => @@ -237,15 +251,18 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { serverTypingStartedExpiryPeriodMilliseconds = initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds, serverTypingStoppedWaitPeriodMilliseconds = initialSnapshot.serverTypingStoppedWaitPeriodMilliseconds, serverTypingStartedWaitPeriodMilliseconds = initialSnapshot.serverTypingStartedWaitPeriodMilliseconds, + serverJitsiServerUrl = initialSnapshot.serverJitsiServerUrl, realmAllowMessageEditing = initialSnapshot.realmAllowMessageEditing, realmCanDeleteAnyMessageGroup = initialSnapshot.realmCanDeleteAnyMessageGroup, realmCanDeleteOwnMessageGroup = initialSnapshot.realmCanDeleteOwnMessageGroup, + realmEnableReadReceipts = initialSnapshot.realmEnableReadReceipts, + realmJitsiServerUrl = initialSnapshot.realmJitsiServerUrl, realmMandatoryTopics = initialSnapshot.realmMandatoryTopics, maxFileUploadSizeMib = initialSnapshot.maxFileUploadSizeMib, realmMessageContentDeleteLimitSeconds = initialSnapshot.realmMessageContentDeleteLimitSeconds, realmMessageContentEditLimitSeconds = initialSnapshot.realmMessageContentEditLimitSeconds, - realmEnableReadReceipts = initialSnapshot.realmEnableReadReceipts, realmPresenceDisabled = initialSnapshot.realmPresenceDisabled, + realmVideoChatProvider = initialSnapshot.realmVideoChatProvider, realmWaitingPeriodThreshold = initialSnapshot.realmWaitingPeriodThreshold, realmWildcardMentionPolicy = initialSnapshot.realmWildcardMentionPolicy, realmDeleteOwnMessagePolicy = initialSnapshot.realmDeleteOwnMessagePolicy, @@ -254,6 +271,7 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { realmDefaultExternalAccounts = initialSnapshot.realmDefaultExternalAccounts, maxChannelNameLength = initialSnapshot.maxChannelNameLength, maxTopicLength = initialSnapshot.maxTopicLength, + jitsiServerUrl = initialSnapshot.jitsiServerUrl, customProfileFields = _sortCustomProfileFields(initialSnapshot.customProfileFields); @override @@ -382,6 +400,8 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { final int serverTypingStoppedWaitPeriodMilliseconds; @override final int serverTypingStartedWaitPeriodMilliseconds; + @override + final String? serverJitsiServerUrl; @override final bool realmAllowMessageEditing; @@ -392,6 +412,8 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { @override final bool realmEnableReadReceipts; @override + final String? realmJitsiServerUrl; + @override final bool realmMandatoryTopics; @override final int maxFileUploadSizeMib; @@ -402,6 +424,8 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { @override final bool realmPresenceDisabled; @override + final RealmVideoChatProvider realmVideoChatProvider; + @override final int realmWaitingPeriodThreshold; @override @@ -428,6 +452,9 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore { @override final int maxTopicLength; + @override + final String? jitsiServerUrl; + @override List customProfileFields; From 358f94f013a65bedc585e61cee58d07ecefccf50 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Mon, 1 Dec 2025 16:23:27 +0530 Subject: [PATCH 08/11] store: Add getter for hasZoomToken --- lib/model/store.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/model/store.dart b/lib/model/store.dart index b051e75b72..ef7bdbab64 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -562,6 +562,7 @@ class PerAccountStore extends PerAccountStoreBase with emoji: EmojiStoreImpl(core: core, allRealmEmoji: initialSnapshot.realmEmoji), userSettings: initialSnapshot.userSettings, + hasZoomToken: initialSnapshot.hasZoomToken, pushDevices: PushDeviceManager(core: core), savedSnippets: SavedSnippetStoreImpl(core: core, savedSnippets: initialSnapshot.savedSnippets ?? []), @@ -586,6 +587,7 @@ class PerAccountStore extends PerAccountStoreBase with required RealmStoreImpl realm, required EmojiStoreImpl emoji, required this.userSettings, + required this.hasZoomToken, required this.pushDevices, required SavedSnippetStoreImpl savedSnippets, required this.typingNotifier, @@ -658,6 +660,8 @@ class PerAccountStore extends PerAccountStoreBase with final UserSettings userSettings; + bool hasZoomToken; + final PushDeviceManager pushDevices; @override From 6c4314132c17c399824f3868fbf6247be1593f20 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Thu, 11 Dec 2025 17:39:13 +0530 Subject: [PATCH 09/11] api: Add routes to fetch zoom and bigBlueButton call url --- lib/api/model/model.dart | 18 ++++++++++++++++++ lib/api/model/model.g.dart | 14 ++++++++++++++ lib/api/route/video_call.dart | 23 +++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 lib/api/route/video_call.dart diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index e84124555d..69bd493271 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -76,6 +76,24 @@ class RealmAvailableVideoChatProviders { Map toJson() => _$RealmAvailableVideoChatProvidersToJson(this); } +@JsonSerializable(fieldRename: FieldRename.snake) +class VideoCallResponse { + final String msg; + final String result; + final String url; + + VideoCallResponse({ + required this.msg, + required this.result, + required this.url, + }); + + factory VideoCallResponse.fromJson(Map json) => + _$VideoCallResponseFromJson(json); + + Map toJson() => _$VideoCallResponseToJson(this); +} + /// As in [InitialSnapshot.customProfileFields]. /// /// For docs, search for "custom_profile_fields:" diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart index 3163d33eb6..57708c00ba 100644 --- a/lib/api/model/model.g.dart +++ b/lib/api/model/model.g.dart @@ -37,6 +37,20 @@ Map _$RealmAvailableVideoChatProvidersToJson( RealmAvailableVideoChatProviders instance, ) => {'name': instance.name, 'id': instance.id}; +VideoCallResponse _$VideoCallResponseFromJson(Map json) => + VideoCallResponse( + msg: json['msg'] as String, + result: json['result'] as String, + url: json['url'] as String, + ); + +Map _$VideoCallResponseToJson(VideoCallResponse instance) => + { + 'msg': instance.msg, + 'result': instance.result, + 'url': instance.url, + }; + CustomProfileField _$CustomProfileFieldFromJson(Map json) => CustomProfileField( id: (json['id'] as num).toInt(), diff --git a/lib/api/route/video_call.dart b/lib/api/route/video_call.dart new file mode 100644 index 0000000000..18bbdc3b7e --- /dev/null +++ b/lib/api/route/video_call.dart @@ -0,0 +1,23 @@ +import '../core.dart'; +import '../model/model.dart'; + +/// Creates a Zoom video/audio call +/// +/// POST /api/v1/calls/zoom/create +Future createZoomCall(ApiConnection connection, { + required bool isVideoCall, +}) async { + return connection.post('createZoomCall', VideoCallResponse.fromJson, + '/calls/zoom/create', {'is_video_call': isVideoCall}); +} + +/// Creates a BigBlueButton meeting +/// +/// GET /api/v1/calls/bigbluebutton/create +Future createBigBlueButtonCall(ApiConnection connection, { + required String meetingName, + required bool voiceOnly, +}) async { + return connection.get('createBigBlueButtonCall', VideoCallResponse.fromJson, + '/calls/bigbluebutton/create', {'meeting_name': meetingName, 'voice_only': voiceOnly}); +} From ebc180af747bd18a5a897ec7b35604f6a20aa2d2 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Fri, 19 Dec 2025 18:26:03 +0530 Subject: [PATCH 10/11] api: Add event `HasZoomTokenEvent` --- lib/api/model/events.dart | 22 ++++++++++++++++++++++ lib/api/model/events.g.dart | 13 +++++++++++++ lib/model/store.dart | 3 +++ 3 files changed, 38 insertions(+) diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart index c3a2eb0126..b76ea7d9a1 100644 --- a/lib/api/model/events.dart +++ b/lib/api/model/events.dart @@ -29,6 +29,7 @@ sealed class Event { case 'update': return UserSettingsUpdateEvent.fromJson(json); default: return UnexpectedEvent.fromJson(json); } + case 'has_zoom_token': return HasZoomTokenEvent.fromJson(json); case 'custom_profile_fields': return CustomProfileFieldsEvent.fromJson(json); case 'user_group': switch (json['op'] as String) { @@ -1632,6 +1633,27 @@ enum ReactionOp { remove, } +/// A Zulip event of type `hasZoomToken`: https://zulip.com/api/get-events#has_zoom_token +@JsonSerializable(fieldRename: FieldRename.snake) +class HasZoomTokenEvent extends Event { + @override + @JsonKey(includeToJson: true) + String get type => 'has_zoom_token'; + + final bool value; + + HasZoomTokenEvent({ + required super.id, + required this.value, + }); + + factory HasZoomTokenEvent.fromJson(Map json) => + _$HasZoomTokenEventFromJson(json); + + @override + Map toJson() => _$HasZoomTokenEventToJson(this); +} + /// 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 fc0c3a95e2..dc633bfae2 100644 --- a/lib/api/model/events.g.dart +++ b/lib/api/model/events.g.dart @@ -1025,6 +1025,19 @@ const _$ReactionTypeEnumMap = { ReactionType.zulipExtraEmoji: 'zulip_extra_emoji', }; +HasZoomTokenEvent _$HasZoomTokenEventFromJson(Map json) => + HasZoomTokenEvent( + id: (json['id'] as num).toInt(), + value: json['value'] as bool, + ); + +Map _$HasZoomTokenEventToJson(HasZoomTokenEvent instance) => + { + 'id': instance.id, + 'type': instance.type, + 'value': instance.value, + }; + HeartbeatEvent _$HeartbeatEventFromJson(Map json) => HeartbeatEvent(id: (json['id'] as num).toInt()); diff --git a/lib/model/store.dart b/lib/model/store.dart index ef7bdbab64..07f7b095cb 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -796,6 +796,9 @@ class PerAccountStore extends PerAccountStoreBase with } notifyListeners(); + case HasZoomTokenEvent(): + assert(debugLog("server event: has_zoom_token")); + case CustomProfileFieldsEvent(): assert(debugLog("server event: custom_profile_fields")); _realm.handleCustomProfileFieldsEvent(event); From cf770f7076770ef4a0b5a9e4282f1e728d58ecc3 Mon Sep 17 00:00:00 2001 From: MritunjayTiwari14 Date: Fri, 19 Dec 2025 18:31:32 +0530 Subject: [PATCH 11/11] compose_box: Add `Add video call` button Fixes #1824. Generation and Insertion of url is based on similar logic as used in web. --- assets/l10n/app_en.arb | 20 ++ lib/generated/l10n/zulip_localizations.dart | 30 +++ .../l10n/zulip_localizations_ar.dart | 15 ++ .../l10n/zulip_localizations_de.dart | 15 ++ .../l10n/zulip_localizations_el.dart | 15 ++ .../l10n/zulip_localizations_en.dart | 15 ++ .../l10n/zulip_localizations_es.dart | 15 ++ .../l10n/zulip_localizations_fr.dart | 15 ++ .../l10n/zulip_localizations_he.dart | 15 ++ .../l10n/zulip_localizations_hu.dart | 15 ++ .../l10n/zulip_localizations_it.dart | 15 ++ .../l10n/zulip_localizations_ja.dart | 15 ++ .../l10n/zulip_localizations_nb.dart | 15 ++ .../l10n/zulip_localizations_pl.dart | 15 ++ .../l10n/zulip_localizations_ru.dart | 15 ++ .../l10n/zulip_localizations_sk.dart | 15 ++ .../l10n/zulip_localizations_sl.dart | 15 ++ .../l10n/zulip_localizations_uk.dart | 15 ++ .../l10n/zulip_localizations_zh.dart | 15 ++ lib/model/store.dart | 7 + lib/widgets/compose_box.dart | 217 ++++++++++++++++++ test/widgets/compose_box_test.dart | 47 ++++ 22 files changed, 576 insertions(+) diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 5afc9544d2..7003f4a12c 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -478,6 +478,10 @@ "@errorCouldNotEditMessageTitle": { "description": "Error title when an exception prevented us from opening the compose box for editing a message." }, + "errorCouldNotAppendCallUrl": "Fail to get call URL", + "@errorCouldNotAppendCallUrl": { + "description": "Error title when fetching a call URL failed." + }, "successLinkCopied": "Link copied", "@successLinkCopied": { "description": "Success message after copy link action completed." @@ -574,6 +578,14 @@ "@composeBoxAttachFromCameraTooltip": { "description": "Tooltip for compose box icon to attach an image from the camera to the message." }, + "composeBoxAddVideoCallTooltip": "Add video call", + "@composeBoxAddVideoCallTooltip": { + "description": "Tooltip for compose box icon to add a video call url to the message." + }, + "composeBoxAddVoiceCallTooltip": "Add voice call", + "@composeBoxAddVoiceCallTooltip": { + "description": "Tooltip for compose box icon to add a voice call url to the message." + }, "composeBoxGenericContentHint": "Type a message", "@composeBoxGenericContentHint": { "description": "Hint text for content input when sending a message." @@ -654,6 +666,14 @@ "filename": {"type": "String", "example": "file.txt"} } }, + "composeBoxVideoCallLinkText": "Join video call.", + "@composeBoxVideoCallLinkText": { + "description": "Text for a Markdown link to a video-call URL." + }, + "composeBoxVoiceCallLinkText": "Join voice call.", + "@composeBoxVoiceCallLinkText": { + "description": "Text for a Markdown link to a voice-call URL." + }, "composeBoxLoadingMessage": "(loading message {messageId})", "@composeBoxLoadingMessage": { "description": "Placeholder in compose box showing the quoted message is currently loading.", diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 7e0b9096a6..3d066095d6 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -783,6 +783,12 @@ abstract class ZulipLocalizations { /// **'Could not edit message'** String get errorCouldNotEditMessageTitle; + /// Error title when fetching a call URL failed. + /// + /// In en, this message translates to: + /// **'Fail to get call URL'** + String get errorCouldNotAppendCallUrl; + /// Success message after copy link action completed. /// /// In en, this message translates to: @@ -927,6 +933,18 @@ abstract class ZulipLocalizations { /// **'Take a photo'** String get composeBoxAttachFromCameraTooltip; + /// Tooltip for compose box icon to add a video call url to the message. + /// + /// In en, this message translates to: + /// **'Add video call'** + String get composeBoxAddVideoCallTooltip; + + /// Tooltip for compose box icon to add a voice call url to the message. + /// + /// In en, this message translates to: + /// **'Add voice call'** + String get composeBoxAddVoiceCallTooltip; + /// Hint text for content input when sending a message. /// /// In en, this message translates to: @@ -1029,6 +1047,18 @@ abstract class ZulipLocalizations { /// **'Uploading {filename}…'** String composeBoxUploadingFilename(String filename); + /// Text for a Markdown link to a video-call URL. + /// + /// In en, this message translates to: + /// **'Join video call.'** + String get composeBoxVideoCallLinkText; + + /// Text for a Markdown link to a voice-call URL. + /// + /// In en, this message translates to: + /// **'Join voice call.'** + String get composeBoxVoiceCallLinkText; + /// Placeholder in compose box showing the quoted message is currently loading. /// /// 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 0de3988707..9099abc085 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -406,6 +406,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Could not edit message'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link copied'; @@ -485,6 +488,12 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -544,6 +553,12 @@ class ZulipLocalizationsAr extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart index e1f57cd9ae..3ad409d7a6 100644 --- a/lib/generated/l10n/zulip_localizations_de.dart +++ b/lib/generated/l10n/zulip_localizations_de.dart @@ -423,6 +423,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations { String get errorCouldNotEditMessageTitle => 'Konnte Nachricht nicht bearbeiten'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link kopiert'; @@ -502,6 +505,12 @@ class ZulipLocalizationsDe extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Ein Foto aufnehmen'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Eine Nachricht eingeben'; @@ -563,6 +572,12 @@ class ZulipLocalizationsDe extends ZulipLocalizations { return 'Lade $filename hoch…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(lade Nachricht $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_el.dart b/lib/generated/l10n/zulip_localizations_el.dart index 37510410f9..8cf0c276c7 100644 --- a/lib/generated/l10n/zulip_localizations_el.dart +++ b/lib/generated/l10n/zulip_localizations_el.dart @@ -406,6 +406,9 @@ class ZulipLocalizationsEl extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Could not edit message'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link copied'; @@ -485,6 +488,12 @@ class ZulipLocalizationsEl extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -544,6 +553,12 @@ class ZulipLocalizationsEl extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index feb32d69e5..c4f5405f13 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -406,6 +406,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Could not edit message'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link copied'; @@ -485,6 +488,12 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -544,6 +553,12 @@ class ZulipLocalizationsEn extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_es.dart b/lib/generated/l10n/zulip_localizations_es.dart index 163fb4e6a4..d6553cf0c2 100644 --- a/lib/generated/l10n/zulip_localizations_es.dart +++ b/lib/generated/l10n/zulip_localizations_es.dart @@ -406,6 +406,9 @@ class ZulipLocalizationsEs extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Could not edit message'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link copied'; @@ -485,6 +488,12 @@ class ZulipLocalizationsEs extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -544,6 +553,12 @@ class ZulipLocalizationsEs extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index 979a819efc..fd82fdc848 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -422,6 +422,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations { String get errorCouldNotEditMessageTitle => 'Le message n\'a pas pu être modifié'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Lien copié'; @@ -501,6 +504,12 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -560,6 +569,12 @@ class ZulipLocalizationsFr extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_he.dart b/lib/generated/l10n/zulip_localizations_he.dart index 4a813be1b0..f51998cdda 100644 --- a/lib/generated/l10n/zulip_localizations_he.dart +++ b/lib/generated/l10n/zulip_localizations_he.dart @@ -406,6 +406,9 @@ class ZulipLocalizationsHe extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Could not edit message'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link copied'; @@ -485,6 +488,12 @@ class ZulipLocalizationsHe extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -544,6 +553,12 @@ class ZulipLocalizationsHe extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_hu.dart b/lib/generated/l10n/zulip_localizations_hu.dart index 6b3805bf99..942706dc28 100644 --- a/lib/generated/l10n/zulip_localizations_hu.dart +++ b/lib/generated/l10n/zulip_localizations_hu.dart @@ -406,6 +406,9 @@ class ZulipLocalizationsHu extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Could not edit message'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link copied'; @@ -485,6 +488,12 @@ class ZulipLocalizationsHu extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -544,6 +553,12 @@ class ZulipLocalizationsHu extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart index 0c06cd4b38..270ddee6f9 100644 --- a/lib/generated/l10n/zulip_localizations_it.dart +++ b/lib/generated/l10n/zulip_localizations_it.dart @@ -417,6 +417,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations { String get errorCouldNotEditMessageTitle => 'Impossibile modificare il messaggio'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Collegamento copiato'; @@ -498,6 +501,12 @@ class ZulipLocalizationsIt extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Fai una foto'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Batti un messaggio'; @@ -557,6 +566,12 @@ class ZulipLocalizationsIt extends ZulipLocalizations { return 'Caricamento $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(caricamento messaggio $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index d59e294c8a..269bb7c833 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -398,6 +398,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'メッセージを編集できませんでした'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'リンクをコピーしました'; @@ -473,6 +476,12 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => '写真を撮る'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'メッセージを入力'; @@ -532,6 +541,12 @@ class ZulipLocalizationsJa extends ZulipLocalizations { return '$filename をアップロード中…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(メッセージ $messageId を読み込み中)'; diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart index d828f44d95..17b7dc6f2e 100644 --- a/lib/generated/l10n/zulip_localizations_nb.dart +++ b/lib/generated/l10n/zulip_localizations_nb.dart @@ -406,6 +406,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Could not edit message'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link copied'; @@ -485,6 +488,12 @@ class ZulipLocalizationsNb extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -544,6 +553,12 @@ class ZulipLocalizationsNb extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index 88fbc24a57..f25939f7e5 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -418,6 +418,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Nie można zmienić wiadomości'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Skopiowano odnośnik'; @@ -497,6 +500,12 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Zrób zdjęcie'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Wpisz wiadomość'; @@ -557,6 +566,12 @@ class ZulipLocalizationsPl extends ZulipLocalizations { return 'Przekazywanie $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(ładowanie wiadomości $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 06cfea774d..e6db692880 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -420,6 +420,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Сбой редактирования'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Ссылка скопирована'; @@ -499,6 +502,12 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Сделать снимок'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Ввести сообщение'; @@ -558,6 +567,12 @@ class ZulipLocalizationsRu extends ZulipLocalizations { return 'Загрузка $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(загрузка сообщения $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart index 4841efd265..e7b09e1e56 100644 --- a/lib/generated/l10n/zulip_localizations_sk.dart +++ b/lib/generated/l10n/zulip_localizations_sk.dart @@ -406,6 +406,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Could not edit message'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link copied'; @@ -485,6 +488,12 @@ class ZulipLocalizationsSk extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -544,6 +553,12 @@ class ZulipLocalizationsSk extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart index c4c6d91ed1..a797eb9b92 100644 --- a/lib/generated/l10n/zulip_localizations_sl.dart +++ b/lib/generated/l10n/zulip_localizations_sl.dart @@ -429,6 +429,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Sporočila ni mogoče urediti'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Povezava je bila kopirana'; @@ -510,6 +513,12 @@ class ZulipLocalizationsSl extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Fotografiraj'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Vnesite sporočilo'; @@ -569,6 +578,12 @@ class ZulipLocalizationsSl extends ZulipLocalizations { return 'Nalaganje $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(nalaganje sporočila $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart index e202787969..e382834e7a 100644 --- a/lib/generated/l10n/zulip_localizations_uk.dart +++ b/lib/generated/l10n/zulip_localizations_uk.dart @@ -419,6 +419,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations { String get errorCouldNotEditMessageTitle => 'Не вдалося редагувати повідомлення'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Посилання скопійовано'; @@ -499,6 +502,12 @@ class ZulipLocalizationsUk extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Зробити фото'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Ввести повідомлення'; @@ -558,6 +567,12 @@ class ZulipLocalizationsUk extends ZulipLocalizations { return 'Завантаження $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(завантаження повідомлення $messageId)'; diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart index 292a511d57..de1f2e75ea 100644 --- a/lib/generated/l10n/zulip_localizations_zh.dart +++ b/lib/generated/l10n/zulip_localizations_zh.dart @@ -406,6 +406,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get errorCouldNotEditMessageTitle => 'Could not edit message'; + @override + String get errorCouldNotAppendCallUrl => 'Fail to get call URL'; + @override String get successLinkCopied => 'Link copied'; @@ -485,6 +488,12 @@ class ZulipLocalizationsZh extends ZulipLocalizations { @override String get composeBoxAttachFromCameraTooltip => 'Take a photo'; + @override + String get composeBoxAddVideoCallTooltip => 'Add video call'; + + @override + String get composeBoxAddVoiceCallTooltip => 'Add voice call'; + @override String get composeBoxGenericContentHint => 'Type a message'; @@ -544,6 +553,12 @@ class ZulipLocalizationsZh extends ZulipLocalizations { return 'Uploading $filename…'; } + @override + String get composeBoxVideoCallLinkText => 'Join video call.'; + + @override + String get composeBoxVoiceCallLinkText => 'Join voice call.'; + @override String composeBoxLoadingMessage(int messageId) { return '(loading message $messageId)'; diff --git a/lib/model/store.dart b/lib/model/store.dart index 07f7b095cb..1de0798d4a 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -17,6 +17,7 @@ import '../api/route/events.dart'; import '../api/backoff.dart'; import '../api/route/realm.dart'; import '../log.dart'; +import '../widgets/compose_box.dart'; import 'actions.dart'; import 'autocomplete.dart'; import 'database.dart'; @@ -798,6 +799,12 @@ class PerAccountStore extends PerAccountStoreBase with case HasZoomTokenEvent(): assert(debugLog("server event: has_zoom_token")); + final hasZoomTokenUpdated = event.value; + if (hasZoomToken != hasZoomTokenUpdated) { + hasZoomToken = hasZoomTokenUpdated; + if(hasZoomToken) await ComposeCall.handleHasZoomTokenEvent(); + notifyListeners(); + } case CustomProfileFieldsEvent(): assert(debugLog("server event: custom_profile_fields")); diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 52faeaf151..a341e9aad6 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -7,10 +7,12 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart' as path; +import 'package:url_launcher/url_launcher.dart'; import '../api/exception.dart'; import '../api/model/model.dart'; import '../api/route/messages.dart'; +import '../api/route/video_call.dart'; import '../generated/l10n/zulip_localizations.dart'; import '../model/binding.dart'; import '../model/compose.dart'; @@ -1034,6 +1036,220 @@ Future _uploadFiles({ } } +class ComposeCall { + static final Map Function()> _zoomTokenCallbacks = {}; + static void clearZoomCallbacks(int? editMessageId) { + final key = editMessageId?.toString() ?? ''; + _zoomTokenCallbacks.remove(key); + } + + static Future handleHasZoomTokenEvent() async { + final callbacks = Map Function()>.from(_zoomTokenCallbacks); + _zoomTokenCallbacks.clear(); + for (final callback in callbacks.values) { + await callback(); + } + } + + static int generateRandomId(int min, int max) { + return min + (DateTime.now().microsecondsSinceEpoch % (max - min)); + } +} + +class _AddComposeCallUrlButton extends StatefulWidget { + const _AddComposeCallUrlButton({ + required this.controller, + required this.enabled, + required this.isVideoCall, + }); + final ComposeBoxController controller; + final bool enabled; + final bool isVideoCall; + + @override + State<_AddComposeCallUrlButton> createState() => _AddComposeCallUrlButtonState(); +} + +class _AddComposeCallUrlButtonState extends State<_AddComposeCallUrlButton> { + + static String getJitsiServerUrl(PerAccountStore store) { + return store.realmJitsiServerUrl ?? store.serverJitsiServerUrl ?? + store.jitsiServerUrl ?? 'https://meet.jit.si'; + } + + Future _openZoomOAuthWindow() async { + final store = PerAccountStoreWidget.of(context); + final url = store.realmUrl.resolve('/calls/zoom/register'); + + await launchUrl(url, mode: LaunchMode.platformDefault); + } + + void _insertCallUrl(String url, String visibleText) { + final placeholder = inlineLink(visibleText, url); + final contentController = widget.controller.content; + final insertionRange = contentController.insertionIndex(); + contentController.value = contentController.value.replaced(insertionRange, '$placeholder\n\n'); + widget.controller.contentFocusNode.requestFocus(); + } + + void _handleJitsiCall({ + required ComposeContentController contentController, + required bool isAudioCall, + }) { + final store = PerAccountStoreWidget.of(context); + final zulipLocalization = ZulipLocalizations.of(context); + final videoCallId = ComposeCall.generateRandomId(100000000000000, 999999999999999); + final jitsiServerUrl = getJitsiServerUrl(store); + final videoCallLink = '$jitsiServerUrl/$videoCallId'; + + if (!isAudioCall) { + _insertCallUrl('$videoCallLink#config.startWithVideoMuted=false', + zulipLocalization.composeBoxVideoCallLinkText); + } else { + _insertCallUrl('$videoCallLink#config.startWithVideoMuted=true', + zulipLocalization.composeBoxVideoCallLinkText); + } + + } + + Future _createBigBlueButtonCall ({ + required ComposeContentController contentController, + required bool voiceOnly, + }) async { + final zulipLocalization = ZulipLocalizations.of(context); + final store = PerAccountStoreWidget.of(context); + try { + final connection = store.connection; + final result = await createBigBlueButtonCall( + connection, meetingName: "Null", //TODO: Fetch message stream title + voiceOnly: voiceOnly); + + if (!voiceOnly) { + _insertCallUrl(result.url, zulipLocalization.composeBoxVideoCallLinkText); + } else { + _insertCallUrl(result.url, zulipLocalization.composeBoxAddVoiceCallTooltip); + } + } on ApiRequestException catch (e) { + if (!mounted) return; + final zulipLocalizations = ZulipLocalizations.of(context); + final message = switch (e) { + ZulipApiException() => zulipLocalizations.errorServerMessage(e.message), + _ => e.message, + }; + showErrorDialog(context: context, + title: zulipLocalizations.errorCouldNotAppendCallUrl, + message: message); + return; + } + } + + + + Future handleZoomCall({ + required ComposeContentController contentController, + required bool isVideoCall, + required bool isServerToServer, + int? editMessageId, + }) async { + final store = PerAccountStoreWidget.of(context); + final key = editMessageId?.toString() ?? ''; + ComposeCall.clearZoomCallbacks(editMessageId); + Future prepareZoomCall() async { + final zulipLocalization = ZulipLocalizations.of(context); + final store = PerAccountStoreWidget.of(context); + try { + final connection = store.connection; + final result = await createZoomCall(connection, isVideoCall: isVideoCall); + + if (isVideoCall) { + _insertCallUrl(result.url, zulipLocalization.composeBoxVideoCallLinkText); + } else { + _insertCallUrl(result.url, zulipLocalization.composeBoxAddVoiceCallTooltip); + } + + } on ApiRequestException catch (e) { + if (!mounted) return; + store.hasZoomToken = false; + ComposeCall.clearZoomCallbacks(editMessageId); + final zulipLocalizations = ZulipLocalizations.of(context); + final message = switch (e) { + ZulipApiException() => zulipLocalizations.errorServerMessage(e.message), + _ => e.message, + }; + showErrorDialog(context: context, + title: zulipLocalizations.errorCouldNotAppendCallUrl, + message: message); + return; + } + + } + + if (store.hasZoomToken || isServerToServer) { + await prepareZoomCall(); + } else { + ComposeCall._zoomTokenCallbacks[key] = prepareZoomCall; + await _openZoomOAuthWindow(); + } + + } + + + Future generateComposeCallUrl({ + required ComposeContentController contentController, + required bool isAudioCall, + int? editMessageId, + }) async { + final store = PerAccountStoreWidget.of(context); + final realmAvailableVideoChatProviders = store.realmAvailableVideoChatProviders; + final realmVideoChatProvider = store.realmVideoChatProvider; + + final providerIsZoom = realmAvailableVideoChatProviders['zoom'] != null && + realmVideoChatProvider.apiValue == realmAvailableVideoChatProviders['zoom']!.id; + final providerIsZoomServerToServer = + realmAvailableVideoChatProviders['zoom_server_to_server'] != null && + realmVideoChatProvider.apiValue == realmAvailableVideoChatProviders['zoom_server_to_server']!.id; + + if (providerIsZoom || providerIsZoomServerToServer) { + await handleZoomCall( + contentController: contentController, + isVideoCall: !isAudioCall, + isServerToServer: providerIsZoomServerToServer, + ); + } else if (realmAvailableVideoChatProviders['big_blue_button'] != null && + realmVideoChatProvider.apiValue == realmAvailableVideoChatProviders['big_blue_button']!.id) { + await _createBigBlueButtonCall( + contentController: contentController, + voiceOnly: isAudioCall, + ); + } else { + _handleJitsiCall( + contentController: contentController, + isAudioCall: isAudioCall, + ); + } + } + + + Future _handlePress(BuildContext context) async { + final contentController = widget.controller.content; + await generateComposeCallUrl(contentController: contentController, + isAudioCall: !widget.isVideoCall, editMessageId: null); + } + + @override + Widget build(BuildContext context) { + final designVariables = DesignVariables.of(context); + final zulipLocalizations = ZulipLocalizations.of(context); + + return SizedBox( + width: _composeButtonSize, + child: IconButton( + icon: Icon(ZulipIcons.video, color: designVariables.foreground.withFadedAlpha(0.5)), + tooltip: zulipLocalizations.composeBoxAddVideoCallTooltip, + onPressed: widget.enabled ? () => _handlePress(context) : null)); + } +} + abstract class _AttachUploadsButton extends StatelessWidget { const _AttachUploadsButton({required this.controller, required this.enabled}); @@ -1473,6 +1689,7 @@ abstract class _ComposeBoxBody extends StatelessWidget { _AttachFileButton(controller: controller, enabled: composeButtonsEnabled), _AttachMediaButton(controller: controller, enabled: composeButtonsEnabled), _AttachFromCameraButton(controller: controller, enabled: composeButtonsEnabled), + _AddComposeCallUrlButton(controller: controller, enabled: composeButtonsEnabled, isVideoCall: true), ]; final topicInput = buildTopicInput(); diff --git a/test/widgets/compose_box_test.dart b/test/widgets/compose_box_test.dart index a847120691..121eb018aa 100644 --- a/test/widgets/compose_box_test.dart +++ b/test/widgets/compose_box_test.dart @@ -62,7 +62,13 @@ void main() { List? streams, List subscriptions = const [], List? messages, + bool? hasZoomToken, bool? mandatoryTopics, + RealmVideoChatProvider? realmVideoChatProvider, + String? realmJitsiServerUrl, + Map? realmAvailableVideoChatProviders, + String? jitsiServerUrl, + String? serverJitsiServerUrl, int? zulipFeatureLevel, int? maxTopicLength, }) async { @@ -90,6 +96,8 @@ void main() { subscriptions: subscriptions, zulipFeatureLevel: zulipFeatureLevel, realmMandatoryTopics: mandatoryTopics, + realmVideoChatProvider: realmVideoChatProvider, + jitsiServerUrl: jitsiServerUrl, realmAllowMessageEditing: true, realmMessageContentEditLimitSeconds: null, maxTopicLength: maxTopicLength, @@ -1053,6 +1061,44 @@ void main() { }); }); + group('compose call button', () { + Future prepare(WidgetTester tester, { + String? jitsiServerUrl, + RealmVideoChatProvider? realmVideoChatProvider, + bool? hasZoomToken, + }) async { + TypingNotifier.debugEnable = false; + addTearDown(TypingNotifier.debugReset); + + final channel = eg.stream(); + final narrow = ChannelNarrow(channel.streamId); + await prepareComposeBox(tester, + narrow: narrow, + streams: [channel], + realmVideoChatProvider : realmVideoChatProvider, + jitsiServerUrl : jitsiServerUrl, + hasZoomToken: hasZoomToken, + ); + + await enterTopic(tester, narrow: narrow, topic: 'some topic'); + await tester.pump(); + } + + group('add video call link', () { + testWidgets('jitsi success', (tester) async { + await prepare(tester); + connection.prepare(); + + await tester.tap(find.byIcon(ZulipIcons.video)); + await tester.pump(); + check(controller!.content.text) + ..startsWith('[Join video call.](https://meet.jit.si') + ..endsWith('#config.startWithVideoMuted=false)\n\n'); + }); + + }); + }); + group('uploads', () { void checkAppearsLoading(WidgetTester tester, bool expected) { final sendButtonElement = tester.element(find.ancestor( @@ -1332,6 +1378,7 @@ void main() { check(attachButtonFinder(ZulipIcons.attach_file).evaluate().length).equals(areShown ? 1 : 0); check(attachButtonFinder(ZulipIcons.image).evaluate().length).equals(areShown ? 1 : 0); check(attachButtonFinder(ZulipIcons.camera).evaluate().length).equals(areShown ? 1 : 0); + check(attachButtonFinder(ZulipIcons.video).evaluate().length).equals(areShown ? 1 : 0); } void checkBannerWithLabel(String label, {required bool isShown}) {