diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf
index 4fa457ba54..f6641a2a6e 100644
Binary files a/assets/icons/ZulipIcons.ttf and b/assets/icons/ZulipIcons.ttf differ
diff --git a/assets/icons/video.svg b/assets/icons/video.svg
new file mode 100644
index 0000000000..efeaa6d55a
--- /dev/null
+++ b/assets/icons/video.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb
index 45c7e6ca94..5a3c720556 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -574,6 +574,10 @@
"@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."
+ },
"composeBoxGenericContentHint": "Type a message",
"@composeBoxGenericContentHint": {
"description": "Hint text for content input when sending a message."
@@ -654,6 +658,10 @@
"filename": {"type": "String", "example": "file.txt"}
}
},
+ "composeBoxVideoCallLinkText": "Join video call.",
+ "@composeBoxVideoCallLinkText": {
+ "description": "Text for a Markdown link to a video-call URL."
+ },
"composeBoxLoadingMessage": "(loading message {messageId})",
"@composeBoxLoadingMessage": {
"description": "Placeholder in compose box showing the quoted message is currently loading.",
diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart
index eeedcde14d..f7f294f749 100644
--- a/lib/api/model/initial_snapshot.dart
+++ b/lib/api/model/initial_snapshot.dart
@@ -94,6 +94,10 @@ class InitialSnapshot {
final String realmName;
+ final int realmVideoChatProvider;
+
+ final String? realmJitsiServerUrl;
+
/// 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.
@@ -115,10 +119,14 @@ class InitialSnapshot {
final Map realmDefaultExternalAccounts;
+ final String? jitsiServerUrl;
+
final int maxFileUploadSizeMib;
final Uri serverEmojiDataUrl;
+ final String? serverJitsiServerUrl;
+
final String? realmEmptyTopicDisplayName; // TODO(server-10)
@JsonKey(readValue: _readUsersIsActiveFallbackTrue)
@@ -185,6 +193,8 @@ class InitialSnapshot {
required this.realmWildcardMentionPolicy,
required this.realmMandatoryTopics,
required this.realmName,
+ required this.realmVideoChatProvider,
+ required this.realmJitsiServerUrl,
required this.realmWaitingPeriodThreshold,
required this.realmMessageContentDeleteLimitSeconds,
required this.realmAllowMessageEditing,
@@ -193,8 +203,10 @@ class InitialSnapshot {
required this.realmIconUrl,
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 1c5505a653..3c6bcc9b0e 100644
--- a/lib/api/model/initial_snapshot.g.dart
+++ b/lib/api/model/initial_snapshot.g.dart
@@ -103,6 +103,8 @@ InitialSnapshot _$InitialSnapshotFromJson(
),
realmMandatoryTopics: json['realm_mandatory_topics'] as bool,
realmName: json['realm_name'] as String,
+ realmVideoChatProvider: (json['realm_video_chat_provider'] as num).toInt(),
+ realmJitsiServerUrl: json['realm_jitsi_server_url'] as String?,
realmWaitingPeriodThreshold: (json['realm_waiting_period_threshold'] as num)
.toInt(),
realmMessageContentDeleteLimitSeconds:
@@ -120,8 +122,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')
@@ -183,6 +187,8 @@ Map _$InitialSnapshotToJson(
'realm_wildcard_mention_policy': instance.realmWildcardMentionPolicy,
'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,
@@ -193,8 +199,10 @@ Map _$InitialSnapshotToJson(
'realm_icon_url': instance.realmIconUrl.toString(),
'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/lib/api/model/model.dart b/lib/api/model/model.dart
index da12823520..73273a2d32 100644
--- a/lib/api/model/model.dart
+++ b/lib/api/model/model.dart
@@ -213,6 +213,28 @@ class RealmEmojiItem {
Map toJson() => _$RealmEmojiItemToJson(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),
+ jitsi(apiValue: 1),
+ zoomUser(apiValue: 3),
+ bigBlueButton(apiValue: 4),
+ zoomServer(apiValue: 5),
+ unknown(apiValue: null);
+
+ const RealmVideoChatProvider({
+ required this.apiValue,
+ });
+
+ final int? apiValue;
+
+ int? toJson() => apiValue;
+}
+
/// A user's status, with [text] and [emoji] parts.
///
/// If a part is null, that part is empty/unset.
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index ce46ae6e7d..f86e0431b9 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -927,6 +927,12 @@ 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;
+
/// Hint text for content input when sending a message.
///
/// In en, this message translates to:
@@ -1029,6 +1035,12 @@ 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;
+
/// 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 d4c35968bf..9c7ce47807 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -485,6 +485,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 0a43ac50b5..cbd6b853eb 100644
--- a/lib/generated/l10n/zulip_localizations_de.dart
+++ b/lib/generated/l10n/zulip_localizations_de.dart
@@ -502,6 +502,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Ein Foto aufnehmen';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Eine Nachricht eingeben';
@@ -563,6 +566,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
return 'Lade $filename hoch…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 c7725a64e2..fecf066840 100644
--- a/lib/generated/l10n/zulip_localizations_el.dart
+++ b/lib/generated/l10n/zulip_localizations_el.dart
@@ -485,6 +485,9 @@ class ZulipLocalizationsEl extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsEl extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 334df16239..bb6d9d74a0 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -485,6 +485,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 f45d3db383..26ae7c59bb 100644
--- a/lib/generated/l10n/zulip_localizations_es.dart
+++ b/lib/generated/l10n/zulip_localizations_es.dart
@@ -485,6 +485,9 @@ class ZulipLocalizationsEs extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsEs extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 0d64c3b679..f374e019da 100644
--- a/lib/generated/l10n/zulip_localizations_fr.dart
+++ b/lib/generated/l10n/zulip_localizations_fr.dart
@@ -501,6 +501,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -560,6 +563,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 5e1609ccba..756ba02c5e 100644
--- a/lib/generated/l10n/zulip_localizations_he.dart
+++ b/lib/generated/l10n/zulip_localizations_he.dart
@@ -485,6 +485,9 @@ class ZulipLocalizationsHe extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsHe extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 aacb5d9d8d..f660bf3246 100644
--- a/lib/generated/l10n/zulip_localizations_hu.dart
+++ b/lib/generated/l10n/zulip_localizations_hu.dart
@@ -485,6 +485,9 @@ class ZulipLocalizationsHu extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsHu extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 4fc1ba6e42..6019341511 100644
--- a/lib/generated/l10n/zulip_localizations_it.dart
+++ b/lib/generated/l10n/zulip_localizations_it.dart
@@ -498,6 +498,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Fai una foto';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Batti un messaggio';
@@ -557,6 +560,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
return 'Caricamento $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 e779208dcf..7dfb7d4a5a 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -473,6 +473,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => '写真を撮る';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'メッセージを入力';
@@ -532,6 +535,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
return '$filename をアップロード中…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 3d9b7990e7..d5888981cc 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -485,6 +485,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 ea8d3b4fd8..dc7b9f521b 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -497,6 +497,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Zrób zdjęcie';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Wpisz wiadomość';
@@ -557,6 +560,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
return 'Przekazywanie $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 17f82a0d26..af365c4aa0 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -499,6 +499,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Сделать снимок';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Ввести сообщение';
@@ -558,6 +561,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
return 'Загрузка $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 092070d0f3..4adaa2a533 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -485,6 +485,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 f18f440748..e2c9488196 100644
--- a/lib/generated/l10n/zulip_localizations_sl.dart
+++ b/lib/generated/l10n/zulip_localizations_sl.dart
@@ -510,6 +510,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Fotografiraj';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Vnesite sporočilo';
@@ -569,6 +572,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
return 'Nalaganje $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 3810960b63..6c88162525 100644
--- a/lib/generated/l10n/zulip_localizations_uk.dart
+++ b/lib/generated/l10n/zulip_localizations_uk.dart
@@ -499,6 +499,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Зробити фото';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Ввести повідомлення';
@@ -558,6 +561,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
return 'Завантаження $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video 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 5db806ac0e..ae8fc55655 100644
--- a/lib/generated/l10n/zulip_localizations_zh.dart
+++ b/lib/generated/l10n/zulip_localizations_zh.dart
@@ -485,6 +485,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/model/realm.dart b/lib/model/realm.dart
index 5255e50623..7a66ceaae7 100644
--- a/lib/model/realm.dart
+++ b/lib/model/realm.dart
@@ -47,6 +47,10 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore {
GroupSettingValue? get realmCanDeleteOwnMessageGroup; // TODO(server-10)
bool get realmEnableReadReceipts;
bool get realmMandatoryTopics;
+ int get realmVideoChatProvider;
+ String? get realmJitsiServerUrl;
+ String? get jitsiServerUrl;
+ String? get serverJitsiServerUrl;
int get maxFileUploadSizeMib;
int? get realmMessageContentDeleteLimitSeconds;
Duration? get realmMessageContentEditLimit =>
@@ -176,6 +180,14 @@ mixin ProxyRealmStore on RealmStore {
@override
bool get realmMandatoryTopics => realmStore.realmMandatoryTopics;
@override
+ int get realmVideoChatProvider => realmStore.realmVideoChatProvider;
+ @override
+ String? get realmJitsiServerUrl => realmStore.realmJitsiServerUrl;
+ @override
+ String? get jitsiServerUrl => realmStore.jitsiServerUrl;
+ @override
+ String? get serverJitsiServerUrl => realmStore.serverJitsiServerUrl;
+ @override
int get maxFileUploadSizeMib => realmStore.maxFileUploadSizeMib;
@override
int? get realmMessageContentDeleteLimitSeconds => realmStore.realmMessageContentDeleteLimitSeconds;
@@ -234,6 +246,10 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
realmCanDeleteAnyMessageGroup = initialSnapshot.realmCanDeleteAnyMessageGroup,
realmCanDeleteOwnMessageGroup = initialSnapshot.realmCanDeleteOwnMessageGroup,
realmMandatoryTopics = initialSnapshot.realmMandatoryTopics,
+ realmVideoChatProvider = initialSnapshot.realmVideoChatProvider,
+ realmJitsiServerUrl = initialSnapshot.realmJitsiServerUrl,
+ jitsiServerUrl = initialSnapshot.jitsiServerUrl,
+ serverJitsiServerUrl = initialSnapshot.serverJitsiServerUrl,
maxFileUploadSizeMib = initialSnapshot.maxFileUploadSizeMib,
realmMessageContentDeleteLimitSeconds = initialSnapshot.realmMessageContentDeleteLimitSeconds,
realmMessageContentEditLimitSeconds = initialSnapshot.realmMessageContentEditLimitSeconds,
@@ -385,6 +401,14 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
@override
final bool realmMandatoryTopics;
@override
+ final int realmVideoChatProvider;
+ @override
+ final String? realmJitsiServerUrl;
+ @override
+ final String? jitsiServerUrl;
+ @override
+ final String? serverJitsiServerUrl;
+ @override
final int maxFileUploadSizeMib;
@override
final int? realmMessageContentDeleteLimitSeconds;
diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart
index 6e83356042..4d341ebfa3 100644
--- a/lib/widgets/compose_box.dart
+++ b/lib/widgets/compose_box.dart
@@ -1030,6 +1030,62 @@ Future _uploadFiles({
}
}
+class _AttachVideoChatUrlButton extends StatelessWidget {
+ const _AttachVideoChatUrlButton({
+ required this.controller,
+ required this.enabled,
+ });
+
+ final ComposeBoxController controller;
+ final bool enabled;
+
+ static const int jitsi = 1;
+ static const int zoomUser = 3;
+
+ String _generateJitsiUrl(String serverUrl, String visibleText) {
+ final id = List.generate(15, (_) => Random.secure().nextInt(10)).join();
+ return inlineLink(visibleText, '$serverUrl/$id#config.startWithVideoMuted=false');
+ }
+
+ String? _getMeetingUrl(ZulipLocalizations zulipLocalization, int? provider, String? jitsiServerUrl) {
+ final visibleText = zulipLocalization.composeBoxVideoCallLinkText;
+
+ switch (provider) {
+ case jitsi: return jitsiServerUrl == null ? null :_generateJitsiUrl(jitsiServerUrl, visibleText);
+ case zoomUser: return inlineLink(visibleText,
+ 'https://zoom.us/start/meeting');
+ default: return null;
+ }
+ }
+
+ void _handlePress(BuildContext context) {
+ final store = PerAccountStoreWidget.of(context);
+ final zulipLocalizations = ZulipLocalizations.of(context);
+
+ final placeholder = _getMeetingUrl(zulipLocalizations,
+ store.realmVideoChatProvider, store.jitsiServerUrl);
+ if (placeholder == null) return;
+
+ final contentController = controller.content;
+ final insertionRange = contentController.insertionIndex();
+ contentController.value = contentController.value.replaced(insertionRange, '$placeholder\n\n');
+ controller.contentFocusNode.requestFocus();
+ }
+
+ @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: enabled ? () => _handlePress(context) : null));
+ }
+}
+
abstract class _AttachUploadsButton extends StatelessWidget {
const _AttachUploadsButton({required this.controller, required this.enabled});
@@ -1442,6 +1498,7 @@ abstract class _ComposeBoxBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final store = PerAccountStoreWidget.of(context);
final themeData = Theme.of(context);
final designVariables = DesignVariables.of(context);
@@ -1469,6 +1526,9 @@ abstract class _ComposeBoxBody extends StatelessWidget {
_AttachFileButton(controller: controller, enabled: composeButtonsEnabled),
_AttachMediaButton(controller: controller, enabled: composeButtonsEnabled),
_AttachFromCameraButton(controller: controller, enabled: composeButtonsEnabled),
+ store.realmVideoChatProvider == 0
+ ? const SizedBox.shrink()
+ : _AttachVideoChatUrlButton(controller: controller, enabled: composeButtonsEnabled),
];
final topicInput = buildTopicInput();
diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart
index c9eb68361b..56e9842780 100644
--- a/lib/widgets/icons.dart
+++ b/lib/widgets/icons.dart
@@ -198,6 +198,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
}
diff --git a/test/example_data.dart b/test/example_data.dart
index a6e3e9655d..aeb36df825 100644
--- a/test/example_data.dart
+++ b/test/example_data.dart
@@ -1337,6 +1337,8 @@ InitialSnapshot initialSnapshot({
RealmWildcardMentionPolicy? realmWildcardMentionPolicy,
bool? realmMandatoryTopics,
String? realmName,
+ int? realmVideoChatProvider,
+ String? realmJitsiServerUrl,
int? realmWaitingPeriodThreshold,
int? realmMessageContentDeleteLimitSeconds,
bool? realmAllowMessageEditing,
@@ -1345,8 +1347,10 @@ InitialSnapshot initialSnapshot({
Uri? realmIconUrl,
bool? realmPresenceDisabled,
Map? realmDefaultExternalAccounts,
+ String? jitsiServerUrl,
int? maxFileUploadSizeMib,
Uri? serverEmojiDataUrl,
+ String? serverJitsiServerUrl,
String? realmEmptyTopicDisplayName,
List? realmUsers,
List? realmNonActiveUsers,
@@ -1401,6 +1405,8 @@ InitialSnapshot initialSnapshot({
realmWildcardMentionPolicy: realmWildcardMentionPolicy ?? RealmWildcardMentionPolicy.everyone,
realmMandatoryTopics: realmMandatoryTopics ?? true,
realmName: realmName ?? 'Example Zulip organization',
+ realmVideoChatProvider: realmVideoChatProvider ?? 1,
+ realmJitsiServerUrl: realmJitsiServerUrl,
realmWaitingPeriodThreshold: realmWaitingPeriodThreshold ?? 0,
realmMessageContentDeleteLimitSeconds: realmMessageContentDeleteLimitSeconds,
realmAllowMessageEditing: realmAllowMessageEditing ?? true,
@@ -1409,9 +1415,11 @@ InitialSnapshot initialSnapshot({
realmIconUrl: realmIconUrl ?? _realmIcon,
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 ?? [],
diff --git a/test/widgets/compose_box_test.dart b/test/widgets/compose_box_test.dart
index d27e374e3b..383c43022b 100644
--- a/test/widgets/compose_box_test.dart
+++ b/test/widgets/compose_box_test.dart
@@ -63,6 +63,8 @@ void main() {
List subscriptions = const [],
List? messages,
bool? mandatoryTopics,
+ int? realmVideoChatProvider,
+ String? jitsiServerUrl,
int? zulipFeatureLevel,
int? maxTopicLength,
}) async {
@@ -90,6 +92,8 @@ void main() {
subscriptions: subscriptions,
zulipFeatureLevel: zulipFeatureLevel,
realmMandatoryTopics: mandatoryTopics,
+ realmVideoChatProvider: realmVideoChatProvider,
+ jitsiServerUrl: jitsiServerUrl,
realmAllowMessageEditing: true,
realmMessageContentEditLimitSeconds: null,
maxTopicLength: maxTopicLength,
@@ -1050,6 +1054,61 @@ void main() {
});
});
+ group('video call button', () {
+ Future prepare(WidgetTester tester, {
+ String? jitsiServerUrl,
+ int? realmVideoChatProvider,
+ }) async {
+ TypingNotifier.debugEnable = false;
+ addTearDown(TypingNotifier.debugReset);
+
+ final channel = eg.stream();
+ final narrow = ChannelNarrow(channel.streamId);
+ await prepareComposeBox(tester,
+ narrow: narrow,
+ streams: [channel],
+ jitsiServerUrl : jitsiServerUrl,
+ realmVideoChatProvider : realmVideoChatProvider,
+ );
+
+ await enterTopic(tester, narrow: narrow, topic: 'some topic');
+ await tester.pump();
+ }
+
+ group('attach video call link', () {
+ testWidgets('Ensure no video call button when realmVideoChatProvider is 0', (tester) async {
+ await prepare(tester, realmVideoChatProvider: 0);
+ connection.prepare();
+
+ check(find.byIcon(ZulipIcons.video)).findsNothing();
+ });
+
+ 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');
+ });
+
+ testWidgets('zoom success', (tester) async {
+ await prepare(tester, jitsiServerUrl: '',
+ realmVideoChatProvider: 2);
+ connection.prepare();
+
+ await tester.tap(find.byIcon(ZulipIcons.video));
+ await tester.pump();
+
+ check(controller!.content.text)
+ .equals('[Join video call.](https://zoom.us/start/meeting)\n\n');
+ });
+ });
+ });
+
group('uploads', () {
void checkAppearsLoading(WidgetTester tester, bool expected) {
final sendButtonElement = tester.element(find.ancestor(
@@ -1329,6 +1388,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}) {