From f9452878aa133160ce3ed92bee8b8c83949ee0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= Date: Thu, 16 Mar 2023 08:09:24 +0000 Subject: [PATCH 01/14] Translated using Weblate (French) Currently translated at 93.3% (941 of 1008 strings) Translation: mattermost-languages-shipped/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/fr/ --- assets/base/i18n/fr.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/base/i18n/fr.json b/assets/base/i18n/fr.json index f9b300800..62f2d6b73 100644 --- a/assets/base/i18n/fr.json +++ b/assets/base/i18n/fr.json @@ -992,5 +992,10 @@ "invite.members.user_is_guest": "", "invite.members.already_member": "", "mobile.calls_start_call_exists": "", - "mobile.calls_not_connected": "" + "mobile.calls_not_connected": "", + "channel_notification_preferences.notification.thread_replies": "M'avertir des réponses aux fils de discussion que je suis sur cette chaîne", + "channel_notification_preferences.notification.mention": "Mentions, messages directs uniquement", + "channel_notification_preferences.notification.all": "Tous les nouveaux messages", + "channel_notification_preferences.muted_title": "Ce canal est en sourdine", + "channel_notification_preferences.muted_content": "Vous pouvez modifier les paramètres de notification, mais vous ne recevrez pas de notifications tant que le canal n'aura pas été désactivé." } From 3b2aa0e2989bde23e9ccc29af0b73fac78e35529 Mon Sep 17 00:00:00 2001 From: Kaya Zeren Date: Fri, 17 Mar 2023 04:51:18 +0000 Subject: [PATCH 02/14] Translated using Weblate (Turkish) Currently translated at 100.0% (1008 of 1008 strings) Translation: mattermost-languages-shipped/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/tr/ --- assets/base/i18n/tr.json | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/assets/base/i18n/tr.json b/assets/base/i18n/tr.json index 8200169c1..5e41b2312 100644 --- a/assets/base/i18n/tr.json +++ b/assets/base/i18n/tr.json @@ -398,7 +398,7 @@ "channel_info.copied": "Kopyalandı", "channel_info.convert_private_title": "{displayName} kanalı özel kanala dönüştürülsün mü?", "channel_info.convert_private_success": "{displayName} özel bir kanala dönüştürüldü.", - "channel_info.convert_private_description": "{displayName} özel bir kanala dönüştürüldüğünde, geçmiş iletiler ve üyelikler korunur. Herkese açık olarak paylaşılmış dosyalara bağlantıya sahip olan herkes erişmeye devam edebilir. Özel kanallara yalnızca çağrı ile üye olunabilir. \n \nBu değişiklik kalıcıdır ve geri alınamaz.\n\n{displayName} kanalını özel kanala dönüştürmek istediğinize emin misiniz?", + "channel_info.convert_private_description": "{displayName} özel bir kanala dönüştürüldüğünde, geçmiş iletiler ve üyelikler korunur. Herkese açık olarak paylaşılmış dosyalara bağlantıya sahip olan herkes erişmeyi sürdürebilir. Özel kanallara yalnızca çağrı ile üye olunabilir. \n \nBu değişiklik kalıcıdır ve geri alınamaz.\n\n{displayName} kanalını özel kanala dönüştürmek istediğinize emin misiniz?", "channel_info.convert_private": "Özel kanala dönüştür", "channel_info.convert_failed": "{displayName} özel kanala dönüştürülemedi.", "channel_info.close_dm_channel": "Bu doğrudan iletiyi kapatmak istediğinize emin misiniz? Bu işlem iletiyi giriş sayfanızdan kaldıracak, ancak istediğiniz zaman yeniden açabilirsiniz.", @@ -549,7 +549,7 @@ "mobile.create_direct_message.start": "Görüşmeyi başlat", "mobile.create_channel.title": "Yeni kanal", "mobile.components.select_server_view.msg_welcome": "Hoş geldiniz", - "mobile.components.select_server_view.msg_description": "Bir sunucuya benzersiz bir adres üzerinden erişilir ve takımınızın iletişim merkezidir", + "mobile.components.select_server_view.msg_description": "Bir sunucu takımınızın benzersiz bir adres üzerinden erişilen iletişim merkezidir", "mobile.components.select_server_view.msg_connect": "Bir sunucu ile bağlantı kuralım", "mobile.components.select_server_view.displayName": "Görüntülenecek ad", "mobile.components.select_server_view.displayHelp": "Sunucunuzun görüntülenecek adını seçin", @@ -971,7 +971,7 @@ "display_settings.crt.off": "Kapat", "display_settings.crt": "Daraltılmış yanıt konuları", "channel_info.archive_description.cannot_view_archived": "Bu işlem kanalı takımdan arşivleyecek ve içeriğine hiç bir kullanıcı erişemeyecek.\n\n{term} {name} kanalını arşivlemek istediğinize emin misiniz?", - "channel_info.archive_description.can_view_archived": "Bu işlem takım kanalını arşivleyecek. Kanal üyeleri kanal içeriğini görmeye devam edebilecek.\n\n{term} {name} kanalını arşivlemek istediğinize emin misiniz?", + "channel_info.archive_description.can_view_archived": "Bu işlem takım kanalını arşivleyecek. Kanal üyeleri kanal içeriğini görmeyi sürdürebilecek.\n\n{term} {name} kanalını arşivlemek istediğinize emin misiniz?", "snack.bar.remove.user": "1 üye kanaldan çıkarıldı", "mobile.manage_members.section_title_members": "ÜYELER", "mobile.manage_members.section_title_admins": "KANAL YÖNETİCİLERİ", @@ -987,10 +987,24 @@ "mobile.manage_members.change_role.error": "Rol güncellenirken bir sorun çıktı. Lütfen bağlantınızı denetleyip yeniden deneyin.", "mobile.manage_members.cancel": "İptal", "mobile.manage_members.admin": "Yönetici", - "mobile.calls_recording_stop_none_in_progress": "", - "mobile.calls_recording_stop_no_permissions": "", - "mobile.calls_recording_start_no_permissions": "", - "mobile.calls_recording_start_in_progress": "", - "mobile.calls_host_rec_error_title": "", - "mobile.calls_host_rec_error": "" + "mobile.calls_recording_stop_none_in_progress": "Sürmekte olan bir kayıt yok.", + "mobile.calls_recording_stop_no_permissions": "Kaydı durdurma izniniz yok. Lütfen çağrı sahibinden kaydı durdurmasını isteyin.", + "mobile.calls_recording_start_no_permissions": "Kayıt başlatma izniniz yok. Lütfen çağrı sahibinden kaydı başlatmasını isteyin.", + "mobile.calls_recording_start_in_progress": "Sürmekte olan bir kayıt var.", + "mobile.calls_host_rec_error_title": "Kayıtta bir sorun çıktı", + "mobile.calls_host_rec_error": "Lütfen yeniden kaydetmeyi deneyin. Sorun giderme yardımı için sistem yöneticinizle de görüşebilirsiniz.", + "notification.no_post": "İleti bulunamadı.", + "notification.no_connection": "Sunucuya ulaşılamadığından bildirim kanalını / takımını alamadık.", + "invite.summary.back": "Geri dön", + "channel_notification_preferences.muted_title": "Bu kanalın bildirimleri kapatılmış", + "channel_notification_preferences.muted_content": "Bildirim ayarlarını değiştirebilirsiniz, ancak kanal bildirimleri açılana kadar bildirim almazsınız.", + "channel_notification_preferences.unmute_content": "Kanal bildirimlerini aç", + "channel_notification_preferences.thread_replies": "Konu yanıtları", + "channel_notification_preferences.reset_default": "Varsayılana dön", + "channel_notification_preferences.notify_about": "Bana şunlar bildirilsin...", + "channel_notification_preferences.notification.thread_replies": "Bu kanalda izlediğim konulara verilen yanıtlar bana bildirilsin", + "channel_notification_preferences.notification.none": "Hiçbir şey", + "channel_notification_preferences.notification.mention": "Anmalar, yalnızca doğrudan iletiler", + "channel_notification_preferences.notification.all": "Tüm yeni iletiler", + "channel_notification_preferences.default": "(varsayılan)" } From cc51fd905b84eb27ffdc65615104f5e532a80f61 Mon Sep 17 00:00:00 2001 From: Christopher Poile Date: Mon, 20 Mar 2023 08:56:51 -0400 Subject: [PATCH 03/14] MM-51362 - Fix proximity notice engaging after call ended (#7205) * fix proximity notice engaging after call ended * remove fork --- app/products/calls/actions/calls.ts | 1 - ios/Podfile.lock | 4 +- package-lock.json | 10 +-- package.json | 2 +- .../modules/react-native-incall-manager.d.ts | 73 +++++++++++++++++++ 5 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 types/modules/react-native-incall-manager.d.ts diff --git a/app/products/calls/actions/calls.ts b/app/products/calls/actions/calls.ts index 96ca93af5..250484122 100644 --- a/app/products/calls/actions/calls.ts +++ b/app/products/calls/actions/calls.ts @@ -279,7 +279,6 @@ export const leaveCall = () => { connection.disconnect(); connection = null; } - setSpeakerphoneOn(false); }; export const leaveCallPopCallScreen = async () => { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ae89e5cbd..a5403ae5d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -500,7 +500,7 @@ PODS: - React-perflogger (= 0.71.3) - ReactNativeExceptionHandler (2.10.10): - React-Core - - ReactNativeIncallManager (4.0.0): + - ReactNativeIncallManager (4.0.1): - React-Core - ReactNativeKeyboardTrackingView (5.7.0): - React @@ -999,7 +999,7 @@ SPEC CHECKSUMS: React-runtimeexecutor: 7bf0dafc7b727d93c8cb94eb00a9d3753c446c3e ReactCommon: 6f65ea5b7d84deb9e386f670dd11ce499ded7b40 ReactNativeExceptionHandler: b11ff67c78802b2f62eed0e10e75cb1ef7947c60 - ReactNativeIncallManager: b169b57d3064d8f62478f8fc3c485da6c75045d1 + ReactNativeIncallManager: 0d2cf9f4d50359728a30c08549762fe67a2efb81 ReactNativeKeyboardTrackingView: 02137fac3b2ebd330d74fa54ead48b14750a2306 ReactNativeNavigation: d79d9d53e6025851936bb8b3d13760b86302a669 RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd diff --git a/package-lock.json b/package-lock.json index e5fb3b60f..1c4f4f186 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,7 @@ "react-native-hw-keyboard-event": "0.0.4", "react-native-image-picker": "5.0.2", "react-native-in-app-review": "4.2.1", - "react-native-incall-manager": "github:cpoile/react-native-incall-manager", + "react-native-incall-manager": "4.0.1", "react-native-keyboard-aware-scroll-view": "0.9.5", "react-native-keyboard-tracking-view": "5.7.0", "react-native-keychain": "8.1.1", @@ -18461,8 +18461,8 @@ "integrity": "sha512-Bny/ukRhkSSzlsbVpB3vsIuWYuOHBPlxguNwZ0TWK+7IQq8/vTRDf17y1P/4+jMIjBO0WNJCzBxHkXdnlhoTmQ==" }, "node_modules/react-native-incall-manager": { - "version": "4.0.0", - "resolved": "git+ssh://git@github.com/cpoile/react-native-incall-manager.git#6b66ae7bab194c82573c7b3891b0ac3af71d424e", + "version": "4.0.1", + "resolved": "git+ssh://git@github.com/react-native-webrtc/react-native-incall-manager.git#6d927ef24c6e47c6134177a4bb14a71054f85b65", "license": "ISC", "peerDependencies": { "react-native": ">=0.40.0" @@ -35891,8 +35891,8 @@ "integrity": "sha512-Bny/ukRhkSSzlsbVpB3vsIuWYuOHBPlxguNwZ0TWK+7IQq8/vTRDf17y1P/4+jMIjBO0WNJCzBxHkXdnlhoTmQ==" }, "react-native-incall-manager": { - "version": "git+ssh://git@github.com/cpoile/react-native-incall-manager.git#6b66ae7bab194c82573c7b3891b0ac3af71d424e", - "from": "react-native-incall-manager@github:cpoile/react-native-incall-manager", + "version": "git+ssh://git@github.com/react-native-webrtc/react-native-incall-manager.git#6d927ef24c6e47c6134177a4bb14a71054f85b65", + "from": "react-native-incall-manager@4.0.1", "requires": {} }, "react-native-iphone-x-helper": { diff --git a/package.json b/package.json index 7b440471a..2830122a7 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "react-native-hw-keyboard-event": "0.0.4", "react-native-image-picker": "5.0.2", "react-native-in-app-review": "4.2.1", - "react-native-incall-manager": "github:cpoile/react-native-incall-manager", + "react-native-incall-manager": "4.0.1", "react-native-keyboard-aware-scroll-view": "0.9.5", "react-native-keyboard-tracking-view": "5.7.0", "react-native-keychain": "8.1.1", diff --git a/types/modules/react-native-incall-manager.d.ts b/types/modules/react-native-incall-manager.d.ts new file mode 100644 index 000000000..f58bde157 --- /dev/null +++ b/types/modules/react-native-incall-manager.d.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// Type definitions for react-native-incall-manager 3.2 +// Project: https://github.com/zxcpoiu/react-native-incall-manager#readme +// Definitions by: Carlos Quiroga +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.8 + +declare module 'react-native-incall-manager' { + export interface StartSetup { + media?: string | undefined; + auto?: boolean | undefined; + ringback?: string | undefined; + } + + export interface StopSetup { + busytone?: string | undefined; + } + + export class InCallManager { + start(setup?: StartSetup): void; + + stop(setup?: StopSetup): void; + + turnScreenOff(): void; + + turnScreenOn(): void; + + getIsWiredHeadsetPluggedIn(): Promise; + + setFlashOn(enable?: boolean, brightness?: number): number; + + setKeepScreenOn(enable?: boolean): void; + + setSpeakerphoneOn(enable?: boolean): void; + + setForceSpeakerphoneOn(_flag?: boolean): void; + + setMicrophoneMute(enable?: boolean): void; + + startRingtone( + ringtone?: string, + vibrate_pattern?: any[], + ios_category?: string, + seconds?: number + ): void; + + stopRingtone(): void; + + startProximitySensor(): void; + + stopProximitySensor(): void; + + startRingback(ringback?: string): void; + + stopRingback(): void; + + pokeScreen(_timeout?: number): void; + + getAudioUri(audioType: string, fileType: string): any; + + chooseAudioRoute(route: any): any; + + requestAudioFocus(): void; + + abandonAudioFocus(): void; + } + + declare const _default: InCallManager; + + export default _default; +} From c03758f54932be736fccecfefdb82e4ce8f53ebb Mon Sep 17 00:00:00 2001 From: Christopher Poile Date: Mon, 20 Mar 2023 09:02:58 -0400 Subject: [PATCH 04/14] clear screensharing state if current screensharer leaves call (#7213) --- app/products/calls/state/actions.test.ts | 60 ++++++++++++++++++++++++ app/products/calls/state/actions.ts | 12 +++++ 2 files changed, 72 insertions(+) diff --git a/app/products/calls/state/actions.test.ts b/app/products/calls/state/actions.test.ts index 87d020cb6..b02dbbe33 100644 --- a/app/products/calls/state/actions.test.ts +++ b/app/products/calls/state/actions.test.ts @@ -312,6 +312,66 @@ describe('useCallsState', () => { assert.deepEqual(result.current[2], expectedCurrentCallState); }); + it('leftCall with screensharing on', () => { + const initialCallsState: CallsState = { + ...DefaultCallsState, + calls: { + 'channel-1': { + ...call1, + screenOn: 'user-1', + }, + }, + }; + const initialChannelsWithCallsState = { + 'channel-1': true, + }; + const initialCurrentCallState: CurrentCall = { + ...DefaultCurrentCall, + connected: true, + serverUrl: 'server1', + myUserId: 'myUserId', + ...call1, + screenOn: 'user-1', + }; + const expectedCallsState = { + 'channel-1': { + participants: { + 'user-2': {id: 'user-2', muted: true, raisedHand: 0}, + }, + channelId: 'channel-1', + startTime: 123, + threadId: 'thread-1', + ownerId: 'user-1', + hostId: 'user-1', + screenOn: '', + }, + }; + const expectedChannelsWithCallsState = initialChannelsWithCallsState; + const expectedCurrentCallState: CurrentCall = { + ...initialCurrentCallState, + ...expectedCallsState['channel-1'], + }; + + // setup + const {result} = renderHook(() => { + return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()]; + }); + act(() => { + setCallsState('server1', initialCallsState); + setChannelsWithCalls('server1', initialChannelsWithCallsState); + setCurrentCall(initialCurrentCallState); + }); + assert.deepEqual(result.current[0], initialCallsState); + assert.deepEqual(result.current[1], initialChannelsWithCallsState); + assert.deepEqual(result.current[2], initialCurrentCallState); + + // test + act(() => userLeftCall('server1', 'channel-1', 'user-1')); + assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState); + assert.deepEqual(result.current[1], expectedChannelsWithCallsState); + assert.deepEqual(result.current[2], expectedCurrentCallState); + }); + it('callStarted', () => { // setup const {result} = renderHook(() => { diff --git a/app/products/calls/state/actions.ts b/app/products/calls/state/actions.ts index ba4d26555..73c4f3d40 100644 --- a/app/products/calls/state/actions.ts +++ b/app/products/calls/state/actions.ts @@ -141,6 +141,12 @@ export const userLeftCall = (serverUrl: string, channelId: string, userId: strin participants: {...callsState.calls[channelId].participants}, }; delete nextCall.participants[userId]; + + // If they were screensharing, remove that. + if (nextCall.screenOn === userId) { + nextCall.screenOn = ''; + } + const nextCalls = {...callsState.calls}; if (Object.keys(nextCall.participants).length === 0) { delete nextCalls[channelId]; @@ -177,6 +183,12 @@ export const userLeftCall = (serverUrl: string, channelId: string, userId: strin voiceOn, }; delete nextCurrentCall.participants[userId]; + + // If they were screensharing, remove that. + if (nextCurrentCall.screenOn === userId) { + nextCurrentCall.screenOn = ''; + } + setCurrentCall(nextCurrentCall); }; From c0cb14e92726a908c42d5c18d3b9a209fb982d9a Mon Sep 17 00:00:00 2001 From: Christopher Poile Date: Tue, 21 Mar 2023 09:18:45 -0400 Subject: [PATCH 05/14] MM-50010 - Share common calls code between web and mobile (#7211) * use calls/common rtcpeer; @calls -> @mmcalls * gitpkg -> standalone github calls-common package * use new mattermost/calls-common repo; add types to WebSocketMessages * mmcalls/common -> mattermost/calls; use a version tag --- app/products/calls/actions/calls.ts | 9 +- app/products/calls/client/rest.ts | 16 +- app/products/calls/components/call_avatar.tsx | 4 +- app/products/calls/connection/connection.ts | 28 ++- .../connection/websocket_event_handlers.ts | 43 ++-- app/products/calls/rtcpeer/index.ts | 210 ------------------ app/products/calls/rtcpeer/types.ts | 8 - app/products/calls/state/actions.test.ts | 11 +- app/products/calls/state/actions.ts | 14 +- app/products/calls/state/calls_config.ts | 6 +- app/products/calls/types/calls.ts | 46 +--- app/products/calls/utils.test.ts | 12 +- app/products/calls/utils.ts | 6 +- jest.config.js | 2 +- package-lock.json | 23 ++ package.json | 2 + 16 files changed, 120 insertions(+), 320 deletions(-) delete mode 100644 app/products/calls/rtcpeer/index.ts delete mode 100644 app/products/calls/rtcpeer/types.ts diff --git a/app/products/calls/actions/calls.ts b/app/products/calls/actions/calls.ts index 250484122..274253dc2 100644 --- a/app/products/calls/actions/calls.ts +++ b/app/products/calls/actions/calls.ts @@ -43,14 +43,13 @@ import type { ApiResp, Call, CallParticipant, - CallReactionEmoji, CallsConnection, - RecordingState, ServerCallState, ServerChannelState, } from '@calls/types/calls'; import type {Client} from '@client/rest'; import type ClientError from '@client/rest/error'; +import type {CallRecordingState, EmojiData} from '@mattermost/calls/lib/types'; import type {IntlShape} from 'react-intl'; let connection: CallsConnection | null = null; @@ -321,7 +320,7 @@ export const unraiseHand = () => { } }; -export const sendReaction = (emoji: CallReactionEmoji) => { +export const sendReaction = (emoji: EmojiData) => { if (connection) { connection.sendReaction(emoji); } @@ -414,7 +413,7 @@ export const startCallRecording = async (serverUrl: string, callId: string) => { const client = NetworkManager.getClient(serverUrl); - let data: ApiResp | RecordingState; + let data: ApiResp | CallRecordingState; try { data = await client.startCallRecording(callId); } catch (error) { @@ -432,7 +431,7 @@ export const stopCallRecording = async (serverUrl: string, callId: string) => { const client = NetworkManager.getClient(serverUrl); - let data: ApiResp | RecordingState; + let data: ApiResp | CallRecordingState; try { data = await client.stopCallRecording(callId); } catch (error) { diff --git a/app/products/calls/client/rest.ts b/app/products/calls/client/rest.ts index 140a3ccd9..f9ba29670 100644 --- a/app/products/calls/client/rest.ts +++ b/app/products/calls/client/rest.ts @@ -1,24 +1,20 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type { - ServerChannelState, - ServerCallsConfig, - ApiResp, - RecordingState, -} from '@calls/types/calls'; +import type {ServerChannelState, ApiResp} from '@calls/types/calls'; +import type {CallRecordingState, CallsConfig} from '@mattermost/calls/lib/types'; import type {RTCIceServer} from 'react-native-webrtc'; export interface ClientCallsMix { getEnabled: () => Promise; getCalls: () => Promise; getCallForChannel: (channelId: string) => Promise; - getCallsConfig: () => Promise; + getCallsConfig: () => Promise; enableChannelCalls: (channelId: string, enable: boolean) => Promise; endCall: (channelId: string) => Promise; genTURNCredentials: () => Promise; - startCallRecording: (callId: string) => Promise; - stopCallRecording: (callId: string) => Promise; + startCallRecording: (callId: string) => Promise; + stopCallRecording: (callId: string) => Promise; } const ClientCalls = (superclass: any) => class extends superclass { @@ -52,7 +48,7 @@ const ClientCalls = (superclass: any) => class extends superclass { return this.doFetch( `${this.getCallsRoute()}/config`, {method: 'get'}, - ) as ServerCallsConfig; + ) as CallsConfig; }; enableChannelCalls = async (channelId: string, enable: boolean) => { diff --git a/app/products/calls/components/call_avatar.tsx b/app/products/calls/components/call_avatar.tsx index fb6323df7..10015d773 100644 --- a/app/products/calls/components/call_avatar.tsx +++ b/app/products/calls/components/call_avatar.tsx @@ -8,7 +8,7 @@ import CompassIcon from '@components/compass_icon'; import Emoji from '@components/emoji'; import ProfilePicture from '@components/profile_picture'; -import type {CallReactionEmoji} from '@calls/types/calls'; +import type {EmojiData} from '@mattermost/calls/lib/types'; import type UserModel from '@typings/database/models/servers/user'; type Props = { @@ -18,7 +18,7 @@ type Props = { muted?: boolean; sharingScreen?: boolean; raisedHand?: boolean; - reaction?: CallReactionEmoji; + reaction?: EmojiData; size?: 'm' | 'l'; } diff --git a/app/products/calls/connection/connection.ts b/app/products/calls/connection/connection.ts index 3fa559fdd..9deba7084 100644 --- a/app/products/calls/connection/connection.ts +++ b/app/products/calls/connection/connection.ts @@ -1,28 +1,28 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import {deflate} from 'pako/lib/deflate.js'; +import {RTCPeer} from '@mattermost/calls/lib'; +import {deflate} from 'pako'; import {DeviceEventEmitter, EmitterSubscription} from 'react-native'; import InCallManager from 'react-native-incall-manager'; import { MediaStream, MediaStreamTrack, mediaDevices, + RTCPeerConnection, } from 'react-native-webrtc'; -import RTCPeer from '@calls/rtcpeer'; import {setSpeakerPhone} from '@calls/state'; import {getICEServersConfigs} from '@calls/utils'; import {WebsocketEvents} from '@constants'; import {getServerCredentials} from '@init/credentials'; import NetworkManager from '@managers/network_manager'; -import {logError, logDebug, logWarning} from '@utils/log'; +import {logError, logDebug, logWarning, logInfo} from '@utils/log'; import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client'; -import type {CallReactionEmoji, CallsConnection} from '@calls/types/calls'; +import type {CallsConnection} from '@calls/types/calls'; +import type {EmojiData} from '@mattermost/calls/lib/types'; const peerConnectTimeout = 5000; @@ -164,7 +164,7 @@ export async function newConnection( } }; - const sendReaction = (emoji: CallReactionEmoji) => { + const sendReaction = (emoji: EmojiData) => { if (ws) { ws.send('react', { data: JSON.stringify(emoji), @@ -204,7 +204,19 @@ export async function newConnection( InCallManager.start({media: 'video'}); setSpeakerPhone(true); - peer = new RTCPeer({iceServers: iceConfigs || []}); + peer = new RTCPeer({ + iceServers: iceConfigs || [], + logger: { + logDebug, + logErr: logError, + logWarn: logWarning, + logInfo, + }, + webrtc: { + MediaStream, + RTCPeerConnection, + }, + }); peer.on('offer', (sdp) => { logDebug(`local offer, sending: ${JSON.stringify(sdp)}`); diff --git a/app/products/calls/connection/websocket_event_handlers.ts b/app/products/calls/connection/websocket_event_handlers.ts index 1cd0c6032..69585cf61 100644 --- a/app/products/calls/connection/websocket_event_handlers.ts +++ b/app/products/calls/connection/websocket_event_handlers.ts @@ -22,34 +22,43 @@ import { import {WebsocketEvents} from '@constants'; import DatabaseManager from '@database/manager'; -export const handleCallUserConnected = (serverUrl: string, msg: WebSocketMessage) => { +import type { + CallHostChangedData, CallRecordingStateData, + CallStartData, + EmptyData, + UserConnectedData, + UserDisconnectedData, UserMutedUnmutedData, UserRaiseUnraiseHandData, + UserReactionData, UserScreenOnOffData, UserVoiceOnOffData, +} from '@mattermost/calls/lib/types'; + +export const handleCallUserConnected = (serverUrl: string, msg: WebSocketMessage) => { // Load user model async (if needed). fetchUsersByIds(serverUrl, [msg.data.userID]); userJoinedCall(serverUrl, msg.broadcast.channel_id, msg.data.userID); }; -export const handleCallUserDisconnected = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallUserDisconnected = (serverUrl: string, msg: WebSocketMessage) => { userLeftCall(serverUrl, msg.broadcast.channel_id, msg.data.userID); }; -export const handleCallUserMuted = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallUserMuted = (serverUrl: string, msg: WebSocketMessage) => { setUserMuted(serverUrl, msg.broadcast.channel_id, msg.data.userID, true); }; -export const handleCallUserUnmuted = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallUserUnmuted = (serverUrl: string, msg: WebSocketMessage) => { setUserMuted(serverUrl, msg.broadcast.channel_id, msg.data.userID, false); }; -export const handleCallUserVoiceOn = (msg: WebSocketMessage) => { +export const handleCallUserVoiceOn = (msg: WebSocketMessage) => { setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, true); }; -export const handleCallUserVoiceOff = (msg: WebSocketMessage) => { +export const handleCallUserVoiceOff = (msg: WebSocketMessage) => { setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, false); }; -export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return; @@ -66,7 +75,7 @@ export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage) => { }); }; -export const handleCallEnded = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallEnded = (serverUrl: string, msg: WebSocketMessage) => { DeviceEventEmitter.emit(WebsocketEvents.CALLS_CALL_END, { channelId: msg.broadcast.channel_id, }); @@ -74,38 +83,38 @@ export const handleCallEnded = (serverUrl: string, msg: WebSocketMessage) => { callEnded(serverUrl, msg.broadcast.channel_id); }; -export const handleCallChannelEnabled = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallChannelEnabled = (serverUrl: string, msg: WebSocketMessage) => { setChannelEnabled(serverUrl, msg.broadcast.channel_id, true); }; -export const handleCallChannelDisabled = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallChannelDisabled = (serverUrl: string, msg: WebSocketMessage) => { setChannelEnabled(serverUrl, msg.broadcast.channel_id, false); }; -export const handleCallScreenOn = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallScreenOn = (serverUrl: string, msg: WebSocketMessage) => { setCallScreenOn(serverUrl, msg.broadcast.channel_id, msg.data.userID); }; -export const handleCallScreenOff = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallScreenOff = (serverUrl: string, msg: WebSocketMessage) => { setCallScreenOff(serverUrl, msg.broadcast.channel_id); }; -export const handleCallUserRaiseHand = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallUserRaiseHand = (serverUrl: string, msg: WebSocketMessage) => { setRaisedHand(serverUrl, msg.broadcast.channel_id, msg.data.userID, msg.data.raised_hand); }; -export const handleCallUserUnraiseHand = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallUserUnraiseHand = (serverUrl: string, msg: WebSocketMessage) => { setRaisedHand(serverUrl, msg.broadcast.channel_id, msg.data.userID, msg.data.raised_hand); }; -export const handleCallUserReacted = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallUserReacted = (serverUrl: string, msg: WebSocketMessage) => { userReacted(serverUrl, msg.broadcast.channel_id, msg.data); }; -export const handleCallRecordingState = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallRecordingState = (serverUrl: string, msg: WebSocketMessage) => { setRecordingState(serverUrl, msg.broadcast.channel_id, msg.data.recState); }; -export const handleCallHostChanged = (serverUrl: string, msg: WebSocketMessage) => { +export const handleCallHostChanged = (serverUrl: string, msg: WebSocketMessage) => { setHost(serverUrl, msg.broadcast.channel_id, msg.data.hostID); }; diff --git a/app/products/calls/rtcpeer/index.ts b/app/products/calls/rtcpeer/index.ts deleted file mode 100644 index 742023071..000000000 --- a/app/products/calls/rtcpeer/index.ts +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -/* eslint-disable @typescript-eslint/ban-ts-comment */ - -import {EventEmitter} from 'events'; - -import { - MediaStream, - MediaStreamTrack, - RTCIceCandidate, - RTCPeerConnection, - RTCPeerConnectionIceEvent, - RTCRtpSender, - RTCSessionDescription, -} from 'react-native-webrtc'; - -import {logDebug, logError} from '@utils/log'; - -import type {RTCPeerConfig} from './types'; -import type RTCTrackEvent from 'react-native-webrtc/lib/typescript/RTCTrackEvent'; - -const rtcConnFailedErr = new Error('rtc connection failed'); - -export default class RTCPeer extends EventEmitter { - private pc: RTCPeerConnection | null; - private readonly senders: { [key: string]: RTCRtpSender }; - private candidates: RTCIceCandidate[] = []; - private makingOffer = false; - - public connected: boolean; - - constructor(config: RTCPeerConfig) { - super(); - - // We keep a map of track IDs -> RTP sender so that we can easily - // replace tracks when muting/unmuting. - this.senders = {}; - - this.pc = new RTCPeerConnection(config); - this.pc.onnegotiationneeded = () => this.onNegotiationNeeded(); - this.pc.onicecandidate = (ev) => this.onICECandidate(ev); - this.pc.oniceconnectionstatechange = () => this.onICEConnectionStateChange(); - this.pc.onconnectionstatechange = () => this.onConnectionStateChange(); - this.pc.ontrack = (ev) => this.onTrack(ev); - - this.connected = false; - - // We create a data channel for two reasons: - // - Initiate a connection without preemptively adding audio/video tracks. - // - Use this communication channel for further negotiation (to be implemented). - this.pc.createDataChannel('calls-dc'); - } - - private onICECandidate(ev: RTCPeerConnectionIceEvent) { - if (ev.candidate) { - this.emit('candidate', ev.candidate); - } - } - - private onConnectionStateChange() { - switch (this.pc?.connectionState) { - case 'connected': - this.connected = true; - break; - case 'failed': - this.emit('close', rtcConnFailedErr); - break; - } - } - - private onICEConnectionStateChange() { - switch (this.pc?.iceConnectionState) { - case 'connected': - this.emit('connect'); - break; - case 'failed': - this.emit('close', rtcConnFailedErr); - break; - case 'closed': - this.emit('close'); - break; - default: - } - } - - private async onNegotiationNeeded() { - try { - this.makingOffer = true; - await this.pc?.setLocalDescription(); - this.emit('offer', this.pc?.localDescription); - } catch (err) { - this.emit('error', err); - } finally { - this.makingOffer = false; - } - } - - private onTrack(ev: RTCTrackEvent) { - if (ev.streams.length === 0) { - this.emit('stream', new MediaStream([ev.track])); - return; - } - this.emit('stream', ev.streams[0]); - } - - public async signal(data: string) { - if (!this.pc) { - throw new Error('peer has been destroyed'); - } - - const msg = JSON.parse(data); - - if (msg.type === 'offer' && (this.makingOffer || this.pc?.signalingState !== 'stable')) { - logDebug('signaling conflict, we are polite, proceeding...'); - } - - try { - switch (msg.type) { - case 'candidate': - // It's possible that ICE candidates are received moments before - // we set the initial remote description which would cause an - // error. In such case we queue them up to be added later. - if (this.pc.remoteDescription && this.pc.remoteDescription.type) { - this.pc.addIceCandidate(msg.candidate).catch((err) => { - logError('failed to add candidate', err); - }); - } else { - logDebug('received ice candidate before remote description, queuing...'); - this.candidates.push(msg.candidate); - } - break; - case 'offer': - await this.pc.setRemoteDescription(new RTCSessionDescription(msg)); - await this.pc.setLocalDescription(); - this.emit('answer', this.pc.localDescription); - break; - case 'answer': - await this.pc.setRemoteDescription(msg); - for (const candidate of this.candidates) { - logDebug('adding queued ice candidate'); - this.pc.addIceCandidate(candidate).catch((err) => { - logError('failed to add candidate', err); - }); - } - break; - default: - this.emit('error', Error('invalid signaling data received')); - } - } catch (err) { - this.emit('error', err); - } - } - - public async addTrack(track: MediaStreamTrack, stream?: MediaStream) { - if (!this.pc) { - throw new Error('peer has been destroyed'); - } - const sender = await this.pc.addTrack(track, stream!); - if (sender) { - this.senders[track.id] = sender; - } - } - - public addStream(stream: MediaStream) { - stream.getTracks().forEach((track) => { - this.addTrack(track, stream); - }); - } - - public replaceTrack(oldTrackID: string, newTrack: MediaStreamTrack | null) { - const sender = this.senders[oldTrackID]; - if (!sender) { - throw new Error('sender for track not found'); - } - if (newTrack && newTrack.id !== oldTrackID) { - delete this.senders[oldTrackID]; - this.senders[newTrack.id] = sender; - } - sender.replaceTrack(newTrack); - } - - public getStats() { - if (!this.pc) { - throw new Error('peer has been destroyed'); - } - return this.pc.getStats(); - } - - public destroy() { - if (!this.pc) { - throw new Error('peer has been destroyed already'); - } - - this.removeAllListeners('candidate'); - this.removeAllListeners('connect'); - this.removeAllListeners('error'); - this.removeAllListeners('close'); - this.removeAllListeners('offer'); - this.removeAllListeners('answer'); - this.removeAllListeners('stream'); - this.pc.onnegotiationneeded = null; - this.pc.onicecandidate = null; - this.pc.oniceconnectionstatechange = null; - this.pc.onconnectionstatechange = null; - this.pc.ontrack = null; - this.pc.close(); - this.pc = null; - this.connected = false; - } -} diff --git a/app/products/calls/rtcpeer/types.ts b/app/products/calls/rtcpeer/types.ts deleted file mode 100644 index ed9afbc1f..000000000 --- a/app/products/calls/rtcpeer/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import type {RTCIceServer} from 'react-native-webrtc'; - -export type RTCPeerConfig = { - iceServers: RTCIceServer[]; -} diff --git a/app/products/calls/state/actions.test.ts b/app/products/calls/state/actions.test.ts index b02dbbe33..c35eefaf7 100644 --- a/app/products/calls/state/actions.test.ts +++ b/app/products/calls/state/actions.test.ts @@ -40,8 +40,6 @@ import { setPluginEnabled, setUserVoiceOn, } from '@calls/state/actions'; -import {License} from '@constants'; - import { Call, CallsState, @@ -51,8 +49,10 @@ import { DefaultCurrentCall, DefaultGlobalCallsState, GlobalCallsState, - RecordingState, -} from '../types/calls'; +} from '@calls/types/calls'; +import {License} from '@constants'; + +import type {CallRecordingState} from '@mattermost/calls/lib/types'; jest.mock('@calls/alerts'); @@ -857,6 +857,7 @@ describe('useCallsState', () => { it('config', () => { const newConfig = { + ...DefaultCallsConfig, ICEServers: [], ICEServersConfigs: [ { @@ -974,7 +975,7 @@ describe('useCallsState', () => { myUserId: 'myUserId', ...call1, }; - const recState: RecordingState = { + const recState: CallRecordingState = { init_at: 123, start_at: 231, end_at: 345, diff --git a/app/products/calls/state/actions.ts b/app/products/calls/state/actions.ts index 73c4f3d40..b7e266115 100644 --- a/app/products/calls/state/actions.ts +++ b/app/products/calls/state/actions.ts @@ -16,17 +16,17 @@ import { } from '@calls/state'; import { Call, - CallReaction, - CallsConfig, + CallsConfigState, ChannelsWithCalls, CurrentCall, DefaultCall, DefaultCurrentCall, ReactionStreamEmoji, - RecordingState, } from '@calls/types/calls'; import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/calls'; +import type {CallRecordingState, UserReactionData} from '@mattermost/calls/lib/types'; + export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary, enabled: Dictionary) => { const channelsWithCalls = Object.keys(calls).reduce( (accum, next) => { @@ -402,7 +402,7 @@ export const setSpeakerPhone = (speakerphoneOn: boolean) => { } }; -export const setConfig = (serverUrl: string, config: Partial) => { +export const setConfig = (serverUrl: string, config: Partial) => { const callsConfig = getCallsConfig(serverUrl); setCallsConfig(serverUrl, {...callsConfig, ...config}); }; @@ -435,7 +435,7 @@ export const setMicPermissionsErrorDismissed = () => { setCurrentCall(nextCurrentCall); }; -export const userReacted = (serverUrl: string, channelId: string, reaction: CallReaction) => { +export const userReacted = (serverUrl: string, channelId: string, reaction: UserReactionData) => { // Note: Simplification for performance: // If you are not in the call with the reaction, ignore it. There could be many calls ongoing in your // servers, do we want to be tracking reactions and setting timeouts for all those calls? No. @@ -487,7 +487,7 @@ export const userReacted = (serverUrl: string, channelId: string, reaction: Call }, REACTION_TIMEOUT); }; -const userReactionTimeout = (serverUrl: string, channelId: string, reaction: CallReaction) => { +const userReactionTimeout = (serverUrl: string, channelId: string, reaction: UserReactionData) => { const currentCall = getCurrentCall(); if (currentCall?.channelId !== channelId) { return; @@ -511,7 +511,7 @@ const userReactionTimeout = (serverUrl: string, channelId: string, reaction: Cal setCurrentCall(nextCurrentCall); }; -export const setRecordingState = (serverUrl: string, channelId: string, recState: RecordingState) => { +export const setRecordingState = (serverUrl: string, channelId: string, recState: CallRecordingState) => { const callsState = getCallsState(serverUrl); if (!callsState.calls[channelId]) { return; diff --git a/app/products/calls/state/calls_config.ts b/app/products/calls/state/calls_config.ts index 07d9b095d..5008845a2 100644 --- a/app/products/calls/state/calls_config.ts +++ b/app/products/calls/state/calls_config.ts @@ -4,9 +4,9 @@ import {useEffect, useState} from 'react'; import {BehaviorSubject} from 'rxjs'; -import {CallsConfig, DefaultCallsConfig} from '@calls/types/calls'; +import {CallsConfigState, DefaultCallsConfig} from '@calls/types/calls'; -const callsConfigSubjects: Dictionary> = {}; +const callsConfigSubjects: Dictionary> = {}; const getCallsConfigSubject = (serverUrl: string) => { if (!callsConfigSubjects[serverUrl]) { @@ -20,7 +20,7 @@ export const getCallsConfig = (serverUrl: string) => { return getCallsConfigSubject(serverUrl).value; }; -export const setCallsConfig = (serverUrl: string, callsConfig: CallsConfig) => { +export const setCallsConfig = (serverUrl: string, callsConfig: CallsConfigState) => { getCallsConfigSubject(serverUrl).next(callsConfig); }; diff --git a/app/products/calls/types/calls.ts b/app/products/calls/types/calls.ts index 7746f46fd..974da36f6 100644 --- a/app/products/calls/types/calls.ts +++ b/app/products/calls/types/calls.ts @@ -1,8 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type {CallRecordingState, CallsConfig, EmojiData, UserReactionData} from '@mattermost/calls/lib/types'; import type UserModel from '@typings/database/models/servers/user'; -import type {RTCIceServer} from 'react-native-webrtc'; export type GlobalCallsState = { micPermissionsGranted: boolean; @@ -31,7 +31,7 @@ export type Call = { screenOn: string; threadId: string; ownerId: string; - recState?: RecordingState; + recState?: CallRecordingState; hostId: string; } @@ -75,7 +75,7 @@ export type CallParticipant = { muted: boolean; raisedHand: number; userModel?: UserModel; - reaction?: CallReaction; + reaction?: UserReactionData; } export type ChannelsWithCalls = Dictionary; @@ -100,7 +100,7 @@ export type ServerCallState = { screen_sharing_id: string; owner_id: string; host_id: string; - recording: RecordingState; + recording: CallRecordingState; } export type CallsConnection = { @@ -111,26 +111,16 @@ export type CallsConnection = { raiseHand: () => void; unraiseHand: () => void; initializeVoiceTrack: () => void; - sendReaction: (emoji: CallReactionEmoji) => void; + sendReaction: (emoji: EmojiData) => void; } -export type ServerCallsConfig = { - ICEServers?: string[]; // deprecated - ICEServersConfigs: RTCIceServer[]; +export type CallsConfigState = CallsConfig & { AllowEnableCalls: boolean; - DefaultEnabled: boolean; - NeedsTURNCredentials: boolean; - sku_short_name: string; - MaxCallParticipants: number; - EnableRecordings: boolean; -} - -export type CallsConfig = ServerCallsConfig & { pluginEnabled: boolean; last_retrieved_at: number; } -export const DefaultCallsConfig: CallsConfig = { +export const DefaultCallsConfig: CallsConfigState = { pluginEnabled: false, ICEServers: [], // deprecated ICEServersConfigs: [], @@ -141,6 +131,8 @@ export const DefaultCallsConfig: CallsConfig = { sku_short_name: '', MaxCallParticipants: 0, EnableRecordings: false, + MaxRecordingDuration: 60, + AllowScreenSharing: true, }; export type ApiResp = { @@ -149,29 +141,9 @@ export type ApiResp = { status_code: number; } -export type CallReactionEmoji = { - name: string; - skin?: string; - unified: string; - literal?: string; -} - -export type CallReaction = { - user_id: string; - emoji: CallReactionEmoji; - timestamp: number; -} - export type ReactionStreamEmoji = { name: string; latestTimestamp: number; count: number; literal?: string; }; - -export type RecordingState = { - init_at: number; - start_at: number; - end_at: number; - err?: string; -} diff --git a/app/products/calls/utils.test.ts b/app/products/calls/utils.test.ts index 8e985626a..7d794f480 100644 --- a/app/products/calls/utils.test.ts +++ b/app/products/calls/utils.test.ts @@ -3,15 +3,15 @@ import assert from 'assert'; +import {CallsConfigState, DefaultCallsConfig} from '@calls/types/calls'; import {License} from '@constants'; import {getICEServersConfigs} from './utils'; -import type {CallsConfig} from '@calls/types/calls'; - describe('getICEServersConfigs', () => { it('backwards compatible case, no ICEServersConfigs present', () => { - const config: CallsConfig = { + const config: CallsConfigState = { + ...DefaultCallsConfig, pluginEnabled: true, ICEServers: ['stun:stun.example.com:3478'], ICEServersConfigs: [], @@ -33,7 +33,8 @@ describe('getICEServersConfigs', () => { }); it('ICEServersConfigs set', () => { - const config: CallsConfig = { + const config: CallsConfigState = { + ...DefaultCallsConfig, pluginEnabled: true, ICEServersConfigs: [ { @@ -64,7 +65,8 @@ describe('getICEServersConfigs', () => { }); it('Both ICEServers and ICEServersConfigs set', () => { - const config: CallsConfig = { + const config: CallsConfigState = { + ...DefaultCallsConfig, pluginEnabled: true, ICEServers: ['stun:stuna.example.com:3478'], ICEServersConfigs: [ diff --git a/app/products/calls/utils.ts b/app/products/calls/utils.ts index dbe808cc6..f015d1cbd 100644 --- a/app/products/calls/utils.ts +++ b/app/products/calls/utils.ts @@ -8,9 +8,11 @@ import Calls from '@constants/calls'; import {isMinimumServerVersion} from '@utils/helpers'; import {displayUsername} from '@utils/user'; -import type {CallParticipant, ServerCallsConfig} from '@calls/types/calls'; +import type {CallParticipant} from '@calls/types/calls'; +import type {CallsConfig} from '@mattermost/calls/lib/types'; import type PostModel from '@typings/database/models/servers/post'; import type {IntlShape} from 'react-intl'; +import type {RTCIceServer} from 'react-native-webrtc'; export function sortParticipants(teammateNameDisplay: string, participants?: Dictionary, presenterID?: string): CallParticipant[] { if (!participants) { @@ -106,7 +108,7 @@ export function errorAlert(error: string, intl: IntlShape) { ); } -export function getICEServersConfigs(config: ServerCallsConfig) { +export function getICEServersConfigs(config: CallsConfig): RTCIceServer[] { // if ICEServersConfigs is set, we can trust this to be complete and // coming from an updated API. if (config.ICEServersConfigs && config.ICEServersConfigs.length > 0) { diff --git a/jest.config.js b/jest.config.js index ff0657af7..3618e86c1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -19,6 +19,6 @@ module.exports = { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/test/file_transformer.js', }, transformIgnorePatterns: [ - 'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch)', + 'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch|@mattermost/calls)', ], }; diff --git a/package-lock.json b/package-lock.json index 1c4f4f186..b169dcd25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@formatjs/intl-pluralrules": "5.1.8", "@formatjs/intl-relativetimeformat": "11.1.8", "@gorhom/bottom-sheet": "4.4.5", + "@mattermost/calls": "github:mattermost/calls-common#v0.12.0", "@mattermost/compass-icons": "0.1.35", "@mattermost/react-native-emm": "1.3.5", "@mattermost/react-native-network-client": "1.3.2", @@ -124,6 +125,7 @@ "@types/jest": "29.4.0", "@types/lodash": "4.14.191", "@types/mime-db": "1.43.1", + "@types/pako": "2.0.0", "@types/querystringify": "2.0.0", "@types/react": "18.0.28", "@types/react-native-background-timer": "2.0.0", @@ -3195,6 +3197,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@mattermost/calls": { + "name": "@calls/common", + "version": "0.12.0", + "resolved": "git+ssh://git@github.com/mattermost/calls-common.git#02c52be3b71080023803621877ab96a7fe306fba" + }, "node_modules/@mattermost/commonmark": { "version": "0.30.1-0", "resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-0.tgz", @@ -5957,6 +5964,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", "integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==" }, + "node_modules/@types/pako": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz", + "integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==", + "dev": true + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -24264,6 +24277,10 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@mattermost/calls": { + "version": "git+ssh://git@github.com/mattermost/calls-common.git#02c52be3b71080023803621877ab96a7fe306fba", + "from": "@mattermost/calls@github:mattermost/calls-common#v0.12.0" + }, "@mattermost/commonmark": { "version": "0.30.1-0", "resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-0.tgz", @@ -26328,6 +26345,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", "integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==" }, + "@types/pako": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz", + "integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==", + "dev": true + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", diff --git a/package.json b/package.json index 2830122a7..9724dcf03 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@formatjs/intl-pluralrules": "5.1.8", "@formatjs/intl-relativetimeformat": "11.1.8", "@gorhom/bottom-sheet": "4.4.5", + "@mattermost/calls": "github:mattermost/calls-common#v0.12.0", "@mattermost/compass-icons": "0.1.35", "@mattermost/react-native-emm": "1.3.5", "@mattermost/react-native-network-client": "1.3.2", @@ -121,6 +122,7 @@ "@types/jest": "29.4.0", "@types/lodash": "4.14.191", "@types/mime-db": "1.43.1", + "@types/pako": "2.0.0", "@types/querystringify": "2.0.0", "@types/react": "18.0.28", "@types/react-native-background-timer": "2.0.0", From db18151a673c30081f7f423f138ec20fbe8e46bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 22 Mar 2023 12:41:59 +0100 Subject: [PATCH 06/14] Add debug logs (#7225) * Add debug logs * Fix lint --- app/actions/remote/general.ts | 3 +++ app/actions/remote/post.ts | 3 ++- app/screens/home/tab_bar/home.tsx | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/actions/remote/general.ts b/app/actions/remote/general.ts index 21f0b7210..c3d4b755b 100644 --- a/app/actions/remote/general.ts +++ b/app/actions/remote/general.ts @@ -8,6 +8,7 @@ import {t} from '@i18n'; import NetworkManager from '@managers/network_manager'; import {getDeviceToken} from '@queries/app/global'; import {getExpandedLinks, getPushVerificationStatus} from '@queries/servers/system'; +import {logDebug} from '@utils/log'; import {forceLogoutIfNecessary} from './session'; @@ -63,10 +64,12 @@ export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeou } if (!response.ok) { + logDebug('Server ping returned not ok response', response); NetworkManager.invalidateClient(serverUrl); return {error: {intl: pingError}}; } } catch (error) { + logDebug('Server ping threw an exception', error); NetworkManager.invalidateClient(serverUrl); return {error: {intl: pingError}}; } diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index 45e37cf92..b6d4e11a8 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -26,7 +26,7 @@ import {queryAllUsers} from '@queries/servers/user'; import {setFetchingThreadState} from '@store/fetching_thread_store'; import {getValidEmojis, matchEmoticons} from '@utils/emoji/helpers'; import {isServerError} from '@utils/errors'; -import {logError} from '@utils/log'; +import {logDebug, logError} from '@utils/log'; import {processPostsFetched} from '@utils/post'; import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list'; @@ -136,6 +136,7 @@ export async function createPost(serverUrl: string, post: Partial, files: try { created = await client.createPost(newPost); } catch (error) { + logDebug('Error sending a post', error); const errorPost = { ...newPost, id: pendingPostId, diff --git a/app/screens/home/tab_bar/home.tsx b/app/screens/home/tab_bar/home.tsx index 5ba2e5c50..3b8063df2 100644 --- a/app/screens/home/tab_bar/home.tsx +++ b/app/screens/home/tab_bar/home.tsx @@ -12,6 +12,7 @@ import {subscribeAllServers} from '@database/subscription/servers'; import {subscribeUnreadAndMentionsByServer, UnreadObserverArgs} from '@database/subscription/unreads'; import {useAppState} from '@hooks/device'; import NativeNotification from '@notifications'; +import {logDebug} from '@utils/log'; import {changeOpacity} from '@utils/theme'; import type ServersModel from '@typings/database/models/app/servers'; @@ -56,9 +57,11 @@ const Home = ({isFocused, theme}: Props) => { if (Platform.OS === 'ios') { NativeNotification.getDeliveredNotifications().then((delivered) => { if (mentions === 0 && delivered.length > 0) { + logDebug('Not updating badge count, since we have no mentions in the database, and the number of notifications in the notification center is', delivered.length); return; } + logDebug('Setting the badge count based on database values to', mentions); Notifications.ios.setBadgeCount(mentions); }); } From 123590794544fba7dbc6a485c6a6090b12cf3f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 22 Mar 2023 21:02:27 +0100 Subject: [PATCH 07/14] Consider unread when mentions are present regardless of muted state (#7224) --- app/utils/categories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/categories.ts b/app/utils/categories.ts index ab569b9f2..569045985 100644 --- a/app/utils/categories.ts +++ b/app/utils/categories.ts @@ -24,7 +24,7 @@ export function makeCategoryChannelId(teamId: string, channelId: string) { export const isUnreadChannel = (myChannel: MyChannelModel, notifyProps?: Partial, lastUnreadChannelId?: string) => { const isMuted = notifyProps?.mark_unread === General.MENTION; - return (isMuted && myChannel.mentionsCount) || (!isMuted && myChannel.isUnread) || (myChannel.id === lastUnreadChannelId); + return myChannel.mentionsCount || (!isMuted && myChannel.isUnread) || (myChannel.id === lastUnreadChannelId); }; export const filterArchivedChannels = (channelsWithMyChannel: ChannelWithMyChannel[], currentChannelId: string) => { From 5089157951a03c244b2194d1ec2cb85e8c02df96 Mon Sep 17 00:00:00 2001 From: Christopher Poile Date: Thu, 23 Mar 2023 08:24:36 -0400 Subject: [PATCH 08/14] prioritize raised hands when sorting participants (#7229) --- app/products/calls/utils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/products/calls/utils.ts b/app/products/calls/utils.ts index f015d1cbd..d52b31581 100644 --- a/app/products/calls/utils.ts +++ b/app/products/calls/utils.ts @@ -40,12 +40,6 @@ const sortByState = (presenterID?: string) => { return 1; } - if (!a.muted && b.muted) { - return -1; - } else if (!b.muted && a.muted) { - return 1; - } - if (a.raisedHand && !b.raisedHand) { return -1; } else if (b.raisedHand && !a.raisedHand) { @@ -54,6 +48,12 @@ const sortByState = (presenterID?: string) => { return a.raisedHand - b.raisedHand; } + if (!a.muted && b.muted) { + return -1; + } else if (!b.muted && a.muted) { + return 1; + } + return 0; }; }; From 388bb81acd335e082a10e768728efb38f6a1c22c Mon Sep 17 00:00:00 2001 From: Eduardo Sellanes Date: Fri, 24 Mar 2023 13:50:16 -0400 Subject: [PATCH 09/14] photo permission alert is not displayed properly (#7228) --- app/utils/file/file_picker/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/utils/file/file_picker/index.ts b/app/utils/file/file_picker/index.ts index 86e07730f..1e268c673 100644 --- a/app/utils/file/file_picker/index.ts +++ b/app/utils/file/file_picker/index.ts @@ -16,7 +16,7 @@ import type {IntlShape} from 'react-intl'; const MattermostManaged = NativeModules.MattermostManaged; -type PermissionSource = 'camera' | 'storage' | 'denied_android' | 'denied_ios' | 'photo'; +type PermissionSource = 'camera' | 'storage' | 'photo_android' | 'photo_ios' | 'photo'; export default class FilePickerUtil { private readonly uploadFiles: (files: ExtractedFileInfo[]) => void; @@ -64,7 +64,7 @@ export default class FilePickerUtil { 'Upload files to your server. Open Settings to grant {applicationName} Read and Write access to files on this device.', }, {applicationName}), }, - denied_ios: { + photo_ios: { title: formatMessage( { id: 'mobile.ios.photos_permission_denied_title', @@ -79,7 +79,7 @@ export default class FilePickerUtil { 'Upload photos and videos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo and video library.', }, {applicationName}), }, - denied_android: { + photo_android: { title: formatMessage( { id: 'mobile.android.photos_permission_denied_title', @@ -109,8 +109,8 @@ export default class FilePickerUtil { }; private getPermissionDeniedMessage = (source?: PermissionSource) => { - const sources = ['camera', 'storage', 'photo']; - const deniedSource: PermissionSource = Platform.select({android: 'denied_android', ios: 'denied_ios'})!; + const sources = ['camera', 'storage']; + const deniedSource: PermissionSource = Platform.select({android: 'photo_android', ios: 'photo_ios'})!; const msgForSource = source && sources.includes(source) ? source : deniedSource; return this.getPermissionMessages(msgForSource); From 778513adb8d8237646ede82df562c63c721a21ab Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Sun, 26 Mar 2023 11:07:12 -0300 Subject: [PATCH 10/14] Prevent in-app notification and snackbar from blocking gestures (#7231) --- package-lock.json | 14 +++++----- patches/react-native-navigation+7.32.1.patch | 28 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index b169dcd25..f06344b5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21595,9 +21595,9 @@ } }, "node_modules/webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.76.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.3.tgz", + "integrity": "sha512-18Qv7uGPU8b2vqGeEEObnfICyw2g39CHlDEK4I7NK13LOur1d0HGmGNKGT58Eluwddpn3oEejwvBPoP4M7/KSA==", "dev": true, "peer": true, "dependencies": { @@ -26281,7 +26281,7 @@ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", "requires": { - "@types/react": "^18.0.15", + "@types/react": "*", "hoist-non-react-statics": "^3.3.0" } }, @@ -38203,9 +38203,9 @@ } }, "webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.76.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.3.tgz", + "integrity": "sha512-18Qv7uGPU8b2vqGeEEObnfICyw2g39CHlDEK4I7NK13LOur1d0HGmGNKGT58Eluwddpn3oEejwvBPoP4M7/KSA==", "dev": true, "peer": true, "requires": { diff --git a/patches/react-native-navigation+7.32.1.patch b/patches/react-native-navigation+7.32.1.patch index 616628d8f..c5397f409 100644 --- a/patches/react-native-navigation+7.32.1.patch +++ b/patches/react-native-navigation+7.32.1.patch @@ -162,6 +162,34 @@ index a34598c..b035a76 100644 if (navigationActivity != null) { navigationActivity.onCatalystInstanceDestroy(); } +diff --git a/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt b/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt +index a79e487..dba9b65 100644 +--- a/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt ++++ b/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/utils/MotionEvent.kt +@@ -3,11 +3,20 @@ package com.reactnativenavigation.utils + import android.graphics.Rect + import android.view.MotionEvent + import android.view.View ++import android.view.ViewGroup + + val hitRect = Rect() + + fun MotionEvent.coordinatesInsideView(view: View?): Boolean { + view ?: return false +- view.getHitRect(hitRect) +- return hitRect.contains(x.toInt(), y.toInt()) ++ val viewGroup = (view as ViewGroup).getChildAt(0) as ViewGroup ++ return if (viewGroup.childCount > 0) { ++ val content = viewGroup.getChildAt(0) ++ ++ content.getHitRect(hitRect) ++ ++ hitRect.contains(x.toInt(), y.toInt()) ++ } else { ++ false ++ } + } +\ No newline at end of file diff --git a/node_modules/react-native-navigation/lib/android/app/src/reactNative71/java/com/reactnativenavigation/react/ReactGateway.java b/node_modules/react-native-navigation/lib/android/app/src/reactNative71/java/com/reactnativenavigation/react/ReactGateway.java index 035ec31..630b8d4 100644 --- a/node_modules/react-native-navigation/lib/android/app/src/reactNative71/java/com/reactnativenavigation/react/ReactGateway.java From 5bb7238fd1d6238ce4a5497e734ea88e37399536 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Sun, 26 Mar 2023 11:20:41 -0300 Subject: [PATCH 11/14] Bump app build number to 463 (#7232) --- android/app/build.gradle | 2 +- fastlane/Gemfile.lock | 10 +++++----- ios/Mattermost.xcodeproj/project.pbxproj | 8 ++++---- ios/Mattermost/Info.plist | 2 +- ios/MattermostShare/Info.plist | 2 +- ios/NotificationService/Info.plist | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 4e6846464..6ae94ad89 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,7 +110,7 @@ android { applicationId "com.mattermost.rnbeta" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 461 + versionCode 463 versionName "2.1.0" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' diff --git a/fastlane/Gemfile.lock b/fastlane/Gemfile.lock index 5eb077b02..1acefac1f 100644 --- a/fastlane/Gemfile.lock +++ b/fastlane/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.721.0) - aws-sdk-core (3.170.0) + aws-partitions (1.735.0) + aws-sdk-core (3.171.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) @@ -17,7 +17,7 @@ GEM aws-sdk-kms (1.63.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.119.1) + aws-sdk-s3 (1.119.2) aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) @@ -111,7 +111,7 @@ GEM fastlane-plugin-find_replace_string (0.1.0) fastlane-plugin-versioning_android (0.1.1) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.35.0) + google-apis-androidpublisher_v3 (0.37.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-core (0.11.0) addressable (~> 2.5, >= 2.5.1) @@ -142,7 +142,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.3.0) + googleauth (1.5.0) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 94afe8a32..a7d2c634b 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -1128,7 +1128,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 461; + CURRENT_PROJECT_VERSION = 463; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -1172,7 +1172,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 461; + CURRENT_PROJECT_VERSION = 463; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -1315,7 +1315,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 461; + CURRENT_PROJECT_VERSION = 463; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -1366,7 +1366,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 461; + CURRENT_PROJECT_VERSION = 463; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 666a99983..39a815ec2 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -37,7 +37,7 @@ CFBundleVersion - 461 + 463 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index adbdfec0e..9476142d5 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 2.1.0 CFBundleVersion - 461 + 463 UIAppFonts OpenSans-Bold.ttf diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index 584136eee..1b1873644 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 2.1.0 CFBundleVersion - 461 + 463 NSExtension NSExtensionPointIdentifier From f43e59960453cfaef2e1bbfd40232a2fb0c8a894 Mon Sep 17 00:00:00 2001 From: Frank Tang Date: Sun, 26 Mar 2023 17:09:17 +0000 Subject: [PATCH 12/14] Translated using Weblate (Chinese (Simplified)) Currently translated at 68.4% (690 of 1008 strings) Translation: mattermost-languages-shipped/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/zh_Hans/ --- assets/base/i18n/zh-CN.json | 574 ++++++++++++++++++------------------ 1 file changed, 293 insertions(+), 281 deletions(-) diff --git a/assets/base/i18n/zh-CN.json b/assets/base/i18n/zh-CN.json index 8b1d3e09f..95041926c 100644 --- a/assets/base/i18n/zh-CN.json +++ b/assets/base/i18n/zh-CN.json @@ -5,7 +5,7 @@ "about.enterpriseEditionSt": "位于防火墙后的现代通讯方式。", "about.hash": "编译哈希:", "about.hashee": "企业版编译哈希:", - "about.teamEditionLearn": "加入 Mattermost 社区: ", + "about.teamEditionLearn": "加入 Mattermost 社区:", "about.teamEditionSt": "所有团队的通讯一站式解决,随时随地可搜索和访问。", "about.teamEditiont0": "团队版", "about.teamEditiont1": "企业版", @@ -391,28 +391,28 @@ "account.logout": "注销", "user_status.dnd": "", "video.failed_description": "", - "invite_people_to_team.title": "", - "mobile.calls_unmute": "", + "invite_people_to_team.title": "参加{team} 团队", + "mobile.calls_unmute": "解除静音", "server.remove.alert_description": "", - "mobile.managed.jailbreak_no_debug_info": "", - "channel_info.notification.mention": "", + "mobile.managed.jailbreak_no_debug_info": "无效", + "channel_info.notification.mention": "提及", "notification_settings.pushNotification.mentions_only": "", - "channel_info.close_dm_channel": "", + "channel_info.close_dm_channel": "您确定关闭此对话吗?会话将从主屏幕中删除,但您可以再次打开它。", "notification_settings.email.crt.send": "", "snack.bar.unfavorite.channel": "", "post_info.guest": "", "notification_settings.send_notification.about": "", "settings.display": "", - "mobile.managed.jailbreak_no_reason": "", + "mobile.managed.jailbreak_no_reason": "无效", "mobile.write_storage_permission_denied_description": "", "post_info.auto_responder": "", - "default_skin_tone": "", + "default_skin_tone": "默认皮肤颜色", "invite_people_to_team.message": "", - "mobile.add_team.create_team": "", + "mobile.add_team.create_team": "创建一个新团队", "user_status.offline": "", "servers.login": "", "search_bar.search.placeholder": "", - "extension.no_servers.title": "", + "extension.no_servers.title": "没有连接到任何服务器", "screen.search.results.filter.images": "", "mobile.server_upgrade.description": "", "server.logout.alert_description": "", @@ -420,27 +420,27 @@ "mobile.manage_members.section_title_admins": "", "mobile.manage_members.remove_member": "", "mobile.manage_members.remove": "", - "mobile.manage_members.message": "", - "mobile.manage_members.member": "", - "mobile.manage_members.manage_member": "", - "mobile.manage_members.make_channel_member": "", + "mobile.manage_members.message": "您确定从此频道中删除选中的成员吗?", + "mobile.manage_members.member": "成员", + "mobile.manage_members.manage_member": "管理成员", + "mobile.manage_members.make_channel_member": "成为频道成员", "mobile.display_settings.crt": "", - "mobile.calls_recording_stop_no_permissions": "", - "mobile.calls_recording_start_no_permissions": "", - "mobile.calls_recording_start_in_progress": "", - "mobile.calls_react": "", - "mobile.calls_participant_rec_title": "", - "mobile.calls_okay": "", - "mobile.calls_lower_hand": "", - "mobile.calls_host_rec_error_title": "", - "mobile.calls_host_rec_error": "", + "mobile.calls_recording_stop_no_permissions": "您没有停止录制的权限。请联系通话的主持人停止录制。", + "mobile.calls_recording_start_no_permissions": "您没有开始录制的权限。请联系通话的主持人开始录制。", + "mobile.calls_recording_start_in_progress": "录制已经在进行中了。", + "mobile.calls_react": "反馈", + "mobile.calls_participant_rec_title": "录制正在进行中", + "mobile.calls_okay": "好的", + "mobile.calls_lower_hand": "放下手", + "mobile.calls_host_rec_error_title": "此录制有错误发生", + "mobile.calls_host_rec_error": "请尝试再次录制。您也可以联系系统管理员寻求帮助。", "thread.header.thread_in": "", "snack.bar.remove.user": "", "mobile.manage_members.section_title_members": "", - "mobile.manage_members.make_channel_admin": "", - "mobile.manage_members.done": "", - "mobile.manage_members.cancel": "", - "mobile.manage_members.admin": "", + "mobile.manage_members.make_channel_admin": "成为频道管理员", + "mobile.manage_members.done": "完成", + "mobile.manage_members.cancel": "取消", + "mobile.manage_members.admin": "管理者", "team_list.no_other_teams.title": "", "team_list.no_other_teams.description": "", "share_extension.max_resolution": "", @@ -449,55 +449,55 @@ "settings.advanced.delete_data": "", "settings_display.crt.label": "", "settings_display.crt.desc": "", - "mobile.calls_request_title": "", - "mobile.calls_request_message": "", - "channel_info.archive_description.cannot_view_archived": "", + "mobile.calls_request_title": "通话或能当前没有被开启", + "mobile.calls_request_message": "通话当前正在运行在测试模式下,只有系统管理可以开始录制。请直接联系系统管理员寻求帮助", + "channel_info.archive_description.cannot_view_archived": "这将从团队中归档此频道。归档频道可以根据需要被再次恢复。\n\n您确定要归档{term} {name} 吗?", "channel_info.archive_description.can_view_archived": "这将从团队中归档此频道。频道内容仍可由频道成员查看。\n\n您确定要归档{term} {name} 吗?", "mobile.session_expired": "", - "mobile.calls_not_available_title": "", - "invite.title.summary": "", - "invite.title": "", - "invite.summary.email_invite": "", - "gallery.opening": "", - "edit_server.saving": "", - "display_settings.crt.on": "", - "display_settings.crt.off": "", - "display_settings.crt": "", + "mobile.calls_not_available_title": "通话没有被启用", + "invite.title.summary": "邀请概要", + "invite.title": "邀请", + "invite.summary.email_invite": "邀请邮件已发送", + "gallery.opening": "打开中…", + "edit_server.saving": "保存中", + "display_settings.crt.on": "开", + "display_settings.crt.off": "关", + "display_settings.crt": "收起话题回复", "rate.subtitle": "", "invite.summary.some_not_sent": "", - "invite.summary.smtp_failure": "", - "invite.summary.report.sent": "", - "invite.summary.report.notSent": "", - "invite.summary.not_sent": "", - "invite.summary.error": "", - "invite.summary.done": "", - "invite.shareLink": "", - "invite.sendInvitationsTo": "", - "invite.send_invite": "", - "invite.send_error": "", - "invite.searchPlaceholder": "", - "invite.search.no_results": "", - "invite.search.email_invite": "", - "invite.members.user_is_guest": "", - "invite.members.already_member": "", + "invite.summary.smtp_failure": "SMTP没有在系统控制台里配置", + "invite.summary.report.sent": "{count} 成功的 {count, plural, one {邀请} other {邀请}}", + "invite.summary.report.notSent": "{count} {count, plural, one {邀请} other {邀请}} 没有发送", + "invite.summary.not_sent": "{notSentCount, plural, one {邀请没有} other {邀请没有}} 发送", + "invite.summary.error": "{invitationsCount, plural, one {邀请} other {邀请}} 不能发送成功", + "invite.summary.done": "完成", + "invite.shareLink": "分享链接", + "invite.sendInvitationsTo": "发送邀请到…", + "invite.send_invite": "发送", + "invite.send_error": "在尝试发送邀请里发成了一些错误。请检查您的网络连接后再次尝试。", + "invite.searchPlaceholder": "输入一个名称或者邮件地址…", + "invite.search.no_results": "无人匹配", + "invite.search.email_invite": "邀请", + "invite.members.user_is_guest": "联系您的管理员将此访客变成正式的团队成员", + "invite.members.already_member": "此人已经是团队成员", "terms_of_service.terms_declined.ok": "", - "mobile.calls_start_call_exists": "", - "mobile.calls_not_connected": "", + "mobile.calls_start_call_exists": "此频道已经有通话正在进行。", + "mobile.calls_not_connected": "您没有接入当前频道的通话。", "skintone_selector.tooltip.title": "", "skintone_selector.tooltip.description": "", "video.download_description": "", "user.edit_profile.profile_photo.change_photo": "", "user_status.title": "", "user_status.online": "", - "join_team.error.title": "", - "join_team.error.group_error": "", + "join_team.error.title": "加入团队在出错", + "join_team.error.group_error": "你需要是一个相关组的成员来加入团队。", "server.logout.alert_title": "", "screens.channel_info.gm": "", "permalink.error.cancel": "", - "mobile.camera_type.title": "", - "mobile.calls_end_msg_dm": "", - "custom_status.set_status": "", - "connection_banner.connecting": "", + "mobile.camera_type.title": "相机选项", + "mobile.calls_end_msg_dm": "您确定结束和{displayName}的通话吗?", + "custom_status.set_status": "自定义状态", + "connection_banner.connecting": "正在连接…", "user.settings.general.field_handled_externally": "", "thread.loadingReplies": "", "share_extension.multiple_label": "", @@ -509,61 +509,61 @@ "onboarding.integrations_description": "", "onboarding.calls": "", "notification.not_channel_member": "", - "mobile.calls_open_channel": "", - "mobile.calls_call_thread": "", + "mobile.calls_open_channel": "打开频道", + "mobile.calls_call_thread": "通话主题", "share_extension.message": "", "settings.advanced.delete": "", - "mobile.calls_participant_limit_title_GA": "", + "mobile.calls_participant_limit_title_GA": "此通话达到容量上限", "mobile.calls_limit_msg_GA": "", - "general_settings.report_problem": "", + "general_settings.report_problem": "报告一个问题", "settings.advanced.cancel": "", "mobile.open_dm.error": "", - "mobile.create_direct_message.start": "", - "mobile.create_direct_message.max_limit_reached": "", + "mobile.create_direct_message.start": "开始会话", + "mobile.create_direct_message.max_limit_reached": "聊天最大支持 {maxCount} 个成员", "share_extension.upload_disabled": "", "share_extension.share_screen.title": "", "share_extension.servers_screen.title": "", "share_extension.server_label": "", "notification_settings.threads_mentions": "", "notification_settings.email.emailHelp2": "", - "mobile.custom_status.clear_after.title": "", - "mobile.custom_list.no_results": "", - "mobile.create_direct_message.you": "", - "mobile.calls_stop_recording": "", - "mobile.calls_record": "", - "mobile.calls_rec": "", - "mobile.calls_host_rec_title": "", - "mobile.calls_host_rec_stopped_title": "", + "mobile.custom_status.clear_after.title": "此时间后消除自定义状态", + "mobile.custom_list.no_results": "结果为空", + "mobile.create_direct_message.you": "@{username} - 你", + "mobile.calls_stop_recording": "停止录制", + "mobile.calls_record": "录制", + "mobile.calls_rec": "反馈", + "mobile.calls_host_rec_title": "您正在录制中", + "mobile.calls_host_rec_stopped_title": "录制已经停止。处理中…", "mobile.calls_host_rec_stopped": "", - "mobile.calls_host_rec": "", - "mobile.calls_dismiss": "", - "mobile.announcement_banner.title": "", - "login.signingIn": "", - "login.signIn": "", - "login.invalid_credentials": "", - "connection_banner.not_reachable": "", - "connection_banner.not_connected": "", - "connection_banner.connected": "", + "mobile.calls_host_rec": "您正在录制此会议。让所有人知道这个会议正在录制可能是一个不错的主意。", + "mobile.calls_dismiss": "驳回", + "mobile.announcement_banner.title": "公告", + "login.signingIn": "登录中", + "login.signIn": "登录", + "login.invalid_credentials": "邮件和密码的组合不正确", + "connection_banner.not_reachable": "服务器无法连接", + "connection_banner.not_connected": "无互联网连接", + "connection_banner.connected": "连接已恢复", "share_extension.file_limit.multiple": "", "share_extension.count_limit": "", "settings.advanced.delete_message.confirmation": "", "notification_settings.mentions.keywords_mention": "", "notification_settings.auto_responder.message": "", - "mobile.edit_post.delete_title": "", - "extension.no_servers.description": "", - "extension.no_memberships.title": "", - "extension.no_memberships.description": "", + "mobile.edit_post.delete_title": "确认删除", + "extension.no_servers.description": "为了分享内容,你将需要先登入一个Mattermost服务器。", + "extension.no_memberships.title": "还不是任何团队的成员", + "extension.no_memberships.description": "为了分享内容,你需要是一个Mattermost服务器上的团队成员。", "settings.link.error.title": "", "onboarding.integrations": "", "onboarding.calls_description": "", "mobile.calls_participant_rec": "", - "mobile.calls_limit_msg": "", - "mobile.calls_host": "", - "intro.welcome": "", - "display_settings.clock.standard": "", - "channel_info.custom_status": "", - "channel_info.copy_link": "", - "channel_info.copied": "", + "mobile.calls_limit_msg": "每个通话最的参加者人数为 {maxParticipants}。联系您的系统管理员增加人数上限。", + "mobile.calls_host": "主持人", + "intro.welcome": "欢迎来到 {displayName} 频道。", + "display_settings.clock.standard": "12小时", + "channel_info.custom_status": "定制状态:", + "channel_info.copy_link": "复制链接", + "channel_info.copied": "已复制", "server_upgrade.learn_more": "", "screen.search.results.file_options.download": "", "screen.search.results.file_options.copy_link": "", @@ -583,7 +583,7 @@ "mobile.post_info.unsave": "", "mobile.onboarding.sign_in_to_get_started": "", "mobile.onboarding.sign_in": "", - "mobile.onboarding.next": "", + "mobile.onboarding.next": "下一个", "snack.bar.unmute.channel": "", "snack.bar.message.copied": "", "snack.bar.favorited.channel": "", @@ -621,52 +621,52 @@ "mobile.post_pre_header.pinned_saved": "", "notification_settings.email.send": "", "mobile.search.show_less": "", - "gallery.image_saved": "", + "gallery.image_saved": "图片已保存", "screens.channel_info": "", "notification_settings.threads_start": "", "permalink.error.private_channel_and_team.text": "", - "mobile.calls_error_message": "", - "join_team.error.message": "", - "channel_info.members": "", - "mobile.oauth.failed_to_open_link_no_browser": "", + "mobile.calls_error_message": "错误:{error}", + "join_team.error.message": "在加入团队在出现了一个错误", + "channel_info.members": "成员们", + "mobile.oauth.failed_to_open_link_no_browser": "此链接打开失败。请验证浏览器已经在设备上安装。", "post.reactions.title": "", "mobile.search.modifier.phrases": "", - "channel_info.mobile_notifications": "", + "channel_info.mobile_notifications": "移动设备推送", "settings.notice_text": "", "mobile.search.modifier.exclude": "", "share_extension.channel_label": "", - "emoji_picker.recent": "", + "emoji_picker.recent": "最近使用过", "server.tutorial.swipe": "", - "display_settings.clock.military": "", - "display_settings.tz.auto": "", - "channel_info.mention": "", + "display_settings.clock.military": "24小时", + "display_settings.tz.auto": "自动", + "channel_info.mention": "提及", "mobile.search.modifier.in": "", "mobile.request.invalid_request_method": "", - "channel_info.leave_public_channel": "", + "channel_info.leave_public_channel": "您确定离开公开频道{displayName}?您可以重新再次加入。", "screen.search.results.filter.audio": "", - "mobile.manage_members.manage": "", - "edit_server.title": "", - "invite.summary.sent": "", - "mobile.calls_recording_stop_none_in_progress": "", + "mobile.manage_members.manage": "管理", + "edit_server.title": "编辑服务器名称", + "invite.summary.sent": "您的{sentCount, plural, one {邀请已经} other {邀请已经}} 被发送", + "mobile.calls_recording_stop_none_in_progress": "没有录制正在进行。", "more_messages.text": "", "mobile.server_identifier.exists": "", - "download.error": "", + "download.error": "无法下载文件。请稍后重试", "settings.about.server.version.value": "", - "mobile.calls_call_screen": "", + "mobile.calls_call_screen": "通话", "user.edit_profile.email.web_client": "", "share_extension.channel_error": "", "mobile.search.team.select": "", - "channel_info.muted": "", + "channel_info.muted": "静音", "post_priority.label.important": "", "saved_messages.empty.paragraph": "", "screen.search.results.filter.presentations": "", "snack.bar.link.copied": "", "screen.search.results.file_options.open_in_channel": "", - "invite.summary.member_invite": "", + "invite.summary.member_invite": "被邀请为{teamDisplayName}的团队成员", "user_status.away": "", "settings_display.timezone.automatically": "", "screen.search.modifier.header": "", - "invite.summary.try_again": "", + "invite.summary.try_again": "重试", "mobile.manage_members.change_role.error": "", "notification_settings.threads_start_participate": "", "threads.end_of_list.subtitle": "", @@ -704,55 +704,55 @@ "mobile.screen.your_profile": "", "mobile.screen.settings": "", "mobile.post_info.save": "", - "mobile.oauth.switch_to_browser.error_title": "", - "mobile.oauth.something_wrong.okButton": "", - "mobile.login_options.separator_text": "", - "mobile.login_options.select_option": "", - "mobile.login_options.openid": "", - "mobile.login_options.office365": "", + "mobile.oauth.switch_to_browser.error_title": "登录错误", + "mobile.oauth.something_wrong.okButton": "好的", + "mobile.login_options.separator_text": "或者 登录以", + "mobile.login_options.select_option": "请在下方选择一个登录选项。", + "mobile.login_options.openid": "Open ID", + "mobile.login_options.office365": "Office 365", "mobile.login_options.none": "", - "mobile.login_options.heading": "", - "mobile.login_options.google": "", - "mobile.login_options.gitlab": "", - "mobile.login_options.enter_credentials": "", - "mobile.login_options.cant_heading": "", + "mobile.login_options.heading": "登录您的账户", + "mobile.login_options.google": "Google", + "mobile.login_options.gitlab": "GitLab", + "mobile.login_options.enter_credentials": "请在下方输入您登录的信息。", + "mobile.login_options.cant_heading": "无法登录", "mobile.ios.photos_permission_denied_description": "", - "mobile.edit_post.error": "", + "mobile.edit_post.error": "编辑此消息时发生错误。请再次尝试。", "mobile.edit_post.delete_question": "", - "mobile.calls_leave_call": "", - "mobile.calls_leave": "", - "mobile.calls_lasted": "", - "mobile.calls_join_call": "", - "mobile.calls_error_title": "", - "mobile.calls_end_msg_channel_default": "", + "mobile.calls_leave_call": "离开通话", + "mobile.calls_leave": "离开", + "mobile.calls_lasted": "通话时间{duration}", + "mobile.calls_join_call": "加入通话", + "mobile.calls_error_title": "错误", + "mobile.calls_end_msg_channel_default": "您确定结束通话?", "mobile.calls_end_msg_channel": "", - "mobile.calls_end_call_title": "", - "mobile.calls_call_ended": "", + "mobile.calls_end_call_title": "终止通话", + "mobile.calls_call_ended": "通过已结束", "mobile.android.photos_permission_denied_description": "", - "mobile.add_team.join_team": "", - "mentions.empty.title": "", + "mobile.add_team.join_team": "加入另外的团队", + "mentions.empty.title": "目前没有提及", "mentions.empty.paragraph": "", - "markdown.latex.error": "", - "login_mfa.token": "", - "login_mfa.enterToken": "", - "load_teams_error.title": "", - "load_teams_error.message": "", - "load_channels_error.title": "", - "load_channels_error.message": "", - "load_categories_error.title": "", - "load_categories_error.message": "", - "intro.welcome.public": "", - "intro.welcome.private": "", - "intro.townsquare": "", - "intro.public_channel": "", - "intro.private_channel": "", - "intro.group_message": "", - "intro.direct_message": "", - "intro.created_by": "", - "intro.channel_info": "", - "intro.add_people": "", - "interactive_dialog.submit": "", - "integration_selector.multiselect.submit": "", + "markdown.latex.error": "Latex渲染错误", + "login_mfa.token": "输入MFA令牌", + "login_mfa.enterToken": "完成此签名流程,请从您移动设备上的认证应用上输入代码。", + "load_teams_error.title": "无法载入{serverName}", + "load_teams_error.message": "在这个服务上有内容载入错误。", + "load_channels_error.title": "无法载入{teamDisplayName}", + "load_channels_error.message": "在这个服务上有内容载入错误。", + "load_categories_error.title": "不能在{serverName}载入类型", + "load_categories_error.message": "在这个服务上有内容载入错误。", + "intro.welcome.public": "加入更多的团队成员到频道里或者从下面开始一个对话。", + "intro.welcome.private": "此非公开频道的消息只有被邀请的成员才能看到。", + "intro.townsquare": "欢迎 {name}。每个人加入团队后自动会成为此频道的成员。", + "intro.public_channel": "公开频道", + "intro.private_channel": "非公开频道", + "intro.group_message": "这是您与此群组开始的对话。消息和文件保存在这里的不会对任何群组外的人显示。", + "intro.direct_message": "这是您与{teammate}开始的对话。消息和文件保存在这里的不会对其他任何人显示。", + "intro.created_by": "由{creator}在{date}创建。", + "intro.channel_info": "信息", + "intro.add_people": "添加新人", + "interactive_dialog.submit": "发送", + "integration_selector.multiselect.submit": "完成", "screen.search.results.filter.spreadsheets": "", "your.servers": "", "video.download": "", @@ -782,9 +782,9 @@ "rate.dont_ask_again": "", "rate.button.yes": "", "rate.button.needs_work": "", - "mobile.oauth.switch_to_browser.title": "", - "mobile.integration_selector.loading_options": "", - "mobile.integration_selector.loading_channels": "", + "mobile.oauth.switch_to_browser.title": "切换中…", + "mobile.integration_selector.loading_options": "正在载入选项…", + "mobile.integration_selector.loading_channels": "正在载入频道…", "post_priority.picker.title": "", "post_priority.picker.label.urgent": "", "post_priority.picker.label.standard": "", @@ -797,63 +797,63 @@ "notification_settings.auto_responder": "", "mobile.server_url.deeplink.emm.denied": "", "mobile.reset_status.alert_ok": "", - "mobile.oauth.switch_to_browser": "", - "mobile.calls_you": "", - "mobile.calls_viewing_screen": "", - "mobile.calls_more": "", - "mobile.calls_ended_at": "", - "mobile.calls_mic_error": "", + "mobile.oauth.switch_to_browser": "您正在被切换到登录服务提供商", + "mobile.calls_you": "(您)", + "mobile.calls_viewing_screen": "您正在观看{name}的屏幕", + "mobile.calls_more": "更多", + "mobile.calls_ended_at": "结束于", + "mobile.calls_mic_error": "打开设置允许Mattermost访问您的麦克风权限用来加入。", "notification_settings.push_notification": "", "notification_settings.ooo_auto_responder": "", "notification_settings.mobile.trigger_push": "", "notification_settings.mobile.online": "", "notification_settings.email": "", - "gallery.copy_link.failed": "", - "find_channels.title": "", - "find_channels.open_dm": "", - "find_channels.new_channel": "", - "find_channels.directory": "", + "gallery.copy_link.failed": "复制到剪切板失败", + "find_channels.title": "查找频道", + "find_channels.open_dm": "打开一个聊天", + "find_channels.new_channel": "新建频道", + "find_channels.directory": "文件夹", "mobile.display_settings.timezone": "", - "mobile.display_settings.clockDisplay": "", + "mobile.display_settings.clockDisplay": "时钟显示", "mobile.direct_message.error": "", - "mobile.create_channel.title": "", - "mobile.components.select_server_view.msg_welcome": "", - "mobile.components.select_server_view.msg_description": "", - "mobile.components.select_server_view.msg_connect": "", - "mobile.components.select_server_view.displayName": "", - "mobile.components.select_server_view.displayHelp": "", - "mobile.components.select_server_view.connecting": "", - "mobile.channel_list.unreads": "", - "mobile.channel_list.recent": "", + "mobile.create_channel.title": "新建频道", + "mobile.components.select_server_view.msg_welcome": "欢迎", + "mobile.components.select_server_view.msg_description": "服务器是您的团队通讯的中心,它有唯一的URL", + "mobile.components.select_server_view.msg_connect": "让我们连接一个服务器", + "mobile.components.select_server_view.displayName": "显示名", + "mobile.components.select_server_view.displayHelp": "为您的服务器选择一个显示名", + "mobile.components.select_server_view.connecting": "连接中", + "mobile.channel_list.unreads": "未读", + "mobile.channel_list.recent": "最近", "mobile.camera_photo_permission_denied_description": "", - "mobile.calls_start_call": "", - "mobile.calls_speaker": "", - "mobile.calls_see_logs": "", - "mobile.calls_raise_hand": "", - "mobile.calls_not_available_option": "", - "mobile.calls_not_available_msg": "", - "mobile.calls_noone_talking": "", - "mobile.calls_name_started_call": "", - "mobile.calls_name_is_talking": "", - "mobile.calls_mute": "", - "gallery.unsupported": "", - "gallery.save_failed": "", - "gallery.preparing": "", - "display_settings.tz.manual": "", - "display_settings.timezone": "", - "display_settings.theme": "", - "display_settings.clockDisplay": "", - "create_post.thread_reply": "", - "channel_modal.makePrivate.label": "", - "channel_modal.makePrivate.description": "", - "channel_info.unarchive_failed": "", - "channel_info.unarchive_description": "", - "channel_info.unarchive": "", - "channel_info.set_header": "", - "channel_info.send_mesasge": "", - "channel_info.convert_private_title": "", - "channel_info.convert_private_success": "", - "channel_info.convert_private_description": "", + "mobile.calls_start_call": "开始通话", + "mobile.calls_speaker": "喇叭", + "mobile.calls_see_logs": "查是服务器日志", + "mobile.calls_raise_hand": "举手", + "mobile.calls_not_available_option": "(当前无效)", + "mobile.calls_not_available_msg": "请联系您的系统管理员打开此功能。", + "mobile.calls_noone_talking": "没有人发言", + "mobile.calls_name_started_call": "{name}开启了通话", + "mobile.calls_name_is_talking": "{name}正在说话", + "mobile.calls_mute": "静音", + "gallery.unsupported": "这个文件类型不支持预览。尝试下载或者分享到另外一个应用打开。", + "gallery.save_failed": "无法保存文件", + "gallery.preparing": "准备中…", + "display_settings.tz.manual": "手动", + "display_settings.timezone": "时区", + "display_settings.theme": "主题", + "display_settings.clockDisplay": "时钟显示", + "create_post.thread_reply": "回复此话题…", + "channel_modal.makePrivate.label": "设为非公开", + "channel_modal.makePrivate.description": "当一个频道被设置成非公开,只有被邀请的团队成员能够访问和参与频道", + "channel_info.unarchive_failed": "在尝试取消归档频道{displayName}时发生错误", + "channel_info.unarchive_description": "您确定要取消归档 {term} {name}?", + "channel_info.unarchive": "取消归档频道", + "channel_info.set_header": "设置标题", + "channel_info.send_mesasge": "发送消息", + "channel_info.convert_private_title": "转变 {displayName} 为非公开频道?", + "channel_info.convert_private_success": "{displayName} 现在为非公开频道。", + "channel_info.convert_private_description": "当你转变{displayName}成一个非公开频道,历史和关联关系将会被保留。公共分享文件的链接所有人都可以继续访问。私有频道只能通过邀请加入。\n\n此更改是永久的而且不能撤销。\n\n您确定将{displayName}转变成一个非公开频道吗?", "select_team.description": "", "screens.channel_edit": "", "screen.search.title": "", @@ -870,54 +870,54 @@ "notification_settings.push_threads.replies": "", "snack.bar.undo": "", "settings.save": "", - "edit_server.save": "", - "edit_server.display_help": "", - "edit_server.description": "", - "custom_status.suggestions.title": "", - "custom_status.suggestions.recent_title": "", - "create_direct_message.title": "", - "channel_modal.purposeEx": "", - "channel_modal.nameEx": "", - "channel_modal.headerHelp": "", - "channel_modal.headerEx": "", - "channel_list.find_channels": "", - "channel_list.favorites_category": "", - "channel_list.dms_category": "", - "channel_list.channels_category": "", - "channel_intro.createdOn": "", - "channel_intro.createdBy": "", - "channel_info.unarchive_title": "", - "channel_info.send_a_mesasge": "", - "channel_info.public_channel": "", - "channel_info.private_channel": "", - "channel_info.position": "", - "channel_info.pinned_messages": "", - "channel_info.notification.none": "", - "channel_info.notification.default": "", - "channel_info.notification.all": "", - "channel_info.nickname": "", - "channel_info.local_time": "", - "channel_info.leave_private_channel": "", - "channel_info.leave_channel": "", - "channel_info.leave": "", - "channel_info.ignore_mentions": "", - "channel_info.favorited": "", - "channel_info.favorite": "", - "channel_info.error_close": "", - "channel_info.edit_header": "", - "channel_info.convert_private": "", - "channel_info.convert_failed": "", - "channel_info.close_gm_channel": "", - "channel_info.close_gm": "", - "channel_info.close_dm": "", - "channel_info.close": "", - "channel_info.archive_title": "", - "channel_info.archive_failed": "", + "edit_server.save": "保存", + "edit_server.display_help": "服务器:{url}", + "edit_server.description": "为此服务器指定一个显示名称", + "custom_status.suggestions.title": "建议", + "custom_status.suggestions.recent_title": "最近", + "create_direct_message.title": "创建聊天", + "channel_modal.purposeEx": "用来提交bug和改进的频道", + "channel_modal.nameEx": "Bugs, 市场", + "channel_modal.headerHelp": "设定在频道标题里在频道旁边的文字。举例,输入常见链接 [链接标题](http://example.com)。", + "channel_modal.headerEx": "使用Markdown格式化标题", + "channel_list.find_channels": "寻找频道...", + "channel_list.favorites_category": "收藏", + "channel_list.dms_category": "聊天", + "channel_list.channels_category": "频道", + "channel_intro.createdOn": "{date}创建", + "channel_intro.createdBy": "由{user}在{date}创建", + "channel_info.unarchive_title": "取消归档 {term}", + "channel_info.send_a_mesasge": "发送消息", + "channel_info.public_channel": "公共频道", + "channel_info.private_channel": "私有频道", + "channel_info.position": "职位", + "channel_info.pinned_messages": "标注消息", + "channel_info.notification.none": "从不", + "channel_info.notification.default": "默认", + "channel_info.notification.all": "全部", + "channel_info.nickname": "昵称", + "channel_info.local_time": "两地时间", + "channel_info.leave_private_channel": "您确定离开非公开频道{displayName}?您将不能加入频道除非你被再次邀请。", + "channel_info.leave_channel": "离开频道", + "channel_info.leave": "离开", + "channel_info.ignore_mentions": "忽略 @channel、@here 以及 @all", + "channel_info.favorited": "已收藏", + "channel_info.favorite": "收藏", + "channel_info.error_close": "关闭", + "channel_info.edit_header": "编辑标题", + "channel_info.convert_private": "转成非公开频道", + "channel_info.convert_failed": "我们无法将{displayName} 转变为私有频道。", + "channel_info.close_gm_channel": "您确定关闭此对话吗?会话将从主屏幕中删除,但您可以再次打开它。", + "channel_info.close_gm": "关闭群聊", + "channel_info.close_dm": "关闭对话", + "channel_info.close": "关闭", + "channel_info.archive_title": "归档{term}", + "channel_info.archive_failed": "在试图归档频道{displayName}时发生错误", "channel_info.archive": "归档频道", "channel_info.alertYes": "是", "channel_info.alertNo": "不", "channel_info.alert_retry": "重试", - "channel_header.member_count": "", + "channel_header.member_count": "{count, plural, one {# 成员} other {# 成员}}", "channel_header.info": "频道信息", "channel_header.directchannel.you": "{displayName} (您)", "browse_channels.title": "浏览频道", @@ -961,36 +961,48 @@ "notification_settings.mentions": "", "notification_settings.mention.reply": "", "notification_settings.email.crt.emailInfo": "", - "mobile.calls_limit_reached": "", - "mobile.calls_end_permission_title": "", - "mobile.calls_end_permission_msg": "", - "mobile.calls_enable": "", - "mobile.calls_disable": "", - "mobile.calls_current_call": "", + "mobile.calls_limit_reached": "达到参加者上限", + "mobile.calls_end_permission_title": "错误", + "mobile.calls_end_permission_msg": "您没有权限结束通话。请联系通话创建者结束此通话。", + "mobile.calls_enable": "启动通话", + "mobile.calls_disable": "无效的通话", + "mobile.calls_current_call": "目前的通话", "user.edit_profile.email.auth_service": "", "settings.notice_mobile_link": "", "settings.advanced_settings": "", "settings.about.database": "", "mobile.post_pre_header.saved": "", "mobile.participants.header": "", - "mobile.no_results.spelling": "", - "mobile.no_results_with_term.messages": "", - "mobile.no_results_with_term.files": "", - "mobile.no_results_with_term": "", - "mobile.login_options.saml": "", - "mobile.leave_and_join_title": "", + "mobile.no_results.spelling": "检查拼写或者尝试另外的搜索。", + "mobile.no_results_with_term.messages": "\"{term}\"没有匹配的发现", + "mobile.no_results_with_term.files": "\"{term}\"没有匹配的文件", + "mobile.no_results_with_term": "“{term}” 没有相关结果", + "mobile.login_options.saml": "SAML", + "mobile.leave_and_join_title": "您确定切换到另一个通话吗?", "mobile.leave_and_join_message": "", - "mobile.leave_and_join_confirmation": "", - "mobile.join_channel.error": "", - "mobile.create_post.read_only": "", - "mobile.calls_ok": "", - "home.header.plus_menu": "", - "global_threads.options.title": "", - "general_settings.notifications": "", - "general_settings.help": "", - "general_settings.display": "", - "general_settings.advanced_settings": "", - "general_settings.about": "", - "gallery.video_saved": "", - "gallery.downloading": "" + "mobile.leave_and_join_confirmation": "离开 & 加入", + "mobile.join_channel.error": "我们不会参加频道{displayName}。", + "mobile.create_post.read_only": "此频道只读。", + "mobile.calls_ok": "好的", + "home.header.plus_menu": "选项", + "global_threads.options.title": "话题操作", + "general_settings.notifications": "通知", + "general_settings.help": "帮助", + "general_settings.display": "显示", + "general_settings.advanced_settings": "高级设定", + "general_settings.about": "关于{appTitle}", + "gallery.video_saved": "视频已保存", + "gallery.downloading": "下载中…", + "channel_notification_preferences.unmute_content": "取消频道静音", + "channel_notification_preferences.thread_replies": "话题回复", + "channel_notification_preferences.reset_default": "重设为默认", + "channel_notification_preferences.notify_about": "通知我当…", + "channel_notification_preferences.notification.thread_replies": "此频道我关注的话题有回复时通知我", + "channel_notification_preferences.notification.none": "无", + "channel_notification_preferences.notification.mention": "仅提及,聊天", + "channel_notification_preferences.notification.all": "所有新消息", + "channel_notification_preferences.muted_title": "此频道被已静音", + "channel_notification_preferences.muted_content": "您可以改变通知设置,但是在频道被取消静音前你将不会接受到通知。", + "channel_notification_preferences.default": "(默认)", + "invite.summary.back": "向后" } From 4b04df83f9291b5d0a20040d497ae456d6f266bf Mon Sep 17 00:00:00 2001 From: Tanmay Vardhaman Thole <72058456+tanmaythole@users.noreply.github.com> Date: Tue, 28 Mar 2023 12:47:22 +0530 Subject: [PATCH 13/14] Tapping on a custom status in the message list opens the users profile card (#7199) * Tapping on a custom status in the message list opens the users profile card * Tapping on a custom status in the message list opens the users profile card * code refactored and reuse functions in headerDisplayName * fix import --------- Co-authored-by: Elias Nahum --- .../post/header/display_name/index.tsx | 38 +++++++++++++++---- .../post_list/post/header/header.tsx | 18 ++------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/app/components/post_list/post/header/display_name/index.tsx b/app/components/post_list/post/header/display_name/index.tsx index cef84de99..fc39eeaac 100644 --- a/app/components/post_list/post/header/display_name/index.tsx +++ b/app/components/post_list/post/header/display_name/index.tsx @@ -5,6 +5,7 @@ import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; import {Keyboard, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native'; +import CustomStatusEmoji from '@components/custom_status/custom_status_emoji'; import FormattedText from '@components/formatted_text'; import {Screens} from '@constants'; import {openAsBottomSheet} from '@screens/navigation'; @@ -23,6 +24,8 @@ type HeaderDisplayNameProps = { userIconOverride?: string; userId: string; usernameOverride?: string; + showCustomStatusEmoji: boolean; + customStatus: UserCustomStatus; } const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { @@ -30,11 +33,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { displayName: { color: theme.centerChannelColor, flexGrow: 1, + marginRight: 5, ...typography('Body', 200, 'SemiBold'), }, + displayNameCustomEmojiWidth: { + maxWidth: '90%', + }, displayNameContainer: { maxWidth: '60%', - marginRight: 5, + flexDirection: 'row', + alignItems: 'center', }, displayNameContainerBotReplyWidth: { maxWidth: '50%', @@ -45,7 +53,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { displayNameContainerLandscapeBotReplyWidth: { maxWidth: '70%', }, - + customStatusEmoji: { + color: theme.centerChannelColor, + marginRight: 4, + }, }; }); @@ -54,6 +65,7 @@ const HeaderDisplayName = ({ location, rootPostAuthor, shouldRenderReplyButton, theme, userIconOverride, userId, usernameOverride, + showCustomStatusEmoji, customStatus, }: HeaderDisplayNameProps) => { const dimensions = useWindowDimensions(); const intl = useIntl(); @@ -85,12 +97,17 @@ const HeaderDisplayName = ({ }; const displayNameWidth = calcNameWidth(); - const displayNameStyle = [style.displayNameContainer, displayNameWidth]; + const displayNameContainerStyle = [style.displayNameContainer, displayNameWidth]; + + const displayNameStyle = showCustomStatusEmoji ? style.displayNameCustomEmojiWidth : null; if (displayName) { return ( - - + + {displayName} - - + + {showCustomStatusEmoji && ( + + )} + ); } diff --git a/app/components/post_list/post/header/header.tsx b/app/components/post_list/post/header/header.tsx index 7b9eaab89..1e54536f5 100644 --- a/app/components/post_list/post/header/header.tsx +++ b/app/components/post_list/post/header/header.tsx @@ -4,7 +4,6 @@ import React from 'react'; import {View} from 'react-native'; -import CustomStatusEmoji from '@components/custom_status/custom_status_emoji'; import FormattedTime from '@components/formatted_time'; import PostPriorityLabel from '@components/post_priority/post_priority_label'; import {CHANNEL, THREAD} from '@constants/screens'; @@ -67,11 +66,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { alignSelf: 'center', marginLeft: 6, }, - customStatusEmoji: { - color: theme.centerChannelColor, - marginRight: 4, - marginTop: 2, - }, }; }); @@ -89,11 +83,10 @@ const Header = (props: HeaderProps) => { const displayName = postUserDisplayName(post, author, teammateNameDisplay, enablePostUsernameOverride); const rootAuthorDisplayName = rootPostAuthor ? displayUsername(rootPostAuthor, currentUser.locale, teammateNameDisplay, true) : undefined; const customStatus = getUserCustomStatus(author); - const customStatusExpired = isCustomStatusExpired(author); const showCustomStatusEmoji = Boolean( isCustomStatusEnabled && displayName && customStatus && !(isSystemPost || author?.isBot || isAutoResponse || isWebHook), - ); + ) && !isCustomStatusExpired(author) && Boolean(customStatus?.emoji); return ( <> @@ -110,14 +103,9 @@ const Header = (props: HeaderProps) => { userIconOverride={post.props?.override_icon_url} userId={post.userId} usernameOverride={post.props?.override_username} + showCustomStatusEmoji={showCustomStatusEmoji} + customStatus={customStatus!} /> - {showCustomStatusEmoji && !customStatusExpired && Boolean(customStatus?.emoji) && ( - - )} {(!isSystemPost || isAutoResponse) && Date: Tue, 28 Mar 2023 17:00:11 -0400 Subject: [PATCH 14/14] MM-51227 - Calls: Landscape mode optimizations for screensharing (#7226) * allow landscape for ios; optimize screensharing view; fix reactions * UX changes * UX changes - top bar --- .../calls/components/reaction_bar.tsx | 20 ++- .../calls/screens/call_screen/call_screen.tsx | 125 ++++++++++++++---- 2 files changed, 111 insertions(+), 34 deletions(-) diff --git a/app/products/calls/components/reaction_bar.tsx b/app/products/calls/components/reaction_bar.tsx index d4046262b..90b6c2acb 100644 --- a/app/products/calls/components/reaction_bar.tsx +++ b/app/products/calls/components/reaction_bar.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useCallback} from 'react'; -import {Pressable, StyleSheet, View} from 'react-native'; +import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native'; import {raiseHand, unraiseHand} from '@calls/actions'; import {sendReaction} from '@calls/actions/calls'; @@ -16,12 +16,15 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'space-between', - backgroundColor: 'rgba(255,255,255,0.16)', width: '100%', height: 64, paddingLeft: 16, paddingRight: 16, }, + containerInLandscape: { + paddingBottom: 6, + justifyContent: 'center', + }, button: { display: 'flex', flexDirection: 'row', @@ -33,6 +36,10 @@ const styles = StyleSheet.create({ paddingLeft: 10, paddingRight: 10, }, + buttonLandscape: { + marginRight: 12, + marginLeft: 12, + }, buttonPressed: { backgroundColor: 'rgba(245, 171, 0, 0.24)', }, @@ -54,6 +61,9 @@ interface Props { } const ReactionBar = ({raisedHand}: Props) => { + const {width, height} = useWindowDimensions(); + const isLandscape = width > height; + const LowerHandText = ( { }, [raisedHand]); return ( - + { sendReaction({name, unified})} /> )) diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx index b74f6054e..ddb035b22 100644 --- a/app/products/calls/screens/call_screen/call_screen.tsx +++ b/app/products/calls/screens/call_screen/call_screen.tsx @@ -6,6 +6,7 @@ import {useIntl} from 'react-intl'; import { DeviceEventEmitter, Keyboard, + NativeModules, Platform, Pressable, SafeAreaView, @@ -42,6 +43,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import {useIsTablet} from '@hooks/device'; import WebsocketManager from '@managers/websocket_manager'; import { + allOrientations, bottomSheet, dismissAllModalsAndPopToScreen, dismissBottomSheet, @@ -50,6 +52,7 @@ import { setScreensOrientation, } from '@screens/navigation'; import NavigationStore from '@store/navigation_store'; +import {freezeOtherScreens} from '@utils/gallery'; import {bottomSheetSnapPoint} from '@utils/helpers'; import {mergeNavigationOptions} from '@utils/navigation'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -107,8 +110,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ position: 'absolute', top: 0, backgroundColor: 'rgba(0,0,0,0.64)', - height: 64, - padding: 0, + height: 52, + paddingTop: 0, }, headerLandscapeNoControls: { top: -1000, @@ -160,11 +163,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ }), }, buttonsLandscape: { - height: 128, + height: 110, position: 'absolute', backgroundColor: 'rgba(0,0,0,0.64)', bottom: 0, }, + buttonsLandscapeWithReactions: { + height: 174, + }, buttonsLandscapeNoControls: { bottom: 1000, }, @@ -173,6 +179,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ alignItems: 'center', flex: 1, }, + buttonLandscape: { + flex: 0, + }, mute: { flexDirection: 'column', alignItems: 'center', @@ -198,7 +207,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ otherButtons: { flexDirection: 'row', alignItems: 'center', - alignContent: 'space-between', + }, + otherButtonsLandscape: { + justifyContent: 'center', }, collapseIcon: { color: theme.sidebarText, @@ -208,6 +219,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ borderRadius: 4, overflow: 'hidden', }, + collapseIconLandscape: { + margin: 10, + padding: 0, + backgroundColor: 'transparent', + borderRadius: 0, + }, muteIcon: { color: theme.sidebarText, }, @@ -230,6 +247,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ margin: 10, overflow: 'hidden', }, + buttonIconLandscape: { + borderRadius: 26, + paddingTop: 14, + paddingRight: 16, + paddingBottom: 16, + paddingLeft: 14, + width: 52, + height: 52, + marginLeft: 12, + marginRight: 12, + }, hangUpIcon: { backgroundColor: Preferences.THEMES.denim.dndIndicator, }, @@ -272,7 +300,6 @@ const CallScreen = ({ const style = getStyleSheet(theme); const isLandscape = width > height; - const showControls = !isLandscape || showControlsInLandscape; const myParticipant = currentCall?.participants[currentCall.myUserId]; const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed; @@ -285,11 +312,24 @@ const CallScreen = ({ mergeNavigationOptions('Call', { layout: { componentBackgroundColor: 'black', + orientation: allOrientations, }, topBar: { visible: false, }, }); + if (Platform.OS === 'ios') { + NativeModules.SplitView.unlockOrientation(); + } + + return () => { + setScreensOrientation(isTablet); + if (Platform.OS === 'ios' && !isTablet) { + // We need both the navigation & the module + NativeModules.SplitView.lockPortrait(); + } + freezeOtherScreens(false); + }; }, []); const leaveCallHandler = useCallback(() => { @@ -473,12 +513,14 @@ const CallScreen = ({ streamURL={currentCall.screenShareURL} style={style.screenShareImage} /> - + {!isLandscape && + + } ); } @@ -545,7 +587,11 @@ const CallScreen = ({ {waitingForRecording && } {recording && } @@ -558,18 +604,27 @@ const CallScreen = ({ {usersList} {screenShareView} {micPermissionsError && } - - {showReactions && } + {!isLandscape && + + } + {showReactions && + + } {!isLandscape && {myParticipant.muted ? UnmuteText : MuteText} - } - + + } + {myParticipant.muted ? UnmuteText : MuteText} - } + + }