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..40b1f5ebb0 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."
},
+ "composeBoxAttachFromVideoCallTooltip": "Attach a video call",
+ "@composeBoxAttachFromVideoCallTooltip": {
+ "description": "Tooltip for compose box icon to attach 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"}
}
},
+ "composeBoxUploadedVideoCallUrl": "Join video call.",
+ "@composeBoxUploadedVideoCallUrl": {
+ "description": "Placeholder in compose box showing the video call url is generated."
+ },
"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..92934ee5e6 100644
--- a/lib/api/model/initial_snapshot.dart
+++ b/lib/api/model/initial_snapshot.dart
@@ -90,6 +90,8 @@ class InitialSnapshot {
/// Search for "realm_wildcard_mention_policy" in https://zulip.com/api/register-queue.
final RealmWildcardMentionPolicy realmWildcardMentionPolicy;
+ final int realmVideoChatProvider;
+
final bool realmMandatoryTopics;
final String realmName;
@@ -115,6 +117,8 @@ class InitialSnapshot {
final Map realmDefaultExternalAccounts;
+ final String? jitsiServerUrl;
+
final int maxFileUploadSizeMib;
final Uri serverEmojiDataUrl;
@@ -184,6 +188,7 @@ class InitialSnapshot {
required this.realmDeleteOwnMessagePolicy,
required this.realmWildcardMentionPolicy,
required this.realmMandatoryTopics,
+ required this.realmVideoChatProvider,
required this.realmName,
required this.realmWaitingPeriodThreshold,
required this.realmMessageContentDeleteLimitSeconds,
@@ -193,6 +198,7 @@ class InitialSnapshot {
required this.realmIconUrl,
required this.realmPresenceDisabled,
required this.realmDefaultExternalAccounts,
+ required this.jitsiServerUrl,
required this.maxFileUploadSizeMib,
required this.serverEmojiDataUrl,
required this.realmEmptyTopicDisplayName,
diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart
index 1c5505a653..33a8c95823 100644
--- a/lib/api/model/initial_snapshot.g.dart
+++ b/lib/api/model/initial_snapshot.g.dart
@@ -102,6 +102,7 @@ InitialSnapshot _$InitialSnapshotFromJson(
json['realm_wildcard_mention_policy'],
),
realmMandatoryTopics: json['realm_mandatory_topics'] as bool,
+ realmVideoChatProvider: (json['realm_video_chat_provider'] as num).toInt(),
realmName: json['realm_name'] as String,
realmWaitingPeriodThreshold: (json['realm_waiting_period_threshold'] as num)
.toInt(),
@@ -120,6 +121,7 @@ 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),
realmEmptyTopicDisplayName: json['realm_empty_topic_display_name'] as String?,
@@ -181,6 +183,7 @@ Map _$InitialSnapshotToJson(
'realm_can_delete_own_message_group': instance.realmCanDeleteOwnMessageGroup,
'realm_delete_own_message_policy': instance.realmDeleteOwnMessagePolicy,
'realm_wildcard_mention_policy': instance.realmWildcardMentionPolicy,
+ 'realm_video_chat_provider': instance.realmVideoChatProvider,
'realm_mandatory_topics': instance.realmMandatoryTopics,
'realm_name': instance.realmName,
'realm_waiting_period_threshold': instance.realmWaitingPeriodThreshold,
@@ -193,6 +196,7 @@ 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(),
'realm_empty_topic_display_name': instance.realmEmptyTopicDisplayName,
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index ce46ae6e7d..4385e762b6 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 attach a video call url to the message.
+ ///
+ /// In en, this message translates to:
+ /// **'Attach a video call'**
+ String get composeBoxAttachFromVideoCallTooltip;
+
/// 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);
+ /// Placeholder in compose box showing the video call url is generated.
+ ///
+ /// In en, this message translates to:
+ /// **'Join video call.'**
+ String get composeBoxUploadedVideoCallUrl;
+
/// 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..ff0cf32831 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..9e07433f1a 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Eine Nachricht eingeben';
@@ -563,6 +566,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
return 'Lade $filename hoch…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..3300f5f695 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsEl extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..cf32b9b4f8 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..d580734ec8 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsEs extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..b83299e704 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -560,6 +563,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..2846092c89 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsHe extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..f30594d0a1 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsHu extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..0d0cccb566 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Batti un messaggio';
@@ -557,6 +560,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
return 'Caricamento $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..e6326fe98f 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'メッセージを入力';
@@ -532,6 +535,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
return '$filename をアップロード中…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..1f7b0877e4 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..e1cdaec0f6 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Wpisz wiadomość';
@@ -557,6 +560,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
return 'Przekazywanie $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..b82013f671 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Ввести сообщение';
@@ -558,6 +561,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
return 'Загрузка $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..06d5c4f45d 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..696c4949ae 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Vnesite sporočilo';
@@ -569,6 +572,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
return 'Nalaganje $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..d764cb1070 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Ввести повідомлення';
@@ -558,6 +561,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
return 'Завантаження $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..844d5cd5f4 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 composeBoxAttachFromVideoCallTooltip => 'Attach a video call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +547,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxUploadedVideoCallUrl => '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..18367a6901 100644
--- a/lib/model/realm.dart
+++ b/lib/model/realm.dart
@@ -47,6 +47,8 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore {
GroupSettingValue? get realmCanDeleteOwnMessageGroup; // TODO(server-10)
bool get realmEnableReadReceipts;
bool get realmMandatoryTopics;
+ String? get jitsiServerUrl;
+ int get realmVideoChatProvider;
int get maxFileUploadSizeMib;
int? get realmMessageContentDeleteLimitSeconds;
Duration? get realmMessageContentEditLimit =>
@@ -176,6 +178,10 @@ mixin ProxyRealmStore on RealmStore {
@override
bool get realmMandatoryTopics => realmStore.realmMandatoryTopics;
@override
+ int get realmVideoChatProvider => realmStore.realmVideoChatProvider;
+ @override
+ String? get jitsiServerUrl => realmStore.jitsiServerUrl;
+ @override
int get maxFileUploadSizeMib => realmStore.maxFileUploadSizeMib;
@override
int? get realmMessageContentDeleteLimitSeconds => realmStore.realmMessageContentDeleteLimitSeconds;
@@ -234,6 +240,8 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
realmCanDeleteAnyMessageGroup = initialSnapshot.realmCanDeleteAnyMessageGroup,
realmCanDeleteOwnMessageGroup = initialSnapshot.realmCanDeleteOwnMessageGroup,
realmMandatoryTopics = initialSnapshot.realmMandatoryTopics,
+ realmVideoChatProvider = initialSnapshot.realmVideoChatProvider,
+ jitsiServerUrl = initialSnapshot.jitsiServerUrl,
maxFileUploadSizeMib = initialSnapshot.maxFileUploadSizeMib,
realmMessageContentDeleteLimitSeconds = initialSnapshot.realmMessageContentDeleteLimitSeconds,
realmMessageContentEditLimitSeconds = initialSnapshot.realmMessageContentEditLimitSeconds,
@@ -385,6 +393,10 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
@override
final bool realmMandatoryTopics;
@override
+ final int realmVideoChatProvider;
+ @override
+ final String? jitsiServerUrl;
+ @override
final int maxFileUploadSizeMib;
@override
final int? realmMessageContentDeleteLimitSeconds;
diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart
index 1b00654b8f..8debc7be3d 100644
--- a/lib/widgets/compose_box.dart
+++ b/lib/widgets/compose_box.dart
@@ -1030,6 +1030,63 @@ 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 zoom = 2; //TODO: Add other supported video chat providers
+
+ 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.composeBoxUploadedVideoCallUrl;
+
+ switch (provider) {
+ case 0: return null; //TODO: Implement feedback no video chat provider is setup
+ case jitsi: return jitsiServerUrl == null ? null :_generateJitsiUrl(jitsiServerUrl, visibleText);
+ case zoom: 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.composeBoxAttachFromVideoCallTooltip,
+ onPressed: enabled ? () => _handlePress(context) : null));
+ }
+}
+
abstract class _AttachUploadsButton extends StatelessWidget {
const _AttachUploadsButton({required this.controller, required this.enabled});
@@ -1442,6 +1499,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 +1527,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..c3ecde2207 100644
--- a/test/example_data.dart
+++ b/test/example_data.dart
@@ -1336,6 +1336,7 @@ InitialSnapshot initialSnapshot({
RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy,
RealmWildcardMentionPolicy? realmWildcardMentionPolicy,
bool? realmMandatoryTopics,
+ int? realmVideoChatProvider,
String? realmName,
int? realmWaitingPeriodThreshold,
int? realmMessageContentDeleteLimitSeconds,
@@ -1345,6 +1346,7 @@ InitialSnapshot initialSnapshot({
Uri? realmIconUrl,
bool? realmPresenceDisabled,
Map? realmDefaultExternalAccounts,
+ String? jitsiServerUrl,
int? maxFileUploadSizeMib,
Uri? serverEmojiDataUrl,
String? realmEmptyTopicDisplayName,
@@ -1400,6 +1402,7 @@ InitialSnapshot initialSnapshot({
realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy,
realmWildcardMentionPolicy: realmWildcardMentionPolicy ?? RealmWildcardMentionPolicy.everyone,
realmMandatoryTopics: realmMandatoryTopics ?? true,
+ realmVideoChatProvider: realmVideoChatProvider ?? 1,
realmName: realmName ?? 'Example Zulip organization',
realmWaitingPeriodThreshold: realmWaitingPeriodThreshold ?? 0,
realmMessageContentDeleteLimitSeconds: realmMessageContentDeleteLimitSeconds,
@@ -1409,6 +1412,7 @@ 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'),
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}) {