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