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}) {