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 5afc9544d2..7003f4a12c 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -478,6 +478,10 @@
"@errorCouldNotEditMessageTitle": {
"description": "Error title when an exception prevented us from opening the compose box for editing a message."
},
+ "errorCouldNotAppendCallUrl": "Fail to get call URL",
+ "@errorCouldNotAppendCallUrl": {
+ "description": "Error title when fetching a call URL failed."
+ },
"successLinkCopied": "Link copied",
"@successLinkCopied": {
"description": "Success message after copy link action completed."
@@ -574,6 +578,14 @@
"@composeBoxAttachFromCameraTooltip": {
"description": "Tooltip for compose box icon to attach an image from the camera to the message."
},
+ "composeBoxAddVideoCallTooltip": "Add video call",
+ "@composeBoxAddVideoCallTooltip": {
+ "description": "Tooltip for compose box icon to add a video call url to the message."
+ },
+ "composeBoxAddVoiceCallTooltip": "Add voice call",
+ "@composeBoxAddVoiceCallTooltip": {
+ "description": "Tooltip for compose box icon to add a voice call url to the message."
+ },
"composeBoxGenericContentHint": "Type a message",
"@composeBoxGenericContentHint": {
"description": "Hint text for content input when sending a message."
@@ -654,6 +666,14 @@
"filename": {"type": "String", "example": "file.txt"}
}
},
+ "composeBoxVideoCallLinkText": "Join video call.",
+ "@composeBoxVideoCallLinkText": {
+ "description": "Text for a Markdown link to a video-call URL."
+ },
+ "composeBoxVoiceCallLinkText": "Join voice call.",
+ "@composeBoxVoiceCallLinkText": {
+ "description": "Text for a Markdown link to a voice-call URL."
+ },
"composeBoxLoadingMessage": "(loading message {messageId})",
"@composeBoxLoadingMessage": {
"description": "Placeholder in compose box showing the quoted message is currently loading.",
diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart
index c3a2eb0126..b76ea7d9a1 100644
--- a/lib/api/model/events.dart
+++ b/lib/api/model/events.dart
@@ -29,6 +29,7 @@ sealed class Event {
case 'update': return UserSettingsUpdateEvent.fromJson(json);
default: return UnexpectedEvent.fromJson(json);
}
+ case 'has_zoom_token': return HasZoomTokenEvent.fromJson(json);
case 'custom_profile_fields': return CustomProfileFieldsEvent.fromJson(json);
case 'user_group':
switch (json['op'] as String) {
@@ -1632,6 +1633,27 @@ enum ReactionOp {
remove,
}
+/// A Zulip event of type `hasZoomToken`: https://zulip.com/api/get-events#has_zoom_token
+@JsonSerializable(fieldRename: FieldRename.snake)
+class HasZoomTokenEvent extends Event {
+ @override
+ @JsonKey(includeToJson: true)
+ String get type => 'has_zoom_token';
+
+ final bool value;
+
+ HasZoomTokenEvent({
+ required super.id,
+ required this.value,
+ });
+
+ factory HasZoomTokenEvent.fromJson(Map json) =>
+ _$HasZoomTokenEventFromJson(json);
+
+ @override
+ Map toJson() => _$HasZoomTokenEventToJson(this);
+}
+
/// A Zulip event of type `heartbeat`: https://zulip.com/api/get-events#heartbeat
@JsonSerializable(fieldRename: FieldRename.snake)
class HeartbeatEvent extends Event {
diff --git a/lib/api/model/events.g.dart b/lib/api/model/events.g.dart
index fc0c3a95e2..dc633bfae2 100644
--- a/lib/api/model/events.g.dart
+++ b/lib/api/model/events.g.dart
@@ -1025,6 +1025,19 @@ const _$ReactionTypeEnumMap = {
ReactionType.zulipExtraEmoji: 'zulip_extra_emoji',
};
+HasZoomTokenEvent _$HasZoomTokenEventFromJson(Map json) =>
+ HasZoomTokenEvent(
+ id: (json['id'] as num).toInt(),
+ value: json['value'] as bool,
+ );
+
+Map _$HasZoomTokenEventToJson(HasZoomTokenEvent instance) =>
+ {
+ 'id': instance.id,
+ 'type': instance.type,
+ 'value': instance.value,
+ };
+
HeartbeatEvent _$HeartbeatEventFromJson(Map json) =>
HeartbeatEvent(id: (json['id'] as num).toInt());
diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart
index 2f0257eaac..b4bfd34445 100644
--- a/lib/api/model/initial_snapshot.dart
+++ b/lib/api/model/initial_snapshot.dart
@@ -76,6 +76,8 @@ class InitialSnapshot {
final List userTopics;
+ final bool hasZoomToken;
+
final GroupSettingValue? realmCanDeleteAnyMessageGroup; // TODO(server-10)
final GroupSettingValue? realmCanDeleteOwnMessageGroup; // TODO(server-10)
@@ -96,6 +98,10 @@ class InitialSnapshot {
final String realmName;
+ final RealmVideoChatProvider realmVideoChatProvider;
+
+ final String? realmJitsiServerUrl; // TODO(server-8)
+
/// The number of days until a user's account is treated as a full member.
///
/// Search for "realm_waiting_period_threshold" in https://zulip.com/api/register-queue.
@@ -113,14 +119,20 @@ class InitialSnapshot {
final Uri realmIconUrl;
+ final Map realmAvailableVideoChatProviders;
+
final bool realmPresenceDisabled;
final Map realmDefaultExternalAccounts;
+ final String? jitsiServerUrl; // TODO(server-8): Can ignore this deprecated field
+
final int maxFileUploadSizeMib;
final Uri serverEmojiDataUrl;
+ final String? serverJitsiServerUrl; // TODO(server-8)
+
final String? realmEmptyTopicDisplayName; // TODO(server-10)
@JsonKey(readValue: _readUsersIsActiveFallbackTrue)
@@ -182,22 +194,28 @@ class InitialSnapshot {
required this.userStatuses,
required this.userSettings,
required this.userTopics,
+ required this.hasZoomToken,
required this.realmCanDeleteAnyMessageGroup,
required this.realmCanDeleteOwnMessageGroup,
required this.realmDeleteOwnMessagePolicy,
required this.realmWildcardMentionPolicy,
required this.realmMandatoryTopics,
required this.realmName,
+ required this.realmVideoChatProvider,
+ required this.realmJitsiServerUrl,
required this.realmWaitingPeriodThreshold,
required this.realmMessageContentDeleteLimitSeconds,
required this.realmAllowMessageEditing,
required this.realmMessageContentEditLimitSeconds,
required this.realmEnableReadReceipts,
required this.realmIconUrl,
+ required this.realmAvailableVideoChatProviders,
required this.realmPresenceDisabled,
required this.realmDefaultExternalAccounts,
+ required this.jitsiServerUrl,
required this.maxFileUploadSizeMib,
required this.serverEmojiDataUrl,
+ required this.serverJitsiServerUrl,
required this.realmEmptyTopicDisplayName,
required this.realmUsers,
required this.realmNonActiveUsers,
diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart
index dac7c141f7..9288769af8 100644
--- a/lib/api/model/initial_snapshot.g.dart
+++ b/lib/api/model/initial_snapshot.g.dart
@@ -86,6 +86,7 @@ InitialSnapshot _$InitialSnapshotFromJson(
userTopics: (json['user_topics'] as List)
.map((e) => UserTopicItem.fromJson(e as Map))
.toList(),
+ hasZoomToken: json['has_zoom_token'] as bool,
realmCanDeleteAnyMessageGroup:
json['realm_can_delete_any_message_group'] == null
? null
@@ -104,6 +105,11 @@ InitialSnapshot _$InitialSnapshotFromJson(
),
realmMandatoryTopics: json['realm_mandatory_topics'] as bool,
realmName: json['realm_name'] as String,
+ realmVideoChatProvider: $enumDecode(
+ _$RealmVideoChatProviderEnumMap,
+ json['realm_video_chat_provider'],
+ ),
+ realmJitsiServerUrl: json['realm_jitsi_server_url'] as String?,
realmWaitingPeriodThreshold: (json['realm_waiting_period_threshold'] as num)
.toInt(),
realmMessageContentDeleteLimitSeconds:
@@ -113,6 +119,16 @@ InitialSnapshot _$InitialSnapshotFromJson(
(json['realm_message_content_edit_limit_seconds'] as num?)?.toInt(),
realmEnableReadReceipts: json['realm_enable_read_receipts'] as bool,
realmIconUrl: Uri.parse(json['realm_icon_url'] as String),
+ realmAvailableVideoChatProviders:
+ (json['realm_available_video_chat_providers'] as Map)
+ .map(
+ (k, e) => MapEntry(
+ k,
+ RealmAvailableVideoChatProviders.fromJson(
+ e as Map,
+ ),
+ ),
+ ),
realmPresenceDisabled: json['realm_presence_disabled'] as bool,
realmDefaultExternalAccounts:
(json['realm_default_external_accounts'] as Map).map(
@@ -121,8 +137,10 @@ InitialSnapshot _$InitialSnapshotFromJson(
RealmDefaultExternalAccount.fromJson(e as Map),
),
),
+ jitsiServerUrl: json['jitsi_server_url'] as String?,
maxFileUploadSizeMib: (json['max_file_upload_size_mib'] as num).toInt(),
serverEmojiDataUrl: Uri.parse(json['server_emoji_data_url'] as String),
+ serverJitsiServerUrl: json['server_jitsi_server_url'] as String?,
realmEmptyTopicDisplayName: json['realm_empty_topic_display_name'] as String?,
realmUsers:
(InitialSnapshot._readUsersIsActiveFallbackTrue(json, 'realm_users')
@@ -179,12 +197,15 @@ Map _$InitialSnapshotToJson(
'user_status': instance.userStatuses.map((k, e) => MapEntry(k.toString(), e)),
'user_settings': instance.userSettings,
'user_topics': instance.userTopics,
+ 'has_zoom_token': instance.hasZoomToken,
'realm_can_delete_any_message_group': instance.realmCanDeleteAnyMessageGroup,
'realm_can_delete_own_message_group': instance.realmCanDeleteOwnMessageGroup,
'realm_delete_own_message_policy': instance.realmDeleteOwnMessagePolicy,
'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,10 +214,14 @@ Map _$InitialSnapshotToJson(
instance.realmMessageContentEditLimitSeconds,
'realm_enable_read_receipts': instance.realmEnableReadReceipts,
'realm_icon_url': instance.realmIconUrl.toString(),
+ 'realm_available_video_chat_providers':
+ instance.realmAvailableVideoChatProviders,
'realm_presence_disabled': instance.realmPresenceDisabled,
'realm_default_external_accounts': instance.realmDefaultExternalAccounts,
+ '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,
@@ -220,6 +245,15 @@ const _$RealmWildcardMentionPolicyEnumMap = {
RealmWildcardMentionPolicy.moderators: 7,
};
+const _$RealmVideoChatProviderEnumMap = {
+ RealmVideoChatProvider.none: 0,
+ RealmVideoChatProvider.jitsiMeet: 1,
+ RealmVideoChatProvider.zoomUserOAuth: 3,
+ RealmVideoChatProvider.bigBlueButton: 4,
+ RealmVideoChatProvider.zoomServerToServerOAuth: 5,
+ RealmVideoChatProvider.unknown: null,
+};
+
RealmDefaultExternalAccount _$RealmDefaultExternalAccountFromJson(
Map json,
) => RealmDefaultExternalAccount(
diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart
index c7ae2fb6ac..69bd493271 100644
--- a/lib/api/model/model.dart
+++ b/lib/api/model/model.dart
@@ -56,6 +56,44 @@ class GroupSettingValueNameless extends GroupSettingValue {
Map toJson() => _$GroupSettingValueNamelessToJson(this);
}
+/// As in [InitialSnapshot.realmAvailableVideoChatProviders].
+///
+/// For docs, search for "realm_available_video_chat_providers:"
+/// in .
+@JsonSerializable(fieldRename: FieldRename.snake)
+class RealmAvailableVideoChatProviders {
+ final String name;
+ final int id;
+
+ RealmAvailableVideoChatProviders({
+ required this.name,
+ required this.id,
+ });
+
+ factory RealmAvailableVideoChatProviders.fromJson(Map json) =>
+ _$RealmAvailableVideoChatProvidersFromJson(json);
+
+ Map toJson() => _$RealmAvailableVideoChatProvidersToJson(this);
+}
+
+@JsonSerializable(fieldRename: FieldRename.snake)
+class VideoCallResponse {
+ final String msg;
+ final String result;
+ final String url;
+
+ VideoCallResponse({
+ required this.msg,
+ required this.result,
+ required this.url,
+ });
+
+ factory VideoCallResponse.fromJson(Map json) =>
+ _$VideoCallResponseFromJson(json);
+
+ Map toJson() => _$VideoCallResponseToJson(this);
+}
+
/// As in [InitialSnapshot.customProfileFields].
///
/// For docs, search for "custom_profile_fields:"
@@ -406,6 +444,28 @@ enum Emojiset {
String toJson() => _$EmojisetEnumMap[this]!;
}
+/// As in [InitialSnapshot.realmVideoChatProvider].
+///
+/// For docs, search for "realm_video_chat_provider:"
+/// in .
+@JsonEnum(fieldRename: FieldRename.snake, valueField: "apiValue")
+enum RealmVideoChatProvider {
+ none(apiValue: 0),
+ jitsiMeet(apiValue: 1),
+ zoomUserOAuth(apiValue: 3),
+ bigBlueButton(apiValue: 4),
+ zoomServerToServerOAuth(apiValue: 5),
+ unknown(apiValue: null);
+
+ const RealmVideoChatProvider({
+ required this.apiValue,
+ });
+
+ final int? apiValue;
+
+ int? toJson() => apiValue;
+}
+
/// As in [InitialSnapshot.realmUserGroups] or [UserGroupAddEvent].
@JsonSerializable(fieldRename: FieldRename.snake)
class UserGroup {
diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart
index 95c5588392..57708c00ba 100644
--- a/lib/api/model/model.g.dart
+++ b/lib/api/model/model.g.dart
@@ -26,6 +26,31 @@ Map _$GroupSettingValueNamelessToJson(
'direct_subgroups': instance.directSubgroups,
};
+RealmAvailableVideoChatProviders _$RealmAvailableVideoChatProvidersFromJson(
+ Map json,
+) => RealmAvailableVideoChatProviders(
+ name: json['name'] as String,
+ id: (json['id'] as num).toInt(),
+);
+
+Map _$RealmAvailableVideoChatProvidersToJson(
+ RealmAvailableVideoChatProviders instance,
+) => {'name': instance.name, 'id': instance.id};
+
+VideoCallResponse _$VideoCallResponseFromJson(Map json) =>
+ VideoCallResponse(
+ msg: json['msg'] as String,
+ result: json['result'] as String,
+ url: json['url'] as String,
+ );
+
+Map _$VideoCallResponseToJson(VideoCallResponse instance) =>
+ {
+ 'msg': instance.msg,
+ 'result': instance.result,
+ 'url': instance.url,
+ };
+
CustomProfileField _$CustomProfileFieldFromJson(Map json) =>
CustomProfileField(
id: (json['id'] as num).toInt(),
diff --git a/lib/api/route/video_call.dart b/lib/api/route/video_call.dart
new file mode 100644
index 0000000000..18bbdc3b7e
--- /dev/null
+++ b/lib/api/route/video_call.dart
@@ -0,0 +1,23 @@
+import '../core.dart';
+import '../model/model.dart';
+
+/// Creates a Zoom video/audio call
+///
+/// POST /api/v1/calls/zoom/create
+Future createZoomCall(ApiConnection connection, {
+ required bool isVideoCall,
+}) async {
+ return connection.post('createZoomCall', VideoCallResponse.fromJson,
+ '/calls/zoom/create', {'is_video_call': isVideoCall});
+}
+
+/// Creates a BigBlueButton meeting
+///
+/// GET /api/v1/calls/bigbluebutton/create
+Future createBigBlueButtonCall(ApiConnection connection, {
+ required String meetingName,
+ required bool voiceOnly,
+}) async {
+ return connection.get('createBigBlueButtonCall', VideoCallResponse.fromJson,
+ '/calls/bigbluebutton/create', {'meeting_name': meetingName, 'voice_only': voiceOnly});
+}
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index 7e0b9096a6..3d066095d6 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -783,6 +783,12 @@ abstract class ZulipLocalizations {
/// **'Could not edit message'**
String get errorCouldNotEditMessageTitle;
+ /// Error title when fetching a call URL failed.
+ ///
+ /// In en, this message translates to:
+ /// **'Fail to get call URL'**
+ String get errorCouldNotAppendCallUrl;
+
/// Success message after copy link action completed.
///
/// In en, this message translates to:
@@ -927,6 +933,18 @@ abstract class ZulipLocalizations {
/// **'Take a photo'**
String get composeBoxAttachFromCameraTooltip;
+ /// Tooltip for compose box icon to add a video call url to the message.
+ ///
+ /// In en, this message translates to:
+ /// **'Add video call'**
+ String get composeBoxAddVideoCallTooltip;
+
+ /// Tooltip for compose box icon to add a voice call url to the message.
+ ///
+ /// In en, this message translates to:
+ /// **'Add voice call'**
+ String get composeBoxAddVoiceCallTooltip;
+
/// Hint text for content input when sending a message.
///
/// In en, this message translates to:
@@ -1029,6 +1047,18 @@ abstract class ZulipLocalizations {
/// **'Uploading {filename}…'**
String composeBoxUploadingFilename(String filename);
+ /// Text for a Markdown link to a video-call URL.
+ ///
+ /// In en, this message translates to:
+ /// **'Join video call.'**
+ String get composeBoxVideoCallLinkText;
+
+ /// Text for a Markdown link to a voice-call URL.
+ ///
+ /// In en, this message translates to:
+ /// **'Join voice call.'**
+ String get composeBoxVoiceCallLinkText;
+
/// Placeholder in compose box showing the quoted message is currently loading.
///
/// In en, this message translates to:
diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart
index 0de3988707..9099abc085 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -406,6 +406,9 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Could not edit message';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link copied';
@@ -485,6 +488,12 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +553,12 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart
index e1f57cd9ae..3ad409d7a6 100644
--- a/lib/generated/l10n/zulip_localizations_de.dart
+++ b/lib/generated/l10n/zulip_localizations_de.dart
@@ -423,6 +423,9 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
String get errorCouldNotEditMessageTitle =>
'Konnte Nachricht nicht bearbeiten';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link kopiert';
@@ -502,6 +505,12 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Ein Foto aufnehmen';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Eine Nachricht eingeben';
@@ -563,6 +572,12 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
return 'Lade $filename hoch…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(lade Nachricht $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_el.dart b/lib/generated/l10n/zulip_localizations_el.dart
index 37510410f9..8cf0c276c7 100644
--- a/lib/generated/l10n/zulip_localizations_el.dart
+++ b/lib/generated/l10n/zulip_localizations_el.dart
@@ -406,6 +406,9 @@ class ZulipLocalizationsEl extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Could not edit message';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link copied';
@@ -485,6 +488,12 @@ class ZulipLocalizationsEl extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +553,12 @@ class ZulipLocalizationsEl extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart
index feb32d69e5..c4f5405f13 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -406,6 +406,9 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Could not edit message';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link copied';
@@ -485,6 +488,12 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +553,12 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_es.dart b/lib/generated/l10n/zulip_localizations_es.dart
index 163fb4e6a4..d6553cf0c2 100644
--- a/lib/generated/l10n/zulip_localizations_es.dart
+++ b/lib/generated/l10n/zulip_localizations_es.dart
@@ -406,6 +406,9 @@ class ZulipLocalizationsEs extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Could not edit message';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link copied';
@@ -485,6 +488,12 @@ class ZulipLocalizationsEs extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +553,12 @@ class ZulipLocalizationsEs extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart
index 979a819efc..fd82fdc848 100644
--- a/lib/generated/l10n/zulip_localizations_fr.dart
+++ b/lib/generated/l10n/zulip_localizations_fr.dart
@@ -422,6 +422,9 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
String get errorCouldNotEditMessageTitle =>
'Le message n\'a pas pu être modifié';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Lien copié';
@@ -501,6 +504,12 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -560,6 +569,12 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_he.dart b/lib/generated/l10n/zulip_localizations_he.dart
index 4a813be1b0..f51998cdda 100644
--- a/lib/generated/l10n/zulip_localizations_he.dart
+++ b/lib/generated/l10n/zulip_localizations_he.dart
@@ -406,6 +406,9 @@ class ZulipLocalizationsHe extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Could not edit message';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link copied';
@@ -485,6 +488,12 @@ class ZulipLocalizationsHe extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +553,12 @@ class ZulipLocalizationsHe extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_hu.dart b/lib/generated/l10n/zulip_localizations_hu.dart
index 6b3805bf99..942706dc28 100644
--- a/lib/generated/l10n/zulip_localizations_hu.dart
+++ b/lib/generated/l10n/zulip_localizations_hu.dart
@@ -406,6 +406,9 @@ class ZulipLocalizationsHu extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Could not edit message';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link copied';
@@ -485,6 +488,12 @@ class ZulipLocalizationsHu extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +553,12 @@ class ZulipLocalizationsHu extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart
index 0c06cd4b38..270ddee6f9 100644
--- a/lib/generated/l10n/zulip_localizations_it.dart
+++ b/lib/generated/l10n/zulip_localizations_it.dart
@@ -417,6 +417,9 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
String get errorCouldNotEditMessageTitle =>
'Impossibile modificare il messaggio';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Collegamento copiato';
@@ -498,6 +501,12 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Fai una foto';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Batti un messaggio';
@@ -557,6 +566,12 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
return 'Caricamento $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(caricamento messaggio $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart
index d59e294c8a..269bb7c833 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -398,6 +398,9 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'メッセージを編集できませんでした';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'リンクをコピーしました';
@@ -473,6 +476,12 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => '写真を撮る';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'メッセージを入力';
@@ -532,6 +541,12 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
return '$filename をアップロード中…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(メッセージ $messageId を読み込み中)';
diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart
index d828f44d95..17b7dc6f2e 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -406,6 +406,9 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Could not edit message';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link copied';
@@ -485,6 +488,12 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +553,12 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart
index 88fbc24a57..f25939f7e5 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -418,6 +418,9 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Nie można zmienić wiadomości';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Skopiowano odnośnik';
@@ -497,6 +500,12 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Zrób zdjęcie';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Wpisz wiadomość';
@@ -557,6 +566,12 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
return 'Przekazywanie $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(ładowanie wiadomości $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart
index 06cfea774d..e6db692880 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -420,6 +420,9 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Сбой редактирования';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Ссылка скопирована';
@@ -499,6 +502,12 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Сделать снимок';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Ввести сообщение';
@@ -558,6 +567,12 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
return 'Загрузка $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(загрузка сообщения $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart
index 4841efd265..e7b09e1e56 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -406,6 +406,9 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Could not edit message';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link copied';
@@ -485,6 +488,12 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +553,12 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart
index c4c6d91ed1..a797eb9b92 100644
--- a/lib/generated/l10n/zulip_localizations_sl.dart
+++ b/lib/generated/l10n/zulip_localizations_sl.dart
@@ -429,6 +429,9 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Sporočila ni mogoče urediti';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Povezava je bila kopirana';
@@ -510,6 +513,12 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Fotografiraj';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Vnesite sporočilo';
@@ -569,6 +578,12 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
return 'Nalaganje $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(nalaganje sporočila $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart
index e202787969..e382834e7a 100644
--- a/lib/generated/l10n/zulip_localizations_uk.dart
+++ b/lib/generated/l10n/zulip_localizations_uk.dart
@@ -419,6 +419,9 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
String get errorCouldNotEditMessageTitle =>
'Не вдалося редагувати повідомлення';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Посилання скопійовано';
@@ -499,6 +502,12 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Зробити фото';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Ввести повідомлення';
@@ -558,6 +567,12 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
return 'Завантаження $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(завантаження повідомлення $messageId)';
diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart
index 292a511d57..de1f2e75ea 100644
--- a/lib/generated/l10n/zulip_localizations_zh.dart
+++ b/lib/generated/l10n/zulip_localizations_zh.dart
@@ -406,6 +406,9 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
@override
String get errorCouldNotEditMessageTitle => 'Could not edit message';
+ @override
+ String get errorCouldNotAppendCallUrl => 'Fail to get call URL';
+
@override
String get successLinkCopied => 'Link copied';
@@ -485,6 +488,12 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
@override
String get composeBoxAttachFromCameraTooltip => 'Take a photo';
+ @override
+ String get composeBoxAddVideoCallTooltip => 'Add video call';
+
+ @override
+ String get composeBoxAddVoiceCallTooltip => 'Add voice call';
+
@override
String get composeBoxGenericContentHint => 'Type a message';
@@ -544,6 +553,12 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
return 'Uploading $filename…';
}
+ @override
+ String get composeBoxVideoCallLinkText => 'Join video call.';
+
+ @override
+ String get composeBoxVoiceCallLinkText => 'Join voice call.';
+
@override
String composeBoxLoadingMessage(int messageId) {
return '(loading message $messageId)';
diff --git a/lib/model/realm.dart b/lib/model/realm.dart
index 9acfbda757..b82f0f0139 100644
--- a/lib/model/realm.dart
+++ b/lib/model/realm.dart
@@ -32,6 +32,8 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore {
Duration get serverTypingStartedWaitPeriod => Duration(milliseconds: serverTypingStartedWaitPeriodMilliseconds);
int get serverTypingStartedWaitPeriodMilliseconds;
+ String? get serverJitsiServerUrl;
+
//|//////////////////////////////////////////////////////////////
// Realm settings.
@@ -46,6 +48,7 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore {
GroupSettingValue? get realmCanDeleteAnyMessageGroup; // TODO(server-10)
GroupSettingValue? get realmCanDeleteOwnMessageGroup; // TODO(server-10)
bool get realmEnableReadReceipts;
+ String? get realmJitsiServerUrl;
bool get realmMandatoryTopics;
int get maxFileUploadSizeMib;
int? get realmMessageContentDeleteLimitSeconds;
@@ -54,6 +57,7 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore {
: Duration(seconds: realmMessageContentEditLimitSeconds!);
int? get realmMessageContentEditLimitSeconds;
bool get realmPresenceDisabled;
+ RealmVideoChatProvider get realmVideoChatProvider;
int get realmWaitingPeriodThreshold;
//|//////////////////////////////
@@ -74,11 +78,15 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore {
// TODO(server-10) simplify this
String get realmEmptyTopicDisplayName;
+ Map get realmAvailableVideoChatProviders;
+
Map get realmDefaultExternalAccounts;
int get maxChannelNameLength;
int get maxTopicLength;
+ String? get jitsiServerUrl;
+
//|//////////////////////////////
// Realm settings with their own events.
@@ -167,6 +175,8 @@ mixin ProxyRealmStore on RealmStore {
@override
int get serverTypingStartedWaitPeriodMilliseconds => realmStore.serverTypingStartedWaitPeriodMilliseconds;
@override
+ String? get serverJitsiServerUrl => realmStore.serverJitsiServerUrl;
+ @override
bool get realmAllowMessageEditing => realmStore.realmAllowMessageEditing;
@override
GroupSettingValue? get realmCanDeleteAnyMessageGroup => realmStore.realmCanDeleteAnyMessageGroup;
@@ -175,6 +185,8 @@ mixin ProxyRealmStore on RealmStore {
@override
bool get realmEnableReadReceipts => realmStore.realmEnableReadReceipts;
@override
+ String? get realmJitsiServerUrl => realmStore.realmJitsiServerUrl;
+ @override
bool get realmMandatoryTopics => realmStore.realmMandatoryTopics;
@override
int get maxFileUploadSizeMib => realmStore.maxFileUploadSizeMib;
@@ -185,6 +197,8 @@ mixin ProxyRealmStore on RealmStore {
@override
bool get realmPresenceDisabled => realmStore.realmPresenceDisabled;
@override
+ RealmVideoChatProvider get realmVideoChatProvider => realmStore.realmVideoChatProvider;
+ @override
int get realmWaitingPeriodThreshold => realmStore.realmWaitingPeriodThreshold;
@override
RealmWildcardMentionPolicy get realmWildcardMentionPolicy => realmStore.realmWildcardMentionPolicy;
@@ -193,12 +207,16 @@ mixin ProxyRealmStore on RealmStore {
@override
String get realmEmptyTopicDisplayName => realmStore.realmEmptyTopicDisplayName;
@override
+ Map get realmAvailableVideoChatProviders => realmStore.realmAvailableVideoChatProviders;
+ @override
Map get realmDefaultExternalAccounts => realmStore.realmDefaultExternalAccounts;
@override
int get maxChannelNameLength => realmStore.maxChannelNameLength;
@override
int get maxTopicLength => realmStore.maxTopicLength;
@override
+ String? get jitsiServerUrl => realmStore.jitsiServerUrl;
+ @override
List get customProfileFields => realmStore.customProfileFields;
@override
bool selfHasPassedWaitingPeriod({required DateTime byDate}) =>
@@ -233,22 +251,27 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
serverTypingStartedExpiryPeriodMilliseconds = initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds,
serverTypingStoppedWaitPeriodMilliseconds = initialSnapshot.serverTypingStoppedWaitPeriodMilliseconds,
serverTypingStartedWaitPeriodMilliseconds = initialSnapshot.serverTypingStartedWaitPeriodMilliseconds,
+ serverJitsiServerUrl = initialSnapshot.serverJitsiServerUrl,
realmAllowMessageEditing = initialSnapshot.realmAllowMessageEditing,
realmCanDeleteAnyMessageGroup = initialSnapshot.realmCanDeleteAnyMessageGroup,
realmCanDeleteOwnMessageGroup = initialSnapshot.realmCanDeleteOwnMessageGroup,
+ realmEnableReadReceipts = initialSnapshot.realmEnableReadReceipts,
+ realmJitsiServerUrl = initialSnapshot.realmJitsiServerUrl,
realmMandatoryTopics = initialSnapshot.realmMandatoryTopics,
maxFileUploadSizeMib = initialSnapshot.maxFileUploadSizeMib,
realmMessageContentDeleteLimitSeconds = initialSnapshot.realmMessageContentDeleteLimitSeconds,
realmMessageContentEditLimitSeconds = initialSnapshot.realmMessageContentEditLimitSeconds,
- realmEnableReadReceipts = initialSnapshot.realmEnableReadReceipts,
realmPresenceDisabled = initialSnapshot.realmPresenceDisabled,
+ realmVideoChatProvider = initialSnapshot.realmVideoChatProvider,
realmWaitingPeriodThreshold = initialSnapshot.realmWaitingPeriodThreshold,
realmWildcardMentionPolicy = initialSnapshot.realmWildcardMentionPolicy,
realmDeleteOwnMessagePolicy = initialSnapshot.realmDeleteOwnMessagePolicy,
_realmEmptyTopicDisplayName = initialSnapshot.realmEmptyTopicDisplayName,
+ realmAvailableVideoChatProviders = initialSnapshot.realmAvailableVideoChatProviders,
realmDefaultExternalAccounts = initialSnapshot.realmDefaultExternalAccounts,
maxChannelNameLength = initialSnapshot.maxChannelNameLength,
maxTopicLength = initialSnapshot.maxTopicLength,
+ jitsiServerUrl = initialSnapshot.jitsiServerUrl,
customProfileFields = _sortCustomProfileFields(initialSnapshot.customProfileFields);
@override
@@ -377,6 +400,8 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
final int serverTypingStoppedWaitPeriodMilliseconds;
@override
final int serverTypingStartedWaitPeriodMilliseconds;
+ @override
+ final String? serverJitsiServerUrl;
@override
final bool realmAllowMessageEditing;
@@ -387,6 +412,8 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
@override
final bool realmEnableReadReceipts;
@override
+ final String? realmJitsiServerUrl;
+ @override
final bool realmMandatoryTopics;
@override
final int maxFileUploadSizeMib;
@@ -397,6 +424,8 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
@override
final bool realmPresenceDisabled;
@override
+ final RealmVideoChatProvider realmVideoChatProvider;
+ @override
final int realmWaitingPeriodThreshold;
@override
@@ -412,6 +441,9 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
}
final String? _realmEmptyTopicDisplayName;
+ @override
+ final Map realmAvailableVideoChatProviders;
+
@override
final Map realmDefaultExternalAccounts;
@@ -420,6 +452,9 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
@override
final int maxTopicLength;
+ @override
+ final String? jitsiServerUrl;
+
@override
List customProfileFields;
diff --git a/lib/model/store.dart b/lib/model/store.dart
index b051e75b72..1de0798d4a 100644
--- a/lib/model/store.dart
+++ b/lib/model/store.dart
@@ -17,6 +17,7 @@ import '../api/route/events.dart';
import '../api/backoff.dart';
import '../api/route/realm.dart';
import '../log.dart';
+import '../widgets/compose_box.dart';
import 'actions.dart';
import 'autocomplete.dart';
import 'database.dart';
@@ -562,6 +563,7 @@ class PerAccountStore extends PerAccountStoreBase with
emoji: EmojiStoreImpl(core: core,
allRealmEmoji: initialSnapshot.realmEmoji),
userSettings: initialSnapshot.userSettings,
+ hasZoomToken: initialSnapshot.hasZoomToken,
pushDevices: PushDeviceManager(core: core),
savedSnippets: SavedSnippetStoreImpl(core: core,
savedSnippets: initialSnapshot.savedSnippets ?? []),
@@ -586,6 +588,7 @@ class PerAccountStore extends PerAccountStoreBase with
required RealmStoreImpl realm,
required EmojiStoreImpl emoji,
required this.userSettings,
+ required this.hasZoomToken,
required this.pushDevices,
required SavedSnippetStoreImpl savedSnippets,
required this.typingNotifier,
@@ -658,6 +661,8 @@ class PerAccountStore extends PerAccountStoreBase with
final UserSettings userSettings;
+ bool hasZoomToken;
+
final PushDeviceManager pushDevices;
@override
@@ -792,6 +797,15 @@ class PerAccountStore extends PerAccountStoreBase with
}
notifyListeners();
+ case HasZoomTokenEvent():
+ assert(debugLog("server event: has_zoom_token"));
+ final hasZoomTokenUpdated = event.value;
+ if (hasZoomToken != hasZoomTokenUpdated) {
+ hasZoomToken = hasZoomTokenUpdated;
+ if(hasZoomToken) await ComposeCall.handleHasZoomTokenEvent();
+ notifyListeners();
+ }
+
case CustomProfileFieldsEvent():
assert(debugLog("server event: custom_profile_fields"));
_realm.handleCustomProfileFieldsEvent(event);
diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart
index 52faeaf151..a341e9aad6 100644
--- a/lib/widgets/compose_box.dart
+++ b/lib/widgets/compose_box.dart
@@ -7,10 +7,12 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart' as path;
+import 'package:url_launcher/url_launcher.dart';
import '../api/exception.dart';
import '../api/model/model.dart';
import '../api/route/messages.dart';
+import '../api/route/video_call.dart';
import '../generated/l10n/zulip_localizations.dart';
import '../model/binding.dart';
import '../model/compose.dart';
@@ -1034,6 +1036,220 @@ Future _uploadFiles({
}
}
+class ComposeCall {
+ static final Map Function()> _zoomTokenCallbacks = {};
+ static void clearZoomCallbacks(int? editMessageId) {
+ final key = editMessageId?.toString() ?? '';
+ _zoomTokenCallbacks.remove(key);
+ }
+
+ static Future handleHasZoomTokenEvent() async {
+ final callbacks = Map Function()>.from(_zoomTokenCallbacks);
+ _zoomTokenCallbacks.clear();
+ for (final callback in callbacks.values) {
+ await callback();
+ }
+ }
+
+ static int generateRandomId(int min, int max) {
+ return min + (DateTime.now().microsecondsSinceEpoch % (max - min));
+ }
+}
+
+class _AddComposeCallUrlButton extends StatefulWidget {
+ const _AddComposeCallUrlButton({
+ required this.controller,
+ required this.enabled,
+ required this.isVideoCall,
+ });
+ final ComposeBoxController controller;
+ final bool enabled;
+ final bool isVideoCall;
+
+ @override
+ State<_AddComposeCallUrlButton> createState() => _AddComposeCallUrlButtonState();
+}
+
+class _AddComposeCallUrlButtonState extends State<_AddComposeCallUrlButton> {
+
+ static String getJitsiServerUrl(PerAccountStore store) {
+ return store.realmJitsiServerUrl ?? store.serverJitsiServerUrl ??
+ store.jitsiServerUrl ?? 'https://meet.jit.si';
+ }
+
+ Future _openZoomOAuthWindow() async {
+ final store = PerAccountStoreWidget.of(context);
+ final url = store.realmUrl.resolve('/calls/zoom/register');
+
+ await launchUrl(url, mode: LaunchMode.platformDefault);
+ }
+
+ void _insertCallUrl(String url, String visibleText) {
+ final placeholder = inlineLink(visibleText, url);
+ final contentController = widget.controller.content;
+ final insertionRange = contentController.insertionIndex();
+ contentController.value = contentController.value.replaced(insertionRange, '$placeholder\n\n');
+ widget.controller.contentFocusNode.requestFocus();
+ }
+
+ void _handleJitsiCall({
+ required ComposeContentController contentController,
+ required bool isAudioCall,
+ }) {
+ final store = PerAccountStoreWidget.of(context);
+ final zulipLocalization = ZulipLocalizations.of(context);
+ final videoCallId = ComposeCall.generateRandomId(100000000000000, 999999999999999);
+ final jitsiServerUrl = getJitsiServerUrl(store);
+ final videoCallLink = '$jitsiServerUrl/$videoCallId';
+
+ if (!isAudioCall) {
+ _insertCallUrl('$videoCallLink#config.startWithVideoMuted=false',
+ zulipLocalization.composeBoxVideoCallLinkText);
+ } else {
+ _insertCallUrl('$videoCallLink#config.startWithVideoMuted=true',
+ zulipLocalization.composeBoxVideoCallLinkText);
+ }
+
+ }
+
+ Future _createBigBlueButtonCall ({
+ required ComposeContentController contentController,
+ required bool voiceOnly,
+ }) async {
+ final zulipLocalization = ZulipLocalizations.of(context);
+ final store = PerAccountStoreWidget.of(context);
+ try {
+ final connection = store.connection;
+ final result = await createBigBlueButtonCall(
+ connection, meetingName: "Null", //TODO: Fetch message stream title
+ voiceOnly: voiceOnly);
+
+ if (!voiceOnly) {
+ _insertCallUrl(result.url, zulipLocalization.composeBoxVideoCallLinkText);
+ } else {
+ _insertCallUrl(result.url, zulipLocalization.composeBoxAddVoiceCallTooltip);
+ }
+ } on ApiRequestException catch (e) {
+ if (!mounted) return;
+ final zulipLocalizations = ZulipLocalizations.of(context);
+ final message = switch (e) {
+ ZulipApiException() => zulipLocalizations.errorServerMessage(e.message),
+ _ => e.message,
+ };
+ showErrorDialog(context: context,
+ title: zulipLocalizations.errorCouldNotAppendCallUrl,
+ message: message);
+ return;
+ }
+ }
+
+
+
+ Future handleZoomCall({
+ required ComposeContentController contentController,
+ required bool isVideoCall,
+ required bool isServerToServer,
+ int? editMessageId,
+ }) async {
+ final store = PerAccountStoreWidget.of(context);
+ final key = editMessageId?.toString() ?? '';
+ ComposeCall.clearZoomCallbacks(editMessageId);
+ Future prepareZoomCall() async {
+ final zulipLocalization = ZulipLocalizations.of(context);
+ final store = PerAccountStoreWidget.of(context);
+ try {
+ final connection = store.connection;
+ final result = await createZoomCall(connection, isVideoCall: isVideoCall);
+
+ if (isVideoCall) {
+ _insertCallUrl(result.url, zulipLocalization.composeBoxVideoCallLinkText);
+ } else {
+ _insertCallUrl(result.url, zulipLocalization.composeBoxAddVoiceCallTooltip);
+ }
+
+ } on ApiRequestException catch (e) {
+ if (!mounted) return;
+ store.hasZoomToken = false;
+ ComposeCall.clearZoomCallbacks(editMessageId);
+ final zulipLocalizations = ZulipLocalizations.of(context);
+ final message = switch (e) {
+ ZulipApiException() => zulipLocalizations.errorServerMessage(e.message),
+ _ => e.message,
+ };
+ showErrorDialog(context: context,
+ title: zulipLocalizations.errorCouldNotAppendCallUrl,
+ message: message);
+ return;
+ }
+
+ }
+
+ if (store.hasZoomToken || isServerToServer) {
+ await prepareZoomCall();
+ } else {
+ ComposeCall._zoomTokenCallbacks[key] = prepareZoomCall;
+ await _openZoomOAuthWindow();
+ }
+
+ }
+
+
+ Future generateComposeCallUrl({
+ required ComposeContentController contentController,
+ required bool isAudioCall,
+ int? editMessageId,
+ }) async {
+ final store = PerAccountStoreWidget.of(context);
+ final realmAvailableVideoChatProviders = store.realmAvailableVideoChatProviders;
+ final realmVideoChatProvider = store.realmVideoChatProvider;
+
+ final providerIsZoom = realmAvailableVideoChatProviders['zoom'] != null &&
+ realmVideoChatProvider.apiValue == realmAvailableVideoChatProviders['zoom']!.id;
+ final providerIsZoomServerToServer =
+ realmAvailableVideoChatProviders['zoom_server_to_server'] != null &&
+ realmVideoChatProvider.apiValue == realmAvailableVideoChatProviders['zoom_server_to_server']!.id;
+
+ if (providerIsZoom || providerIsZoomServerToServer) {
+ await handleZoomCall(
+ contentController: contentController,
+ isVideoCall: !isAudioCall,
+ isServerToServer: providerIsZoomServerToServer,
+ );
+ } else if (realmAvailableVideoChatProviders['big_blue_button'] != null &&
+ realmVideoChatProvider.apiValue == realmAvailableVideoChatProviders['big_blue_button']!.id) {
+ await _createBigBlueButtonCall(
+ contentController: contentController,
+ voiceOnly: isAudioCall,
+ );
+ } else {
+ _handleJitsiCall(
+ contentController: contentController,
+ isAudioCall: isAudioCall,
+ );
+ }
+ }
+
+
+ Future _handlePress(BuildContext context) async {
+ final contentController = widget.controller.content;
+ await generateComposeCallUrl(contentController: contentController,
+ isAudioCall: !widget.isVideoCall, editMessageId: null);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final designVariables = DesignVariables.of(context);
+ final zulipLocalizations = ZulipLocalizations.of(context);
+
+ return SizedBox(
+ width: _composeButtonSize,
+ child: IconButton(
+ icon: Icon(ZulipIcons.video, color: designVariables.foreground.withFadedAlpha(0.5)),
+ tooltip: zulipLocalizations.composeBoxAddVideoCallTooltip,
+ onPressed: widget.enabled ? () => _handlePress(context) : null));
+ }
+}
+
abstract class _AttachUploadsButton extends StatelessWidget {
const _AttachUploadsButton({required this.controller, required this.enabled});
@@ -1473,6 +1689,7 @@ abstract class _ComposeBoxBody extends StatelessWidget {
_AttachFileButton(controller: controller, enabled: composeButtonsEnabled),
_AttachMediaButton(controller: controller, enabled: composeButtonsEnabled),
_AttachFromCameraButton(controller: controller, enabled: composeButtonsEnabled),
+ _AddComposeCallUrlButton(controller: controller, enabled: composeButtonsEnabled, isVideoCall: true),
];
final topicInput = buildTopicInput();
diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart
index 0e17f584ce..d16eb967b3 100644
--- a/lib/widgets/icons.dart
+++ b/lib/widgets/icons.dart
@@ -202,6 +202,9 @@ abstract final class ZulipIcons {
/// The Zulip custom icon "unmute".
static const IconData unmute = IconData(0xf13a, fontFamily: "Zulip Icons");
+ /// The Zulip custom icon "video".
+ static const IconData video = IconData(0xf13b, fontFamily: "Zulip Icons");
+
// END GENERATED ICON DATA
}
diff --git a/test/example_data.dart b/test/example_data.dart
index c9beaef72f..6ea5b55b8f 100644
--- a/test/example_data.dart
+++ b/test/example_data.dart
@@ -1352,22 +1352,28 @@ InitialSnapshot initialSnapshot({
Map? userStatuses,
UserSettings? userSettings,
List? userTopics,
+ bool? hasZoomToken,
GroupSettingValue? realmCanDeleteAnyMessageGroup,
GroupSettingValue? realmCanDeleteOwnMessageGroup,
RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy,
RealmWildcardMentionPolicy? realmWildcardMentionPolicy,
bool? realmMandatoryTopics,
String? realmName,
+ RealmVideoChatProvider? realmVideoChatProvider,
+ String? realmJitsiServerUrl,
int? realmWaitingPeriodThreshold,
int? realmMessageContentDeleteLimitSeconds,
bool? realmAllowMessageEditing,
int? realmMessageContentEditLimitSeconds,
bool? realmEnableReadReceipts,
Uri? realmIconUrl,
+ Map? realmAvailableVideoChatProviders,
bool? realmPresenceDisabled,
Map? realmDefaultExternalAccounts,
+ String? jitsiServerUrl,
int? maxFileUploadSizeMib,
Uri? serverEmojiDataUrl,
+ String? serverJitsiServerUrl,
String? realmEmptyTopicDisplayName,
List? realmUsers,
List? realmNonActiveUsers,
@@ -1412,23 +1418,29 @@ InitialSnapshot initialSnapshot({
userSettings: userSettings ?? _userSettings(),
userTopics: userTopics ?? [],
// no default; allow `null` to simulate servers without this
+ hasZoomToken: hasZoomToken ?? false,
realmCanDeleteAnyMessageGroup: realmCanDeleteAnyMessageGroup,
realmCanDeleteOwnMessageGroup: realmCanDeleteOwnMessageGroup,
realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy,
realmWildcardMentionPolicy: realmWildcardMentionPolicy ?? RealmWildcardMentionPolicy.everyone,
realmMandatoryTopics: realmMandatoryTopics ?? true,
realmName: realmName ?? 'Example Zulip organization',
+ realmVideoChatProvider: realmVideoChatProvider ?? RealmVideoChatProvider.jitsiMeet,
+ realmJitsiServerUrl: realmJitsiServerUrl,
realmWaitingPeriodThreshold: realmWaitingPeriodThreshold ?? 0,
realmMessageContentDeleteLimitSeconds: realmMessageContentDeleteLimitSeconds,
realmAllowMessageEditing: realmAllowMessageEditing ?? true,
realmMessageContentEditLimitSeconds: realmMessageContentEditLimitSeconds,
realmEnableReadReceipts: realmEnableReadReceipts ?? true,
realmIconUrl: realmIconUrl ?? _realmIcon,
+ realmAvailableVideoChatProviders: realmAvailableVideoChatProviders ?? {},
realmPresenceDisabled: realmPresenceDisabled ?? false,
realmDefaultExternalAccounts: realmDefaultExternalAccounts ?? {},
+ jitsiServerUrl: jitsiServerUrl ?? 'https://meet.jit.si',
maxFileUploadSizeMib: maxFileUploadSizeMib ?? 25,
serverEmojiDataUrl: serverEmojiDataUrl
?? realmUrl.replace(path: '/static/emoji.json'),
+ serverJitsiServerUrl: serverJitsiServerUrl,
realmEmptyTopicDisplayName: realmEmptyTopicDisplayName ?? defaultRealmEmptyTopicDisplayName,
realmUsers: realmUsers ?? [selfUser],
realmNonActiveUsers: realmNonActiveUsers ?? [],
diff --git a/test/widgets/compose_box_test.dart b/test/widgets/compose_box_test.dart
index a847120691..121eb018aa 100644
--- a/test/widgets/compose_box_test.dart
+++ b/test/widgets/compose_box_test.dart
@@ -62,7 +62,13 @@ void main() {
List? streams,
List subscriptions = const [],
List? messages,
+ bool? hasZoomToken,
bool? mandatoryTopics,
+ RealmVideoChatProvider? realmVideoChatProvider,
+ String? realmJitsiServerUrl,
+ Map? realmAvailableVideoChatProviders,
+ String? jitsiServerUrl,
+ String? serverJitsiServerUrl,
int? zulipFeatureLevel,
int? maxTopicLength,
}) async {
@@ -90,6 +96,8 @@ void main() {
subscriptions: subscriptions,
zulipFeatureLevel: zulipFeatureLevel,
realmMandatoryTopics: mandatoryTopics,
+ realmVideoChatProvider: realmVideoChatProvider,
+ jitsiServerUrl: jitsiServerUrl,
realmAllowMessageEditing: true,
realmMessageContentEditLimitSeconds: null,
maxTopicLength: maxTopicLength,
@@ -1053,6 +1061,44 @@ void main() {
});
});
+ group('compose call button', () {
+ Future prepare(WidgetTester tester, {
+ String? jitsiServerUrl,
+ RealmVideoChatProvider? realmVideoChatProvider,
+ bool? hasZoomToken,
+ }) async {
+ TypingNotifier.debugEnable = false;
+ addTearDown(TypingNotifier.debugReset);
+
+ final channel = eg.stream();
+ final narrow = ChannelNarrow(channel.streamId);
+ await prepareComposeBox(tester,
+ narrow: narrow,
+ streams: [channel],
+ realmVideoChatProvider : realmVideoChatProvider,
+ jitsiServerUrl : jitsiServerUrl,
+ hasZoomToken: hasZoomToken,
+ );
+
+ await enterTopic(tester, narrow: narrow, topic: 'some topic');
+ await tester.pump();
+ }
+
+ group('add video call link', () {
+ testWidgets('jitsi success', (tester) async {
+ await prepare(tester);
+ connection.prepare();
+
+ await tester.tap(find.byIcon(ZulipIcons.video));
+ await tester.pump();
+ check(controller!.content.text)
+ ..startsWith('[Join video call.](https://meet.jit.si')
+ ..endsWith('#config.startWithVideoMuted=false)\n\n');
+ });
+
+ });
+ });
+
group('uploads', () {
void checkAppearsLoading(WidgetTester tester, bool expected) {
final sendButtonElement = tester.element(find.ancestor(
@@ -1332,6 +1378,7 @@ void main() {
check(attachButtonFinder(ZulipIcons.attach_file).evaluate().length).equals(areShown ? 1 : 0);
check(attachButtonFinder(ZulipIcons.image).evaluate().length).equals(areShown ? 1 : 0);
check(attachButtonFinder(ZulipIcons.camera).evaluate().length).equals(areShown ? 1 : 0);
+ check(attachButtonFinder(ZulipIcons.video).evaluate().length).equals(areShown ? 1 : 0);
}
void checkBannerWithLabel(String label, {required bool isShown}) {