@ethora/chat-component-rn 26.5.21 → 26.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +3 -3
- package/index.js +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +20 -8
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +17 -0
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/LanguageSelectorButton.js +62 -0
- package/lib/commonjs/components/MainComponents/LanguageSelectorButton.js.map +1 -0
- package/lib/commonjs/components/MainComponents/LanguageSelectorModal.js +253 -0
- package/lib/commonjs/components/MainComponents/LanguageSelectorModal.js.map +1 -0
- package/lib/commonjs/components/MainComponents/RoomList.js +23 -9
- package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js +72 -11
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/MessageTranslate.js +160 -0
- package/lib/commonjs/components/MessageBubble/MessageTranslate.js.map +1 -0
- package/lib/commonjs/components/MessageBubble/TranslatedMessageBody.js +59 -0
- package/lib/commonjs/components/MessageBubble/TranslatedMessageBody.js.map +1 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +3 -3
- package/lib/commonjs/components/UsersList/UsersList.js +3 -1
- package/lib/commonjs/components/UsersList/UsersList.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/CallButtons.js +159 -0
- package/lib/commonjs/components/VideoCalls/CallButtons.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/CallIcons.js +328 -0
- package/lib/commonjs/components/VideoCalls/CallIcons.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js +550 -0
- package/lib/commonjs/components/VideoCalls/VideoCallOverlay.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/VideoCallSession.js +636 -0
- package/lib/commonjs/components/VideoCalls/VideoCallSession.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/livekitRuntime.js +91 -0
- package/lib/commonjs/components/VideoCalls/livekitRuntime.js.map +1 -0
- package/lib/commonjs/components/VideoCalls/useCallKeep.js +215 -0
- package/lib/commonjs/components/VideoCalls/useCallKeep.js.map +1 -0
- package/lib/commonjs/components/styled/AudioMessage.js +91 -33
- package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
- package/lib/commonjs/components/styled/RoomListComponents/index.js +13 -1
- package/lib/commonjs/components/styled/RoomListComponents/index.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +83 -49
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/components/styled/StyledComponents.js +9 -4
- package/lib/commonjs/components/styled/StyledComponents.js.map +1 -1
- package/lib/commonjs/components/styled/StyledInputComponents/Composing.js +2 -0
- package/lib/commonjs/components/styled/StyledInputComponents/Composing.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +10 -3
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/callLogMessage.js +162 -0
- package/lib/commonjs/helpers/callLogMessage.js.map +1 -0
- package/lib/commonjs/helpers/callPush.js +61 -0
- package/lib/commonjs/helpers/callPush.js.map +1 -0
- package/lib/commonjs/helpers/constants/LANGUAGE_OPTIONS.js +24 -12
- package/lib/commonjs/helpers/constants/LANGUAGE_OPTIONS.js.map +1 -1
- package/lib/commonjs/helpers/createUserNameFromSetUser.js +51 -2
- package/lib/commonjs/helpers/createUserNameFromSetUser.js.map +1 -1
- package/lib/commonjs/helpers/getDataFromXml.js +19 -1
- package/lib/commonjs/helpers/getDataFromXml.js.map +1 -1
- package/lib/commonjs/helpers/incomingCallFromPush.js +74 -0
- package/lib/commonjs/helpers/incomingCallFromPush.js.map +1 -0
- package/lib/commonjs/helpers/insertMessageWithDelimiter.js +13 -1
- package/lib/commonjs/helpers/insertMessageWithDelimiter.js.map +1 -1
- package/lib/commonjs/helpers/isUnresolvedSenderId.js +32 -0
- package/lib/commonjs/helpers/isUnresolvedSenderId.js.map +1 -0
- package/lib/commonjs/hooks/useChatSettingState.js +2 -0
- package/lib/commonjs/hooks/useChatSettingState.js.map +1 -1
- package/lib/commonjs/hooks/useMessageTranslation.js +51 -0
- package/lib/commonjs/hooks/useMessageTranslation.js.map +1 -0
- package/lib/commonjs/hooks/usePushNotifications.js +13 -3
- package/lib/commonjs/hooks/usePushNotifications.js.map +1 -1
- package/lib/commonjs/hooks/useSendMessage.js +12 -2
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/i18n/strings.js +1308 -0
- package/lib/commonjs/i18n/strings.js.map +1 -0
- package/lib/commonjs/i18n/useT.js +60 -0
- package/lib/commonjs/i18n/useT.js.map +1 -0
- package/lib/commonjs/main.js +44 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/networking/api-requests/push.api.js +34 -5
- package/lib/commonjs/networking/api-requests/push.api.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +108 -0
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/networking/callTokenStanza.js +531 -0
- package/lib/commonjs/networking/callTokenStanza.js.map +1 -0
- package/lib/commonjs/networking/outboundQueue.js.map +1 -1
- package/lib/commonjs/networking/stanzaHandlers.js +75 -8
- package/lib/commonjs/networking/stanzaHandlers.js.map +1 -1
- package/lib/commonjs/networking/xmpp/handleStanzas.xmpp.js +47 -11
- package/lib/commonjs/networking/xmpp/handleStanzas.xmpp.js.map +1 -1
- package/lib/commonjs/networking/xmpp/sendTextMessage.xmpp.js +2 -1
- package/lib/commonjs/networking/xmpp/sendTextMessage.xmpp.js.map +1 -1
- package/lib/commonjs/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.js +42 -5
- package/lib/commonjs/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +78 -16
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/callSlice.js +144 -0
- package/lib/commonjs/roomStore/callSlice.js.map +1 -0
- package/lib/commonjs/roomStore/chatSettingsSlice.js +8 -2
- package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/commonjs/roomStore/index.js +7 -1
- package/lib/commonjs/roomStore/index.js.map +1 -1
- package/lib/commonjs/roomStore/persistence.js +69 -5
- package/lib/commonjs/roomStore/persistence.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +136 -6
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/commonjs/utils/translateModePolicy.js +33 -0
- package/lib/commonjs/utils/translateModePolicy.js.map +1 -0
- package/lib/module/components/MainComponents/ChatHeader.js +20 -8
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +17 -0
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/LanguageSelectorButton.js +57 -0
- package/lib/module/components/MainComponents/LanguageSelectorButton.js.map +1 -0
- package/lib/module/components/MainComponents/LanguageSelectorModal.js +246 -0
- package/lib/module/components/MainComponents/LanguageSelectorModal.js.map +1 -0
- package/lib/module/components/MainComponents/RoomList.js +23 -9
- package/lib/module/components/MainComponents/RoomList.js.map +1 -1
- package/lib/module/components/MessageBubble/Message.js +73 -12
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/MessageBubble/MessageTranslate.js +155 -0
- package/lib/module/components/MessageBubble/MessageTranslate.js.map +1 -0
- package/lib/module/components/MessageBubble/TranslatedMessageBody.js +53 -0
- package/lib/module/components/MessageBubble/TranslatedMessageBody.js.map +1 -0
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +3 -3
- package/lib/module/components/UsersList/UsersList.js +3 -1
- package/lib/module/components/UsersList/UsersList.js.map +1 -1
- package/lib/module/components/VideoCalls/CallButtons.js +154 -0
- package/lib/module/components/VideoCalls/CallButtons.js.map +1 -0
- package/lib/module/components/VideoCalls/CallIcons.js +309 -0
- package/lib/module/components/VideoCalls/CallIcons.js.map +1 -0
- package/lib/module/components/VideoCalls/VideoCallOverlay.js +544 -0
- package/lib/module/components/VideoCalls/VideoCallOverlay.js.map +1 -0
- package/lib/module/components/VideoCalls/VideoCallSession.js +630 -0
- package/lib/module/components/VideoCalls/VideoCallSession.js.map +1 -0
- package/lib/module/components/VideoCalls/livekitRuntime.js +83 -0
- package/lib/module/components/VideoCalls/livekitRuntime.js.map +1 -0
- package/lib/module/components/VideoCalls/useCallKeep.js +210 -0
- package/lib/module/components/VideoCalls/useCallKeep.js.map +1 -0
- package/lib/module/components/styled/AudioMessage.js +91 -33
- package/lib/module/components/styled/AudioMessage.js.map +1 -1
- package/lib/module/components/styled/RoomListComponents/index.js +13 -1
- package/lib/module/components/styled/RoomListComponents/index.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +83 -49
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/components/styled/StyledComponents.js +9 -4
- package/lib/module/components/styled/StyledComponents.js.map +1 -1
- package/lib/module/components/styled/StyledInputComponents/Composing.js +2 -0
- package/lib/module/components/styled/StyledInputComponents/Composing.js.map +1 -1
- package/lib/module/context/xmppProvider.js +11 -4
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/callLogMessage.js +152 -0
- package/lib/module/helpers/callLogMessage.js.map +1 -0
- package/lib/module/helpers/callPush.js +55 -0
- package/lib/module/helpers/callPush.js.map +1 -0
- package/lib/module/helpers/constants/LANGUAGE_OPTIONS.js +24 -12
- package/lib/module/helpers/constants/LANGUAGE_OPTIONS.js.map +1 -1
- package/lib/module/helpers/createUserNameFromSetUser.js +49 -1
- package/lib/module/helpers/createUserNameFromSetUser.js.map +1 -1
- package/lib/module/helpers/getDataFromXml.js +20 -1
- package/lib/module/helpers/getDataFromXml.js.map +1 -1
- package/lib/module/helpers/incomingCallFromPush.js +70 -0
- package/lib/module/helpers/incomingCallFromPush.js.map +1 -0
- package/lib/module/helpers/insertMessageWithDelimiter.js +13 -1
- package/lib/module/helpers/insertMessageWithDelimiter.js.map +1 -1
- package/lib/module/helpers/isUnresolvedSenderId.js +27 -0
- package/lib/module/helpers/isUnresolvedSenderId.js.map +1 -0
- package/lib/module/hooks/useChatSettingState.js +2 -0
- package/lib/module/hooks/useChatSettingState.js.map +1 -1
- package/lib/module/hooks/useMessageTranslation.js +46 -0
- package/lib/module/hooks/useMessageTranslation.js.map +1 -0
- package/lib/module/hooks/usePushNotifications.js +13 -3
- package/lib/module/hooks/usePushNotifications.js.map +1 -1
- package/lib/module/hooks/useSendMessage.js +12 -2
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/i18n/strings.js +1300 -0
- package/lib/module/i18n/strings.js.map +1 -0
- package/lib/module/i18n/useT.js +55 -0
- package/lib/module/i18n/useT.js.map +1 -0
- package/lib/module/main.js +7 -1
- package/lib/module/main.js.map +1 -1
- package/lib/module/networking/api-requests/push.api.js +35 -5
- package/lib/module/networking/api-requests/push.api.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +109 -1
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/networking/callTokenStanza.js +526 -0
- package/lib/module/networking/callTokenStanza.js.map +1 -0
- package/lib/module/networking/outboundQueue.js.map +1 -1
- package/lib/module/networking/stanzaHandlers.js +76 -9
- package/lib/module/networking/stanzaHandlers.js.map +1 -1
- package/lib/module/networking/xmpp/handleStanzas.xmpp.js +48 -11
- package/lib/module/networking/xmpp/handleStanzas.xmpp.js.map +1 -1
- package/lib/module/networking/xmpp/sendTextMessage.xmpp.js +1 -1
- package/lib/module/networking/xmpp/sendTextMessage.xmpp.js.map +1 -1
- package/lib/module/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.js +43 -5
- package/lib/module/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.js.map +1 -1
- package/lib/module/networking/xmppClient.js +78 -16
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/callSlice.js +130 -0
- package/lib/module/roomStore/callSlice.js.map +1 -0
- package/lib/module/roomStore/chatSettingsSlice.js +6 -1
- package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/module/roomStore/index.js +6 -1
- package/lib/module/roomStore/index.js.map +1 -1
- package/lib/module/roomStore/persistence.js +69 -5
- package/lib/module/roomStore/persistence.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +134 -5
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/utils/translateModePolicy.js +27 -0
- package/lib/module/utils/translateModePolicy.js.map +1 -0
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/LanguageSelectorButton.d.ts +12 -0
- package/lib/typescript/components/MainComponents/LanguageSelectorButton.d.ts.map +1 -0
- package/lib/typescript/components/MainComponents/LanguageSelectorModal.d.ts +31 -0
- package/lib/typescript/components/MainComponents/LanguageSelectorModal.d.ts.map +1 -0
- package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/MessageTranslate.d.ts +44 -0
- package/lib/typescript/components/MessageBubble/MessageTranslate.d.ts.map +1 -0
- package/lib/typescript/components/MessageBubble/TranslatedMessageBody.d.ts +24 -0
- package/lib/typescript/components/MessageBubble/TranslatedMessageBody.d.ts.map +1 -0
- package/lib/typescript/components/UsersList/UsersList.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/CallButtons.d.ts +16 -0
- package/lib/typescript/components/VideoCalls/CallButtons.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/CallIcons.d.ts +30 -0
- package/lib/typescript/components/VideoCalls/CallIcons.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts +10 -0
- package/lib/typescript/components/VideoCalls/VideoCallOverlay.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts +38 -0
- package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts +52 -0
- package/lib/typescript/components/VideoCalls/livekitRuntime.d.ts.map +1 -0
- package/lib/typescript/components/VideoCalls/useCallKeep.d.ts +32 -0
- package/lib/typescript/components/VideoCalls/useCallKeep.d.ts.map +1 -0
- package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/styled/RoomListComponents/index.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledComponents.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledInputComponents/Composing.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/callLogMessage.d.ts +34 -0
- package/lib/typescript/helpers/callLogMessage.d.ts.map +1 -0
- package/lib/typescript/helpers/callPush.d.ts +23 -0
- package/lib/typescript/helpers/callPush.d.ts.map +1 -0
- package/lib/typescript/helpers/constants/LANGUAGE_OPTIONS.d.ts +14 -0
- package/lib/typescript/helpers/constants/LANGUAGE_OPTIONS.d.ts.map +1 -1
- package/lib/typescript/helpers/createUserNameFromSetUser.d.ts +33 -1
- package/lib/typescript/helpers/createUserNameFromSetUser.d.ts.map +1 -1
- package/lib/typescript/helpers/getDataFromXml.d.ts.map +1 -1
- package/lib/typescript/helpers/incomingCallFromPush.d.ts +14 -0
- package/lib/typescript/helpers/incomingCallFromPush.d.ts.map +1 -0
- package/lib/typescript/helpers/insertMessageWithDelimiter.d.ts.map +1 -1
- package/lib/typescript/helpers/isUnresolvedSenderId.d.ts +2 -0
- package/lib/typescript/helpers/isUnresolvedSenderId.d.ts.map +1 -0
- package/lib/typescript/hooks/useChatSettingState.d.ts +2 -1
- package/lib/typescript/hooks/useChatSettingState.d.ts.map +1 -1
- package/lib/typescript/hooks/useMessageTranslation.d.ts +25 -0
- package/lib/typescript/hooks/useMessageTranslation.d.ts.map +1 -0
- package/lib/typescript/hooks/usePushNotifications.d.ts.map +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/i18n/strings.d.ts +16 -0
- package/lib/typescript/i18n/strings.d.ts.map +1 -0
- package/lib/typescript/i18n/useT.d.ts +34 -0
- package/lib/typescript/i18n/useT.d.ts.map +1 -0
- package/lib/typescript/main.d.ts +6 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/push.api.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts +3 -0
- package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
- package/lib/typescript/networking/callTokenStanza.d.ts +14 -0
- package/lib/typescript/networking/callTokenStanza.d.ts.map +1 -0
- package/lib/typescript/networking/outboundQueue.d.ts +1 -0
- package/lib/typescript/networking/outboundQueue.d.ts.map +1 -1
- package/lib/typescript/networking/stanzaHandlers.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/handleStanzas.xmpp.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/sendTextMessage.xmpp.d.ts +10 -0
- package/lib/typescript/networking/xmpp/sendTextMessage.xmpp.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.d.ts +16 -1
- package/lib/typescript/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts +1 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/callSlice.d.ts +39 -0
- package/lib/typescript/roomStore/callSlice.d.ts.map +1 -0
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts +10 -1
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
- package/lib/typescript/roomStore/index.d.ts +4 -0
- package/lib/typescript/roomStore/index.d.ts.map +1 -1
- package/lib/typescript/roomStore/persistence.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +6 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/models/language.model.d.ts +1 -1
- package/lib/typescript/types/models/language.model.d.ts.map +1 -1
- package/lib/typescript/types/models/message.model.d.ts +7 -0
- package/lib/typescript/types/models/message.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +135 -1
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/lib/typescript/utils/translateModePolicy.d.ts +20 -0
- package/lib/typescript/utils/translateModePolicy.d.ts.map +1 -0
- package/package.json +20 -4
- package/src/components/MainComponents/ChatHeader.tsx +30 -9
- package/src/components/MainComponents/ChatWrapper.tsx +15 -0
- package/src/components/MainComponents/LanguageSelectorButton.tsx +53 -0
- package/src/components/MainComponents/LanguageSelectorModal.tsx +276 -0
- package/src/components/MainComponents/RoomList.tsx +27 -9
- package/src/components/MessageBubble/Message.tsx +109 -17
- package/src/components/MessageBubble/MessageTranslate.tsx +182 -0
- package/src/components/MessageBubble/TranslatedMessageBody.tsx +59 -0
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +3 -3
- package/src/components/UsersList/UsersList.tsx +3 -1
- package/src/components/VideoCalls/CallButtons.tsx +171 -0
- package/src/components/VideoCalls/CallIcons.tsx +246 -0
- package/src/components/VideoCalls/VideoCallOverlay.tsx +627 -0
- package/src/components/VideoCalls/VideoCallSession.tsx +675 -0
- package/src/components/VideoCalls/livekitRuntime.ts +100 -0
- package/src/components/VideoCalls/useCallKeep.ts +252 -0
- package/src/components/styled/AudioMessage.tsx +97 -37
- package/src/components/styled/RoomListComponents/index.tsx +15 -3
- package/src/components/styled/SendInput.tsx +94 -51
- package/src/components/styled/StyledComponents.ts +9 -6
- package/src/components/styled/StyledInputComponents/Composing.tsx +7 -1
- package/src/context/xmppProvider.tsx +14 -1
- package/src/helpers/callLogMessage.ts +174 -0
- package/src/helpers/callPush.ts +90 -0
- package/src/helpers/constants/LANGUAGE_OPTIONS.ts +20 -5
- package/src/helpers/createUserNameFromSetUser.ts +68 -2
- package/src/helpers/getDataFromXml.ts +40 -4
- package/src/helpers/incomingCallFromPush.ts +83 -0
- package/src/helpers/insertMessageWithDelimiter.ts +27 -9
- package/src/helpers/isUnresolvedSenderId.ts +25 -0
- package/src/hooks/useChatSettingState.tsx +4 -0
- package/src/hooks/useMessageTranslation.ts +70 -0
- package/src/hooks/usePushNotifications.ts +15 -3
- package/src/hooks/useSendMessage.tsx +15 -2
- package/src/i18n/strings.ts +1322 -0
- package/src/i18n/useT.ts +56 -0
- package/src/main.ts +18 -1
- package/src/networking/api-requests/push.api.ts +39 -8
- package/src/networking/api-requests/rooms.api.ts +109 -1
- package/src/networking/callTokenStanza.ts +636 -0
- package/src/networking/outboundQueue.ts +1 -0
- package/src/networking/stanzaHandlers.ts +74 -15
- package/src/networking/xmpp/handleStanzas.xmpp.ts +52 -11
- package/src/networking/xmpp/sendTextMessage.xmpp.ts +1 -1
- package/src/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.ts +41 -5
- package/src/networking/xmppClient.ts +97 -28
- package/src/roomStore/callSlice.ts +190 -0
- package/src/roomStore/chatSettingsSlice.ts +16 -0
- package/src/roomStore/index.ts +5 -0
- package/src/roomStore/persistence.ts +103 -3
- package/src/roomStore/roomsSlice.ts +149 -4
- package/src/types/models/language.model.ts +17 -1
- package/src/types/models/message.model.ts +9 -0
- package/src/types/types.ts +142 -2
- package/src/utils/translateModePolicy.ts +31 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional-dependency bridge for the LiveKit React Native SDK.
|
|
3
|
+
*
|
|
4
|
+
* Calls are opt-in (`config.videoCalls.enabled`), and the LiveKit packages
|
|
5
|
+
* are NATIVE modules: they need a dev build, they cannot run in Expo Go,
|
|
6
|
+
* and they add camera/microphone entitlements to whatever app installs
|
|
7
|
+
* them. Most hosts embedding this chat SDK don't want calls at all, so
|
|
8
|
+
* `@livekit/react-native` and `@livekit/react-native-webrtc` are declared
|
|
9
|
+
* as OPTIONAL peer dependencies.
|
|
10
|
+
*
|
|
11
|
+
* That means a plain top-level `import ... from '@livekit/react-native'`
|
|
12
|
+
* is not allowed anywhere in the SDK: Metro resolves imports at bundle
|
|
13
|
+
* time, so a chat-only host would fail to build over a feature it never
|
|
14
|
+
* switched on. Everything call-related goes through `loadLiveKit()`, which
|
|
15
|
+
* requires the modules lazily at call time and returns null when they
|
|
16
|
+
* aren't installed. Callers surface that as a friendly config error rather
|
|
17
|
+
* than a redbox.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface LiveKitRuntime {
|
|
21
|
+
/** livekit-client `Room` constructor. */
|
|
22
|
+
Room: any;
|
|
23
|
+
/** livekit-client `RoomEvent` enum. */
|
|
24
|
+
RoomEvent: any;
|
|
25
|
+
/** livekit-client `Track` enum (Track.Source.Camera etc). */
|
|
26
|
+
Track: any;
|
|
27
|
+
/** livekit-client `ConnectionState` enum. */
|
|
28
|
+
ConnectionState: any;
|
|
29
|
+
/** RN `<VideoTrack trackRef={...} />` renderer. */
|
|
30
|
+
VideoTrack: any;
|
|
31
|
+
/** Native audio session control (routing, speaker, focus). */
|
|
32
|
+
AudioSession: any;
|
|
33
|
+
/** Installs the WebRTC globals livekit-client expects. */
|
|
34
|
+
registerGlobals: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let cached: LiveKitRuntime | null | undefined;
|
|
38
|
+
|
|
39
|
+
export const isLiveKitAvailable = (): boolean => loadLiveKit() !== null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the LiveKit runtime, or null when the optional native packages
|
|
43
|
+
* aren't installed in the host app. Result is memoised (including the
|
|
44
|
+
* failure) so a missing dependency doesn't re-throw on every render.
|
|
45
|
+
*/
|
|
46
|
+
export const loadLiveKit = (): LiveKitRuntime | null => {
|
|
47
|
+
if (cached !== undefined) {
|
|
48
|
+
return cached;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
+
const rn = require('@livekit/react-native');
|
|
54
|
+
// `livekit-client` is a hard dependency of @livekit/react-native, so
|
|
55
|
+
// if the first require succeeded this one will too. Read the enums
|
|
56
|
+
// from it rather than from the RN wrapper: the wrapper re-exports a
|
|
57
|
+
// moving subset across versions, the core package does not.
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
59
|
+
const core = require('livekit-client');
|
|
60
|
+
|
|
61
|
+
cached = {
|
|
62
|
+
Room: core.Room,
|
|
63
|
+
RoomEvent: core.RoomEvent,
|
|
64
|
+
Track: core.Track,
|
|
65
|
+
ConnectionState: core.ConnectionState,
|
|
66
|
+
VideoTrack: rn.VideoTrack,
|
|
67
|
+
AudioSession: rn.AudioSession,
|
|
68
|
+
registerGlobals: rn.registerGlobals,
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
cached = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return cached;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let globalsRegistered = false;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `registerGlobals()` patches RTCPeerConnection / MediaStream / navigator
|
|
81
|
+
* .mediaDevices onto the JS global scope so livekit-client (written for
|
|
82
|
+
* browsers) works on React Native. It must run exactly once per JS
|
|
83
|
+
* context, before the first `new Room()`. Calling it repeatedly is
|
|
84
|
+
* harmless in current versions but has thrown in older ones, so guard it.
|
|
85
|
+
*/
|
|
86
|
+
export const ensureLiveKitGlobals = (runtime: LiveKitRuntime): void => {
|
|
87
|
+
if (globalsRegistered) {return;}
|
|
88
|
+
try {
|
|
89
|
+
runtime.registerGlobals?.();
|
|
90
|
+
} catch {
|
|
91
|
+
// Already registered by the host app (it may use LiveKit elsewhere).
|
|
92
|
+
}
|
|
93
|
+
globalsRegistered = true;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Reset memoised state. Test seam only. */
|
|
97
|
+
export const __resetLiveKitRuntimeForTests = (): void => {
|
|
98
|
+
cached = undefined;
|
|
99
|
+
globalsRegistered = false;
|
|
100
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
4
|
+
import { RootState, store } from '../../roomStore';
|
|
5
|
+
import { acceptIncomingCall, declineIncomingCall } from '../../roomStore/callSlice';
|
|
6
|
+
import { sendCallStateSignal } from '../../networking/callTokenStanza';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Native incoming-call UI (Android ConnectionService / iOS CallKit) via
|
|
10
|
+
* the optional `react-native-callkeep` package.
|
|
11
|
+
*
|
|
12
|
+
* Why optional: CallKeep pulls a telecom-framework dependency into the
|
|
13
|
+
* host app and, on iOS, is only half a solution without PushKit. It is
|
|
14
|
+
* loaded lazily exactly like the LiveKit packages, so hosts that don't
|
|
15
|
+
* install it get the plain in-app ring screen and nothing breaks.
|
|
16
|
+
*
|
|
17
|
+
* Platform reality, stated plainly:
|
|
18
|
+
* - Android: full value. A ConnectionService call shows the system
|
|
19
|
+
* incoming-call screen over the lock screen.
|
|
20
|
+
* - iOS: this only fires while the app is running. A CallKit screen from
|
|
21
|
+
* a killed app requires a VoIP push over PushKit, which needs a
|
|
22
|
+
* separate Apple VoIP certificate and a separate send path on the
|
|
23
|
+
* backend (not FCM). Until that exists, iOS falls back to the regular
|
|
24
|
+
* data push and the in-app ring screen.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
interface CallKeepModule {
|
|
28
|
+
setup: (options: any) => Promise<void>;
|
|
29
|
+
displayIncomingCall: (
|
|
30
|
+
uuid: string,
|
|
31
|
+
handle: string,
|
|
32
|
+
localizedCallerName?: string,
|
|
33
|
+
handleType?: string,
|
|
34
|
+
hasVideo?: boolean
|
|
35
|
+
) => void;
|
|
36
|
+
endCall: (uuid: string) => void;
|
|
37
|
+
endAllCalls: () => void;
|
|
38
|
+
addEventListener: (event: string, handler: (payload: any) => void) => void;
|
|
39
|
+
removeEventListener: (event: string) => void;
|
|
40
|
+
setAvailable?: (available: boolean) => void;
|
|
41
|
+
backToForeground?: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let cachedCallKeep: CallKeepModule | null | undefined;
|
|
45
|
+
|
|
46
|
+
export const loadCallKeep = (): CallKeepModule | null => {
|
|
47
|
+
if (cachedCallKeep !== undefined) {
|
|
48
|
+
return cachedCallKeep;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
// The JS package being installed does NOT mean the native side is
|
|
52
|
+
// linked: a host can `npm i react-native-callkeep` and skip
|
|
53
|
+
// `pod install`, or (very common in this repo) node_modules can carry
|
|
54
|
+
// it over from another branch whose build did link it. In that state
|
|
55
|
+
// requiring the package builds a `new NativeEventEmitter(null)` at
|
|
56
|
+
// module scope, which throws an invariant that RN surfaces as an
|
|
57
|
+
// uncaught redbox on every render of the call overlay — the whole app
|
|
58
|
+
// becomes unusable over a feature the host never switched on.
|
|
59
|
+
// Probing NativeModules first keeps the optional dependency genuinely
|
|
60
|
+
// optional: no native module => behave exactly as if it isn't installed.
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
62
|
+
const { NativeModules } = require('react-native');
|
|
63
|
+
if (!NativeModules?.RNCallKeep) {
|
|
64
|
+
cachedCallKeep = null;
|
|
65
|
+
return cachedCallKeep;
|
|
66
|
+
}
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
68
|
+
const mod = require('react-native-callkeep');
|
|
69
|
+
cachedCallKeep = (mod?.default || mod) as CallKeepModule;
|
|
70
|
+
} catch {
|
|
71
|
+
cachedCallKeep = null;
|
|
72
|
+
}
|
|
73
|
+
return cachedCallKeep;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// CallKeep identifies calls by UUID. Our callIds aren't UUIDs, so derive a
|
|
77
|
+
// stable v4-shaped string from the callId: same call always maps to the
|
|
78
|
+
// same UUID, which is what endCall() needs to dismiss the right screen.
|
|
79
|
+
const toUuid = (seed: string): string => {
|
|
80
|
+
let hash = 0x811c9dc5;
|
|
81
|
+
const bytes: number[] = [];
|
|
82
|
+
for (let i = 0; i < 32; i++) {
|
|
83
|
+
const ch = seed.charCodeAt(i % Math.max(1, seed.length)) + i;
|
|
84
|
+
hash ^= ch;
|
|
85
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
86
|
+
bytes.push(hash & 0xff);
|
|
87
|
+
}
|
|
88
|
+
const hex = bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
89
|
+
return [
|
|
90
|
+
hex.slice(0, 8),
|
|
91
|
+
hex.slice(8, 12),
|
|
92
|
+
'4' + hex.slice(13, 16),
|
|
93
|
+
((parseInt(hex[16], 16) & 0x3) | 0x8).toString(16) + hex.slice(17, 20),
|
|
94
|
+
hex.slice(20, 32),
|
|
95
|
+
].join('-');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const useCallKeep = (): void => {
|
|
99
|
+
const dispatch = useDispatch();
|
|
100
|
+
const call = useSelector((state: RootState) => state.call);
|
|
101
|
+
const callKeep = useMemo(() => loadCallKeep(), []);
|
|
102
|
+
const activeUuidRef = useRef<string | null>(null);
|
|
103
|
+
const readyRef = useRef(false);
|
|
104
|
+
|
|
105
|
+
// One-time native setup.
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!callKeep) {return;}
|
|
108
|
+
let cancelled = false;
|
|
109
|
+
|
|
110
|
+
(async () => {
|
|
111
|
+
try {
|
|
112
|
+
await callKeep.setup({
|
|
113
|
+
ios: {
|
|
114
|
+
appName: 'Ethora',
|
|
115
|
+
supportsVideo: true,
|
|
116
|
+
},
|
|
117
|
+
android: {
|
|
118
|
+
alertTitle: 'Permissions required',
|
|
119
|
+
alertDescription:
|
|
120
|
+
'This application needs to access your phone accounts to show incoming calls',
|
|
121
|
+
cancelButton: 'Cancel',
|
|
122
|
+
okButton: 'ok',
|
|
123
|
+
additionalPermissions: [],
|
|
124
|
+
foregroundService: {
|
|
125
|
+
channelId: 'com.ethora.chat.calls',
|
|
126
|
+
channelName: 'Incoming calls',
|
|
127
|
+
notificationTitle: 'Call in progress',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
if (cancelled) {return;}
|
|
132
|
+
callKeep.setAvailable?.(true);
|
|
133
|
+
readyRef.current = true;
|
|
134
|
+
} catch {
|
|
135
|
+
// Host didn't grant phone-account permission, or the module isn't
|
|
136
|
+
// linked. The in-app ring screen still works, so stay quiet.
|
|
137
|
+
readyRef.current = false;
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
cancelled = true;
|
|
143
|
+
};
|
|
144
|
+
}, [callKeep]);
|
|
145
|
+
|
|
146
|
+
// Bridge native answer/end actions back into Redux.
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!callKeep) {return;}
|
|
149
|
+
|
|
150
|
+
const onAnswer = () => {
|
|
151
|
+
// The user answered from the system screen, which on Android may
|
|
152
|
+
// mean the app was not even in the foreground.
|
|
153
|
+
callKeep.backToForeground?.();
|
|
154
|
+
if (store.getState().call.direction === 'incoming') {
|
|
155
|
+
dispatch(acceptIncomingCall());
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const onEnd = () => {
|
|
160
|
+
const snapshot = store.getState().call;
|
|
161
|
+
if (snapshot.phase === 'idle') {return;}
|
|
162
|
+
if (
|
|
163
|
+
snapshot.direction === 'incoming' &&
|
|
164
|
+
snapshot.phase === 'ringing-incoming'
|
|
165
|
+
) {
|
|
166
|
+
sendCallStateSignal('declined');
|
|
167
|
+
dispatch(declineIncomingCall());
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
callKeep.addEventListener('answerCall', onAnswer);
|
|
172
|
+
callKeep.addEventListener('endCall', onEnd);
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
callKeep.removeEventListener('answerCall');
|
|
176
|
+
callKeep.removeEventListener('endCall');
|
|
177
|
+
};
|
|
178
|
+
}, [callKeep, dispatch]);
|
|
179
|
+
|
|
180
|
+
// Show / dismiss the native screen as the call state changes.
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!callKeep || !readyRef.current) {return;}
|
|
183
|
+
|
|
184
|
+
const isRinging =
|
|
185
|
+
call.direction === 'incoming' && call.phase === 'ringing-incoming';
|
|
186
|
+
|
|
187
|
+
if (isRinging && !activeUuidRef.current) {
|
|
188
|
+
const uuid = toUuid(call.callId || call.roomJid || String(Date.now()));
|
|
189
|
+
activeUuidRef.current = uuid;
|
|
190
|
+
try {
|
|
191
|
+
callKeep.displayIncomingCall(
|
|
192
|
+
uuid,
|
|
193
|
+
call.peerXmppUsername || call.roomName || 'Ethora',
|
|
194
|
+
call.roomName || undefined,
|
|
195
|
+
'generic',
|
|
196
|
+
call.kind === 'video'
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
activeUuidRef.current = null;
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Any exit from ringing (answered in-app, declined, peer cancelled)
|
|
205
|
+
// must tear the system screen down or it hangs around ringing.
|
|
206
|
+
if (!isRinging && activeUuidRef.current && call.phase !== 'connecting' && call.phase !== 'in-call') {
|
|
207
|
+
try {
|
|
208
|
+
callKeep.endCall(activeUuidRef.current);
|
|
209
|
+
} catch {
|
|
210
|
+
// Screen already gone.
|
|
211
|
+
}
|
|
212
|
+
activeUuidRef.current = null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (call.phase === 'idle' && activeUuidRef.current) {
|
|
216
|
+
try {
|
|
217
|
+
callKeep.endCall(activeUuidRef.current);
|
|
218
|
+
} catch {
|
|
219
|
+
// noop
|
|
220
|
+
}
|
|
221
|
+
activeUuidRef.current = null;
|
|
222
|
+
}
|
|
223
|
+
}, [
|
|
224
|
+
callKeep,
|
|
225
|
+
call.direction,
|
|
226
|
+
call.phase,
|
|
227
|
+
call.callId,
|
|
228
|
+
call.roomJid,
|
|
229
|
+
call.roomName,
|
|
230
|
+
call.peerXmppUsername,
|
|
231
|
+
call.kind,
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
// Nothing to clean up on unmount beyond the active screen: this hook
|
|
235
|
+
// lives for the lifetime of the provider.
|
|
236
|
+
useEffect(
|
|
237
|
+
() => () => {
|
|
238
|
+
if (callKeep && activeUuidRef.current) {
|
|
239
|
+
try {
|
|
240
|
+
callKeep.endCall(activeUuidRef.current);
|
|
241
|
+
} catch {
|
|
242
|
+
// noop
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
[callKeep]
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// iOS-only note kept as a runtime no-op so the import isn't unused when
|
|
250
|
+
// the platform check is tree-shaken in release builds.
|
|
251
|
+
void Platform.OS;
|
|
252
|
+
};
|
|
@@ -7,7 +7,12 @@ import {
|
|
|
7
7
|
ActivityIndicator,
|
|
8
8
|
Platform,
|
|
9
9
|
} from 'react-native';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createAudioPlayer,
|
|
12
|
+
setAudioModeAsync,
|
|
13
|
+
type AudioPlayer,
|
|
14
|
+
type AudioStatus,
|
|
15
|
+
} from 'expo-audio';
|
|
11
16
|
import * as FileSystem from 'expo-file-system/legacy';
|
|
12
17
|
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
|
13
18
|
import { PauseIcon, PlayIcon } from '../../assets/icons';
|
|
@@ -196,11 +201,11 @@ const getContainerExtension = (container: AudioContainer) => {
|
|
|
196
201
|
}
|
|
197
202
|
};
|
|
198
203
|
|
|
199
|
-
// AVFoundation (expo-
|
|
204
|
+
// AVFoundation (expo-audio) and the iOS WKWebView <audio> element cannot
|
|
200
205
|
// demux/decode WebM or Ogg Opus — only `AudioContext.decodeAudioData` can
|
|
201
206
|
// on iOS. So on iOS those two containers go through the WebView decoder.
|
|
202
207
|
// Everything else (mp3/m4a/aac/wav), and ALL of Android (ExoPlayer handles
|
|
203
|
-
// Opus natively), plays straight through expo-
|
|
208
|
+
// Opus natively), plays straight through expo-audio.
|
|
204
209
|
const requiresWebAudioDecode = (container: AudioContainer) =>
|
|
205
210
|
Platform.OS === 'ios' && (container === 'webm' || container === 'ogg');
|
|
206
211
|
|
|
@@ -213,7 +218,7 @@ const requiresWebAudioDecode = (container: AudioContainer) =>
|
|
|
213
218
|
// cannot supply, so both hang silently.
|
|
214
219
|
// Therefore the page decodes Opus → PCM, re-encodes to a WAV blob, and ships
|
|
215
220
|
// the bytes back to RN as base64. RN writes the WAV to cache and plays it
|
|
216
|
-
// through expo-
|
|
221
|
+
// through expo-audio (native, no gesture limits, real play/pause/seek).
|
|
217
222
|
const WEB_DECODER_HTML = `<!doctype html>
|
|
218
223
|
<html>
|
|
219
224
|
<head>
|
|
@@ -314,7 +319,11 @@ const AudioMessage = ({
|
|
|
314
319
|
originalName,
|
|
315
320
|
}: AudioMessageProps) => {
|
|
316
321
|
const { config } = useChatSettingState();
|
|
317
|
-
const soundRef = useRef<
|
|
322
|
+
const soundRef = useRef<AudioPlayer | null>(null);
|
|
323
|
+
// expo-audio delivers progress through an event subscription instead of
|
|
324
|
+
// expo-av's `onPlaybackStatusUpdate` callback argument, so the handle has
|
|
325
|
+
// to be held and removed alongside the player itself.
|
|
326
|
+
const statusSubRef = useRef<{ remove: () => void } | null>(null);
|
|
318
327
|
const webViewRef = useRef<WebView | null>(null);
|
|
319
328
|
const preparedRef = useRef<PreparedSource | null>(null);
|
|
320
329
|
const bridgeReadyRef = useRef(false);
|
|
@@ -338,6 +347,29 @@ const AudioMessage = ({
|
|
|
338
347
|
}
|
|
339
348
|
};
|
|
340
349
|
|
|
350
|
+
// expo-audio players are native shared objects: they are NOT garbage
|
|
351
|
+
// collected with the component, so every one we create has to be
|
|
352
|
+
// explicitly `remove()`d (expo-av's `unloadAsync` equivalent) and its
|
|
353
|
+
// status subscription torn down first.
|
|
354
|
+
const releasePlayer = () => {
|
|
355
|
+
statusSubRef.current?.remove();
|
|
356
|
+
statusSubRef.current = null;
|
|
357
|
+
const player = soundRef.current;
|
|
358
|
+
soundRef.current = null;
|
|
359
|
+
if (player) {
|
|
360
|
+
try {
|
|
361
|
+
player.pause();
|
|
362
|
+
} catch {
|
|
363
|
+
/* already released */
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
player.remove();
|
|
367
|
+
} catch {
|
|
368
|
+
/* already released */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
341
373
|
const fail = (reason: unknown) => {
|
|
342
374
|
clearLoadingGuard();
|
|
343
375
|
if (unmountedRef.current) {
|
|
@@ -360,7 +392,7 @@ const AudioMessage = ({
|
|
|
360
392
|
};
|
|
361
393
|
|
|
362
394
|
// Hard ceiling on the loading spinner — whatever stalls (a hung network
|
|
363
|
-
// request, a WebView that never decodes,
|
|
395
|
+
// request, a WebView that never decodes, a player that never loads)
|
|
364
396
|
// the control resolves to an error state instead of spinning forever
|
|
365
397
|
// (the customer-reported "stuck spinner").
|
|
366
398
|
const armLoadingGuard = () => {
|
|
@@ -480,50 +512,70 @@ const AudioMessage = ({
|
|
|
480
512
|
}
|
|
481
513
|
};
|
|
482
514
|
|
|
483
|
-
|
|
515
|
+
// expo-audio reports times in SECONDS (expo-av used milliseconds); the
|
|
516
|
+
// whole UI below is millisecond-based, so convert at this boundary and
|
|
517
|
+
// nowhere else.
|
|
518
|
+
const onPlaybackStatusUpdate = (status: AudioStatus) => {
|
|
484
519
|
if (!status.isLoaded) {
|
|
485
|
-
if (status.error) {
|
|
486
|
-
logAudioDebug('playback status error', { src, error: status.error });
|
|
487
|
-
}
|
|
488
520
|
return;
|
|
489
521
|
}
|
|
490
|
-
setPosition(status.
|
|
491
|
-
setDuration(status.
|
|
492
|
-
setIsPlaying(status.
|
|
522
|
+
setPosition(Math.max(0, status.currentTime * 1000));
|
|
523
|
+
setDuration(status.duration > 0 ? status.duration * 1000 : 0);
|
|
524
|
+
setIsPlaying(status.playing);
|
|
493
525
|
if (status.didJustFinish) {
|
|
494
526
|
didFinishRef.current = true;
|
|
495
527
|
setIsPlaying(false);
|
|
496
528
|
setPosition(0);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
529
|
+
const player = soundRef.current;
|
|
530
|
+
if (player) {
|
|
531
|
+
player.pause();
|
|
532
|
+
void player.seekTo(0);
|
|
533
|
+
}
|
|
501
534
|
}
|
|
502
535
|
};
|
|
503
536
|
|
|
504
537
|
const startNativePlayback = async (localUri: string) => {
|
|
505
538
|
try {
|
|
506
|
-
await
|
|
539
|
+
await setAudioModeAsync({ playsInSilentMode: true });
|
|
507
540
|
} catch {
|
|
508
541
|
/* non-fatal — still plays through the ringer channel */
|
|
509
542
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
543
|
+
// expo-audio has no awaitable `createAsync`: the player is constructed
|
|
544
|
+
// synchronously and loads in the background, reporting `isLoaded` via
|
|
545
|
+
// the status event. Wait for that first loaded status (same 12s budget
|
|
546
|
+
// expo-av's createAsync had) so the spinner still resolves to either
|
|
547
|
+
// real playback or the error state, never to a silent dead button.
|
|
548
|
+
const player = createAudioPlayer({ uri: localUri }, { updateInterval: 250 });
|
|
549
|
+
let settleLoaded: (() => void) | null = null;
|
|
550
|
+
const loaded = new Promise<void>((resolve) => {
|
|
551
|
+
settleLoaded = resolve;
|
|
552
|
+
});
|
|
553
|
+
const subscription = player.addListener(
|
|
554
|
+
'playbackStatusUpdate',
|
|
555
|
+
(status: AudioStatus) => {
|
|
556
|
+
if (status.isLoaded && settleLoaded) {
|
|
557
|
+
settleLoaded();
|
|
558
|
+
settleLoaded = null;
|
|
559
|
+
}
|
|
560
|
+
onPlaybackStatusUpdate(status);
|
|
561
|
+
}
|
|
521
562
|
);
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
await withTimeout(loaded, 12000, 'audio_create_timeout');
|
|
566
|
+
} catch (error) {
|
|
567
|
+
subscription.remove();
|
|
568
|
+
player.remove();
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
522
571
|
if (unmountedRef.current) {
|
|
523
|
-
|
|
572
|
+
subscription.remove();
|
|
573
|
+
player.remove();
|
|
524
574
|
return;
|
|
525
575
|
}
|
|
526
|
-
soundRef.current =
|
|
576
|
+
soundRef.current = player;
|
|
577
|
+
statusSubRef.current = subscription;
|
|
578
|
+
player.play();
|
|
527
579
|
clearLoadingGuard();
|
|
528
580
|
setIsLoading(false);
|
|
529
581
|
setPlaybackError(false);
|
|
@@ -595,14 +647,21 @@ const AudioMessage = ({
|
|
|
595
647
|
// decoded-WAV from the WebView path) → plain toggle.
|
|
596
648
|
if (soundRef.current) {
|
|
597
649
|
try {
|
|
650
|
+
// `play()` / `pause()` emit NO status event of their own (expo-av's
|
|
651
|
+
// playAsync/pauseAsync resolved with one). Playing state only ever
|
|
652
|
+
// reaches us through the periodic time observer, and that observer
|
|
653
|
+
// stops while paused — so without setting this here the button
|
|
654
|
+
// would latch on "pause" forever and never resume.
|
|
598
655
|
if (isPlaying) {
|
|
599
|
-
|
|
656
|
+
soundRef.current.pause();
|
|
657
|
+
setIsPlaying(false);
|
|
600
658
|
} else {
|
|
601
659
|
if (didFinishRef.current) {
|
|
602
|
-
await soundRef.current.
|
|
660
|
+
await soundRef.current.seekTo(0);
|
|
603
661
|
didFinishRef.current = false;
|
|
604
662
|
}
|
|
605
|
-
|
|
663
|
+
soundRef.current.play();
|
|
664
|
+
setIsPlaying(true);
|
|
606
665
|
}
|
|
607
666
|
} catch (error) {
|
|
608
667
|
fail(error);
|
|
@@ -643,7 +702,7 @@ const AudioMessage = ({
|
|
|
643
702
|
return () => {
|
|
644
703
|
unmountedRef.current = true;
|
|
645
704
|
clearLoadingGuard();
|
|
646
|
-
|
|
705
|
+
releasePlayer();
|
|
647
706
|
};
|
|
648
707
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
649
708
|
}, []);
|
|
@@ -651,8 +710,7 @@ const AudioMessage = ({
|
|
|
651
710
|
// Full reset whenever the source changes (component reused for a new
|
|
652
711
|
// message / re-render with a different src).
|
|
653
712
|
useEffect(() => {
|
|
654
|
-
|
|
655
|
-
soundRef.current = null;
|
|
713
|
+
releasePlayer();
|
|
656
714
|
preparedRef.current = null;
|
|
657
715
|
bridgeReadyRef.current = false;
|
|
658
716
|
injectedRef.current = false;
|
|
@@ -693,6 +751,8 @@ const AudioMessage = ({
|
|
|
693
751
|
) : null}
|
|
694
752
|
<View style={styles.container}>
|
|
695
753
|
<TouchableOpacity
|
|
754
|
+
testID="audio-play-button"
|
|
755
|
+
accessibilityLabel="audio-play-button"
|
|
696
756
|
style={[styles.playButton, { backgroundColor: primaryColor }]}
|
|
697
757
|
onPress={togglePlayback}
|
|
698
758
|
disabled={isLoading}
|
|
@@ -68,12 +68,19 @@ export const ChatInfo = ({ children }: { children: React.ReactNode }) => (
|
|
|
68
68
|
);
|
|
69
69
|
|
|
70
70
|
// Chat name and last message display
|
|
71
|
+
// One line, ellipsised. Without this a long room title (or a display name
|
|
72
|
+
// that happens to be an email) wrapped to three or four lines and pushed
|
|
73
|
+
// the whole row's height around.
|
|
71
74
|
export const ChatName = ({ text }: { text: string }) => (
|
|
72
|
-
<Text style={styles.chatName}
|
|
75
|
+
<Text style={styles.chatName} numberOfLines={1} ellipsizeMode="tail">
|
|
76
|
+
{text}
|
|
77
|
+
</Text>
|
|
73
78
|
);
|
|
74
79
|
|
|
75
80
|
export const LastMessage = ({ children }: { children: React.ReactNode }) => (
|
|
76
|
-
<Text style={styles.lastMessage}
|
|
81
|
+
<Text style={styles.lastMessage} numberOfLines={1} ellipsizeMode="tail">
|
|
82
|
+
{children}
|
|
83
|
+
</Text>
|
|
77
84
|
);
|
|
78
85
|
|
|
79
86
|
// User count display for the chat
|
|
@@ -145,7 +152,12 @@ const styles = StyleSheet.create({
|
|
|
145
152
|
flexDirection: 'column',
|
|
146
153
|
},
|
|
147
154
|
chatInfo: {
|
|
148
|
-
|
|
155
|
+
// `flexShrink` + `minWidth: 0` is what actually lets the child Text
|
|
156
|
+
// ellipsise: without them the row keeps its intrinsic width and the
|
|
157
|
+
// text wraps instead of truncating, whatever numberOfLines says.
|
|
158
|
+
flexShrink: 1,
|
|
159
|
+
minWidth: 0,
|
|
160
|
+
maxWidth: '80%',
|
|
149
161
|
},
|
|
150
162
|
chatName: {
|
|
151
163
|
fontWeight: 'bold',
|