@ethora/chat-component-rn 26.10.5 → 26.10.7
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 +10 -0
- package/README.md +18 -0
- package/app.plugin.js +2 -0
- package/lib/commonjs/assets/icons.js +2 -2
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/AlbumGrid.js +152 -0
- package/lib/commonjs/components/MainComponents/AlbumGrid.js.map +1 -0
- package/lib/commonjs/components/MainComponents/ChatRoom.js +2 -2
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +199 -123
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaUploadOverlay.js +109 -0
- package/lib/commonjs/components/MainComponents/MediaUploadOverlay.js.map +1 -0
- package/lib/commonjs/components/MessageBubble/MediaCaption.js +51 -0
- package/lib/commonjs/components/MessageBubble/MediaCaption.js.map +1 -0
- package/lib/commonjs/components/MessageBubble/Message.js +40 -1
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js +2 -1
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +75 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/LastMessageItem.js +20 -4
- package/lib/commonjs/components/RoomComponents/LastMessageItem.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js +3 -2
- package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +3 -2
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +3 -2
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +3 -2
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
- package/lib/commonjs/components/Thread/ThreadWrapper.js +3 -2
- package/lib/commonjs/components/Thread/ThreadWrapper.js.map +1 -1
- package/lib/commonjs/components/styled/MediaFilePreview.js +60 -47
- package/lib/commonjs/components/styled/MediaFilePreview.js.map +1 -1
- package/lib/commonjs/components/styled/MessageImage.js +16 -6
- package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +99 -68
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +24 -10
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +46 -0
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/e2ee/fileEnvelope.js +6 -0
- package/lib/commonjs/e2ee/fileEnvelope.js.map +1 -1
- package/lib/commonjs/e2ee/sealedBody.js +32 -2
- package/lib/commonjs/e2ee/sealedBody.js.map +1 -1
- package/lib/commonjs/helpers/albumLayout.js +150 -0
- package/lib/commonjs/helpers/albumLayout.js.map +1 -0
- package/lib/commonjs/helpers/attachments.js +255 -0
- package/lib/commonjs/helpers/attachments.js.map +1 -0
- package/lib/commonjs/helpers/caption.js +214 -0
- package/lib/commonjs/helpers/caption.js.map +1 -0
- package/lib/commonjs/helpers/createMainMessageForThread.js +5 -1
- package/lib/commonjs/helpers/createMainMessageForThread.js.map +1 -1
- package/lib/commonjs/helpers/createMessageFromXml.js +19 -5
- package/lib/commonjs/helpers/createMessageFromXml.js.map +1 -1
- package/lib/commonjs/helpers/fileKind.js +40 -0
- package/lib/commonjs/helpers/fileKind.js.map +1 -0
- package/lib/commonjs/helpers/getDataFromXml.js +6 -1
- package/lib/commonjs/helpers/getDataFromXml.js.map +1 -1
- package/lib/commonjs/helpers/mapWithConcurrency.js +41 -0
- package/lib/commonjs/helpers/mapWithConcurrency.js.map +1 -0
- package/lib/commonjs/helpers/mediaLimits.js +33 -0
- package/lib/commonjs/helpers/mediaLimits.js.map +1 -0
- package/lib/commonjs/helpers/mediaSendRetry.js +64 -0
- package/lib/commonjs/helpers/mediaSendRetry.js.map +1 -0
- package/lib/commonjs/helpers/mediaWireData.js +62 -0
- package/lib/commonjs/helpers/mediaWireData.js.map +1 -0
- package/lib/commonjs/helpers/mentionSpans.js +19 -0
- package/lib/commonjs/helpers/mentionSpans.js.map +1 -0
- package/lib/commonjs/helpers/mimeToExtension.js +45 -0
- package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
- package/lib/commonjs/helpers/readReceipts.js +173 -0
- package/lib/commonjs/helpers/readReceipts.js.map +1 -0
- package/lib/commonjs/helpers/repairFilenameEncoding.js +79 -0
- package/lib/commonjs/helpers/repairFilenameEncoding.js.map +1 -0
- package/lib/commonjs/helpers/safeJson.js +18 -0
- package/lib/commonjs/helpers/safeJson.js.map +1 -0
- package/lib/commonjs/helpers/secureStoreRuntime.js +10 -1
- package/lib/commonjs/helpers/secureStoreRuntime.js.map +1 -1
- package/lib/commonjs/hooks/useSendMessage.js +384 -193
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/i18n/strings.js +96 -0
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/main.js +81 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/networking/api-requests/auth.api.js +62 -3
- package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
- package/lib/commonjs/networking/api-requests/push.api.js +22 -7
- package/lib/commonjs/networking/api-requests/push.api.js.map +1 -1
- package/lib/commonjs/networking/stanzaHandlers.js +100 -3
- package/lib/commonjs/networking/stanzaHandlers.js.map +1 -1
- package/lib/commonjs/networking/xmpp/handleStanzas.xmpp.js +4 -2
- package/lib/commonjs/networking/xmpp/handleStanzas.xmpp.js.map +1 -1
- package/lib/commonjs/networking/xmpp/sendDisplayedMarker.xmpp.js +28 -0
- package/lib/commonjs/networking/xmpp/sendDisplayedMarker.xmpp.js.map +1 -0
- package/lib/commonjs/networking/xmpp/sendMediaMessage.xmpp.js +53 -2
- package/lib/commonjs/networking/xmpp/sendMediaMessage.xmpp.js.map +1 -1
- package/lib/commonjs/networking/xmpp/sendReceivedReceipt.xmpp.js +28 -0
- package/lib/commonjs/networking/xmpp/sendReceivedReceipt.xmpp.js.map +1 -0
- package/lib/commonjs/networking/xmppClient.js +64 -0
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/push/pushKeyStore.js +169 -0
- package/lib/commonjs/push/pushKeyStore.js.map +1 -0
- package/lib/commonjs/push/sealedBox.js +106 -0
- package/lib/commonjs/push/sealedBox.js.map +1 -0
- package/lib/commonjs/push/sealedPush.js +124 -0
- package/lib/commonjs/push/sealedPush.js.map +1 -0
- package/lib/commonjs/push/sealedPushTask.js +195 -0
- package/lib/commonjs/push/sealedPushTask.js.map +1 -0
- package/lib/commonjs/roomStore/persistence.js +78 -5
- package/lib/commonjs/roomStore/persistence.js.map +1 -1
- package/lib/commonjs/roomStore/roomHeapSlice.js +4 -0
- package/lib/commonjs/roomStore/roomHeapSlice.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +114 -1
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/commonjs/services/pushRegistration.js +12 -7
- package/lib/commonjs/services/pushRegistration.js.map +1 -1
- package/lib/commonjs/types/models/attachment.model.js +2 -0
- package/lib/commonjs/types/models/attachment.model.js.map +1 -0
- package/lib/commonjs/version.js +1 -1
- package/lib/module/assets/icons.js +1 -1
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/MainComponents/AlbumGrid.js +146 -0
- package/lib/module/components/MainComponents/AlbumGrid.js.map +1 -0
- package/lib/module/components/MainComponents/ChatRoom.js +2 -2
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +200 -124
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/MediaUploadOverlay.js +104 -0
- package/lib/module/components/MainComponents/MediaUploadOverlay.js.map +1 -0
- package/lib/module/components/MessageBubble/MediaCaption.js +46 -0
- package/lib/module/components/MessageBubble/MediaCaption.js.map +1 -0
- package/lib/module/components/MessageBubble/Message.js +44 -5
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/MessageBubble/MessageInteractions.js +2 -1
- package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +75 -0
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/RoomComponents/LastMessageItem.js +20 -4
- package/lib/module/components/RoomComponents/LastMessageItem.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastAudioMessage.js +3 -2
- package/lib/module/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js +3 -2
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +3 -2
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +3 -2
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
- package/lib/module/components/Thread/ThreadWrapper.js +3 -2
- package/lib/module/components/Thread/ThreadWrapper.js.map +1 -1
- package/lib/module/components/styled/MediaFilePreview.js +61 -48
- package/lib/module/components/styled/MediaFilePreview.js.map +1 -1
- package/lib/module/components/styled/MessageImage.js +16 -6
- package/lib/module/components/styled/MessageImage.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +99 -68
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +24 -10
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/xmppProvider.js +46 -0
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/e2ee/fileEnvelope.js +1 -1
- package/lib/module/e2ee/fileEnvelope.js.map +1 -1
- package/lib/module/e2ee/sealedBody.js +32 -2
- package/lib/module/e2ee/sealedBody.js.map +1 -1
- package/lib/module/helpers/albumLayout.js +143 -0
- package/lib/module/helpers/albumLayout.js.map +1 -0
- package/lib/module/helpers/attachments.js +245 -0
- package/lib/module/helpers/attachments.js.map +1 -0
- package/lib/module/helpers/caption.js +207 -0
- package/lib/module/helpers/caption.js.map +1 -0
- package/lib/module/helpers/createMainMessageForThread.js +5 -1
- package/lib/module/helpers/createMainMessageForThread.js.map +1 -1
- package/lib/module/helpers/createMessageFromXml.js +20 -6
- package/lib/module/helpers/createMessageFromXml.js.map +1 -1
- package/lib/module/helpers/fileKind.js +34 -0
- package/lib/module/helpers/fileKind.js.map +1 -0
- package/lib/module/helpers/getDataFromXml.js +6 -1
- package/lib/module/helpers/getDataFromXml.js.map +1 -1
- package/lib/module/helpers/mapWithConcurrency.js +37 -0
- package/lib/module/helpers/mapWithConcurrency.js.map +1 -0
- package/lib/module/helpers/mediaLimits.js +27 -0
- package/lib/module/helpers/mediaLimits.js.map +1 -0
- package/lib/module/helpers/mediaSendRetry.js +55 -0
- package/lib/module/helpers/mediaSendRetry.js.map +1 -0
- package/lib/module/helpers/mediaWireData.js +58 -0
- package/lib/module/helpers/mediaWireData.js.map +1 -0
- package/lib/module/helpers/mentionSpans.js +14 -0
- package/lib/module/helpers/mentionSpans.js.map +1 -0
- package/lib/module/helpers/mimeToExtension.js +43 -0
- package/lib/module/helpers/mimeToExtension.js.map +1 -1
- package/lib/module/helpers/readReceipts.js +160 -0
- package/lib/module/helpers/readReceipts.js.map +1 -0
- package/lib/module/helpers/repairFilenameEncoding.js +74 -0
- package/lib/module/helpers/repairFilenameEncoding.js.map +1 -0
- package/lib/module/helpers/safeJson.js +13 -0
- package/lib/module/helpers/safeJson.js.map +1 -0
- package/lib/module/helpers/secureStoreRuntime.js +10 -1
- package/lib/module/helpers/secureStoreRuntime.js.map +1 -1
- package/lib/module/hooks/useSendMessage.js +387 -195
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/i18n/strings.js +96 -0
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/main.js +6 -0
- package/lib/module/main.js.map +1 -1
- package/lib/module/networking/api-requests/auth.api.js +62 -3
- package/lib/module/networking/api-requests/auth.api.js.map +1 -1
- package/lib/module/networking/api-requests/push.api.js +21 -6
- package/lib/module/networking/api-requests/push.api.js.map +1 -1
- package/lib/module/networking/stanzaHandlers.js +97 -4
- package/lib/module/networking/stanzaHandlers.js.map +1 -1
- package/lib/module/networking/xmpp/handleStanzas.xmpp.js +5 -3
- package/lib/module/networking/xmpp/handleStanzas.xmpp.js.map +1 -1
- package/lib/module/networking/xmpp/sendDisplayedMarker.xmpp.js +25 -0
- package/lib/module/networking/xmpp/sendDisplayedMarker.xmpp.js.map +1 -0
- package/lib/module/networking/xmpp/sendMediaMessage.xmpp.js +53 -2
- package/lib/module/networking/xmpp/sendMediaMessage.xmpp.js.map +1 -1
- package/lib/module/networking/xmpp/sendReceivedReceipt.xmpp.js +25 -0
- package/lib/module/networking/xmpp/sendReceivedReceipt.xmpp.js.map +1 -0
- package/lib/module/networking/xmppClient.js +64 -0
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/push/pushKeyStore.js +159 -0
- package/lib/module/push/pushKeyStore.js.map +1 -0
- package/lib/module/push/sealedBox.js +97 -0
- package/lib/module/push/sealedBox.js.map +1 -0
- package/lib/module/push/sealedPush.js +117 -0
- package/lib/module/push/sealedPush.js.map +1 -0
- package/lib/module/push/sealedPushTask.js +187 -0
- package/lib/module/push/sealedPushTask.js.map +1 -0
- package/lib/module/roomStore/persistence.js +78 -5
- package/lib/module/roomStore/persistence.js.map +1 -1
- package/lib/module/roomStore/roomHeapSlice.js +4 -0
- package/lib/module/roomStore/roomHeapSlice.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +109 -0
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/services/pushRegistration.js +12 -7
- package/lib/module/services/pushRegistration.js.map +1 -1
- package/lib/module/types/models/attachment.model.js +2 -0
- package/lib/module/types/models/attachment.model.js.map +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/assets/icons.d.ts +6 -1
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/AlbumGrid.d.ts +21 -0
- package/lib/typescript/components/MainComponents/AlbumGrid.d.ts.map +1 -0
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaUploadOverlay.d.ts +17 -0
- package/lib/typescript/components/MainComponents/MediaUploadOverlay.d.ts.map +1 -0
- package/lib/typescript/components/MessageBubble/MediaCaption.d.ts +12 -0
- package/lib/typescript/components/MessageBubble/MediaCaption.d.ts.map +1 -0
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/LastMessageItem.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastAudioMessage.d.ts +2 -0
- package/lib/typescript/components/RoomComponents/styled/LastAudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts +2 -0
- package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts +2 -0
- package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts +2 -0
- package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
- package/lib/typescript/components/Thread/ThreadWrapper.d.ts.map +1 -1
- package/lib/typescript/components/styled/MediaFilePreview.d.ts.map +1 -1
- package/lib/typescript/components/styled/MessageImage.d.ts +10 -0
- package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts +7 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/components/styled/VideoMessage.d.ts +7 -0
- package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/e2ee/fileEnvelope.d.ts +1 -1
- package/lib/typescript/e2ee/fileEnvelope.d.ts.map +1 -1
- package/lib/typescript/e2ee/sealedBody.d.ts +16 -0
- package/lib/typescript/e2ee/sealedBody.d.ts.map +1 -1
- package/lib/typescript/helpers/albumLayout.d.ts +66 -0
- package/lib/typescript/helpers/albumLayout.d.ts.map +1 -0
- package/lib/typescript/helpers/attachments.d.ts +55 -0
- package/lib/typescript/helpers/attachments.d.ts.map +1 -0
- package/lib/typescript/helpers/caption.d.ts +64 -0
- package/lib/typescript/helpers/caption.d.ts.map +1 -0
- package/lib/typescript/helpers/createMainMessageForThread.d.ts.map +1 -1
- package/lib/typescript/helpers/createMessageFromXml.d.ts.map +1 -1
- package/lib/typescript/helpers/fileKind.d.ts +20 -0
- package/lib/typescript/helpers/fileKind.d.ts.map +1 -0
- package/lib/typescript/helpers/getDataFromXml.d.ts.map +1 -1
- package/lib/typescript/helpers/mapWithConcurrency.d.ts +9 -0
- package/lib/typescript/helpers/mapWithConcurrency.d.ts.map +1 -0
- package/lib/typescript/helpers/mediaLimits.d.ts +13 -0
- package/lib/typescript/helpers/mediaLimits.d.ts.map +1 -0
- package/lib/typescript/helpers/mediaSendRetry.d.ts +26 -0
- package/lib/typescript/helpers/mediaSendRetry.d.ts.map +1 -0
- package/lib/typescript/helpers/mediaWireData.d.ts +16 -0
- package/lib/typescript/helpers/mediaWireData.d.ts.map +1 -0
- package/lib/typescript/helpers/mentionSpans.d.ts +8 -0
- package/lib/typescript/helpers/mentionSpans.d.ts.map +1 -0
- package/lib/typescript/helpers/mimeToExtension.d.ts +7 -0
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
- package/lib/typescript/helpers/readReceipts.d.ts +82 -0
- package/lib/typescript/helpers/readReceipts.d.ts.map +1 -0
- package/lib/typescript/helpers/repairFilenameEncoding.d.ts +12 -0
- package/lib/typescript/helpers/repairFilenameEncoding.d.ts.map +1 -0
- package/lib/typescript/helpers/safeJson.d.ts +2 -0
- package/lib/typescript/helpers/safeJson.d.ts.map +1 -0
- package/lib/typescript/helpers/secureStoreRuntime.d.ts +17 -3
- package/lib/typescript/helpers/secureStoreRuntime.d.ts.map +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts +2 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +6 -0
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/auth.api.d.ts +2 -0
- package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/push.api.d.ts +14 -2
- package/lib/typescript/networking/api-requests/push.api.d.ts.map +1 -1
- package/lib/typescript/networking/stanzaHandlers.d.ts +7 -3
- 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/sendDisplayedMarker.xmpp.d.ts +3 -0
- package/lib/typescript/networking/xmpp/sendDisplayedMarker.xmpp.d.ts.map +1 -0
- package/lib/typescript/networking/xmpp/sendMediaMessage.xmpp.d.ts +12 -0
- package/lib/typescript/networking/xmpp/sendMediaMessage.xmpp.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/sendReceivedReceipt.xmpp.d.ts +3 -0
- package/lib/typescript/networking/xmpp/sendReceivedReceipt.xmpp.d.ts.map +1 -0
- package/lib/typescript/networking/xmppClient.d.ts +15 -0
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/push/pushKeyStore.d.ts +35 -0
- package/lib/typescript/push/pushKeyStore.d.ts.map +1 -0
- package/lib/typescript/push/sealedBox.d.ts +24 -0
- package/lib/typescript/push/sealedBox.d.ts.map +1 -0
- package/lib/typescript/push/sealedPush.d.ts +61 -0
- package/lib/typescript/push/sealedPush.d.ts.map +1 -0
- package/lib/typescript/push/sealedPushTask.d.ts +33 -0
- package/lib/typescript/push/sealedPushTask.d.ts.map +1 -0
- package/lib/typescript/roomStore/persistence.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomHeapSlice.d.ts +22 -1
- package/lib/typescript/roomStore/roomHeapSlice.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +67 -3
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/services/pushRegistration.d.ts +8 -0
- package/lib/typescript/services/pushRegistration.d.ts.map +1 -1
- package/lib/typescript/types/models/attachment.model.d.ts +52 -0
- package/lib/typescript/types/models/attachment.model.d.ts.map +1 -0
- package/lib/typescript/types/models/message.model.d.ts +26 -0
- package/lib/typescript/types/models/message.model.d.ts.map +1 -1
- package/lib/typescript/types/models/modal.model.d.ts +7 -0
- package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
- package/lib/typescript/types/models/room.model.d.ts +2 -0
- package/lib/typescript/types/models/room.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +35 -1
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +1 -1
- package/package.json +14 -1
- package/plugins/notification-service/Info.plist +31 -0
- package/plugins/notification-service/NotificationService.swift +195 -0
- package/plugins/notification-service/SealedBox.swift +355 -0
- package/plugins/notification-service/check/gen-vectors.mjs +24 -0
- package/plugins/notification-service/check/main.swift +37 -0
- package/plugins/notification-service/check/run.sh +12 -0
- package/plugins/notification-service/check/vectors.json +1 -0
- package/plugins/withNotificationServiceExtension.js +189 -0
- package/src/assets/icons.tsx +20 -2
- package/src/components/MainComponents/AlbumGrid.tsx +183 -0
- package/src/components/MainComponents/ChatRoom.tsx +4 -2
- package/src/components/MainComponents/MediaMessage.tsx +246 -130
- package/src/components/MainComponents/MediaUploadOverlay.tsx +102 -0
- package/src/components/MessageBubble/MediaCaption.tsx +45 -0
- package/src/components/MessageBubble/Message.tsx +71 -3
- package/src/components/MessageBubble/MessageInteractions.tsx +2 -1
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +64 -0
- package/src/components/RoomComponents/LastMessageItem.tsx +15 -4
- package/src/components/RoomComponents/styled/LastAudioMessage.tsx +6 -3
- package/src/components/RoomComponents/styled/LastMessageFile.tsx +6 -2
- package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +6 -2
- package/src/components/RoomComponents/styled/LastMessageVideo.tsx +6 -2
- package/src/components/Thread/ThreadWrapper.tsx +5 -9
- package/src/components/styled/MediaFilePreview.tsx +49 -34
- package/src/components/styled/MessageImage.tsx +21 -5
- package/src/components/styled/SendInput.tsx +101 -87
- package/src/components/styled/VideoMessage.tsx +34 -9
- package/src/context/xmppProvider.tsx +32 -0
- package/src/e2ee/fileEnvelope.ts +1 -1
- package/src/e2ee/sealedBody.ts +44 -5
- package/src/helpers/albumLayout.ts +207 -0
- package/src/helpers/attachments.ts +316 -0
- package/src/helpers/caption.ts +283 -0
- package/src/helpers/createMainMessageForThread.ts +6 -1
- package/src/helpers/createMessageFromXml.ts +21 -5
- package/src/helpers/fileKind.ts +38 -0
- package/src/helpers/getDataFromXml.ts +5 -1
- package/src/helpers/mapWithConcurrency.ts +32 -0
- package/src/helpers/mediaLimits.ts +27 -0
- package/src/helpers/mediaSendRetry.ts +59 -0
- package/src/helpers/mediaWireData.ts +57 -0
- package/src/helpers/mentionSpans.ts +17 -0
- package/src/helpers/mimeToExtension.ts +51 -0
- package/src/helpers/readReceipts.ts +219 -0
- package/src/helpers/repairFilenameEncoding.ts +82 -0
- package/src/helpers/safeJson.ts +11 -0
- package/src/helpers/secureStoreRuntime.ts +21 -3
- package/src/hooks/useSendMessage.tsx +448 -249
- package/src/i18n/strings.ts +96 -0
- package/src/main.ts +30 -0
- package/src/networking/api-requests/auth.api.ts +60 -3
- package/src/networking/api-requests/push.api.ts +29 -4
- package/src/networking/stanzaHandlers.ts +91 -2
- package/src/networking/xmpp/handleStanzas.xmpp.ts +6 -2
- package/src/networking/xmpp/sendDisplayedMarker.xmpp.ts +31 -0
- package/src/networking/xmpp/sendMediaMessage.xmpp.ts +60 -3
- package/src/networking/xmpp/sendReceivedReceipt.xmpp.ts +31 -0
- package/src/networking/xmppClient.ts +48 -0
- package/src/push/pushKeyStore.ts +163 -0
- package/src/push/sealedBox.ts +104 -0
- package/src/push/sealedPush.ts +127 -0
- package/src/push/sealedPushTask.ts +175 -0
- package/src/roomStore/persistence.ts +94 -3
- package/src/roomStore/roomHeapSlice.ts +23 -1
- package/src/roomStore/roomsSlice.ts +112 -1
- package/src/services/pushRegistration.ts +25 -5
- package/src/types/models/attachment.model.ts +51 -0
- package/src/types/models/message.model.ts +29 -0
- package/src/types/models/modal.model.ts +7 -0
- package/src/types/models/room.model.ts +2 -0
- package/src/types/types.ts +41 -1
- package/src/version.ts +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Client, xml } from '@xmpp/client';
|
|
2
2
|
import { sendSealedMedia, shouldEncrypt } from '../../e2ee/send';
|
|
3
|
+
import type { IAttachment, IMentionSpan } from '../../types/types';
|
|
4
|
+
import { serializeAttachments } from '../../helpers/attachments';
|
|
5
|
+
import { normalizeCaptionInput } from '../../helpers/caption';
|
|
3
6
|
|
|
4
7
|
// Monotonic counter — see sendTextMessage for the rationale (avoids
|
|
5
8
|
// Date.now() id collisions on <1ms-apart sends).
|
|
@@ -20,6 +23,18 @@ const toServiceXmlns = (devServer: string | undefined, client: Client): string =
|
|
|
20
23
|
return `wss://${ds.replace(/^\/+|\/+$/g, '')}/ws`;
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
/**
|
|
27
|
+
* One media stanza for one or more files.
|
|
28
|
+
*
|
|
29
|
+
* `data` carries the flat fields that describe attachment #0, so a client
|
|
30
|
+
* that predates multi-attach still shows the first file. With more than one
|
|
31
|
+
* file the whole list rides on `<data attachments='JSON'>` (selection order).
|
|
32
|
+
*
|
|
33
|
+
* A typed caption rides on the message too: as `caption` / `captionMentions`
|
|
34
|
+
* attributes in a plain room, and inside the sealed <body> in an e2ee room
|
|
35
|
+
* (never as an attribute there). A message with one file and no caption is
|
|
36
|
+
* byte-identical to what this function produced before multi-attach.
|
|
37
|
+
*/
|
|
23
38
|
export function sendMediaMessage(
|
|
24
39
|
client: Client,
|
|
25
40
|
roomJID: string,
|
|
@@ -30,7 +45,7 @@ export function sendMediaMessage(
|
|
|
30
45
|
const id =
|
|
31
46
|
customId || `send-media-message-${Date.now()}-${(_sendMediaSeq = (_sendMediaSeq + 1) >>> 0)}`;
|
|
32
47
|
|
|
33
|
-
const dataToSend = {
|
|
48
|
+
const dataToSend: Record<string, unknown> = {
|
|
34
49
|
xmlns: toServiceXmlns(devServer, client),
|
|
35
50
|
senderJID: client.jid?.toString(),
|
|
36
51
|
senderFirstName: data.firstName,
|
|
@@ -63,23 +78,65 @@ export function sendMediaMessage(
|
|
|
63
78
|
push: 'true',
|
|
64
79
|
};
|
|
65
80
|
|
|
81
|
+
// Only stamped when there is more than one file, so a single-file stanza
|
|
82
|
+
// keeps exactly the attributes it always had.
|
|
83
|
+
const files: IAttachment[] = Array.isArray(data?.attachments) ? data.attachments : [];
|
|
84
|
+
if (files.length > 1) {
|
|
85
|
+
// The flat fields describe attachment #0, whatever the caller passed, so
|
|
86
|
+
// an older client shows the first file of the message.
|
|
87
|
+
const head = files[0]!;
|
|
88
|
+
const flat: Array<keyof IAttachment> = [
|
|
89
|
+
'attachmentId',
|
|
90
|
+
'location',
|
|
91
|
+
'locationPreview',
|
|
92
|
+
'mimetype',
|
|
93
|
+
'originalName',
|
|
94
|
+
'fileName',
|
|
95
|
+
'size',
|
|
96
|
+
'duration',
|
|
97
|
+
];
|
|
98
|
+
for (const key of flat) {
|
|
99
|
+
const value = head[key];
|
|
100
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
101
|
+
dataToSend[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
dataToSend.attachments = serializeAttachments(files);
|
|
105
|
+
}
|
|
106
|
+
|
|
66
107
|
const keys: string[] | undefined = data?.e2eeKeys;
|
|
67
108
|
if (keys?.length) {
|
|
68
|
-
|
|
109
|
+
dataToSend.clientEncrypted = 'true';
|
|
69
110
|
}
|
|
111
|
+
|
|
112
|
+
const caption = normalizeCaptionInput({
|
|
113
|
+
text: typeof data?.caption === 'string' ? data.caption : '',
|
|
114
|
+
mentions: data?.captionMentions as IMentionSpan[] | undefined,
|
|
115
|
+
});
|
|
70
116
|
const hints = xml('store', { xmlns: 'urn:xmpp:hints' });
|
|
71
117
|
|
|
72
118
|
if (keys?.length && shouldEncrypt(roomJID)) {
|
|
119
|
+
// The caption is sealed with the keys. It is never copied onto <data>.
|
|
120
|
+
const sealedBody = caption
|
|
121
|
+
? { v: 1, keys, caption: caption.caption, ...(caption.mentions ? { captionMentions: caption.mentions } : {}) }
|
|
122
|
+
: { v: 1, keys };
|
|
73
123
|
sendSealedMedia(
|
|
74
124
|
client,
|
|
75
125
|
roomJID,
|
|
76
126
|
id,
|
|
77
|
-
xml('body', {}, JSON.stringify(
|
|
127
|
+
xml('body', {}, JSON.stringify(sealedBody)),
|
|
78
128
|
[xml('data', dataToSend), hints]
|
|
79
129
|
);
|
|
80
130
|
return id;
|
|
81
131
|
}
|
|
82
132
|
|
|
133
|
+
if (caption) {
|
|
134
|
+
dataToSend.caption = caption.caption;
|
|
135
|
+
if (caption.mentions) {
|
|
136
|
+
dataToSend.captionMentions = JSON.stringify(caption.mentions);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
83
140
|
const message = xml(
|
|
84
141
|
'message',
|
|
85
142
|
{
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Client, xml } from '@xmpp/client';
|
|
2
|
+
|
|
3
|
+
// XEP-0184 delivery receipt, broadcast to the room (mirrors
|
|
4
|
+
// sendMessageReaction.xmpp.ts) so every occupant can render tick state.
|
|
5
|
+
// The top-level stanza `from` gets rewritten to the room/nick by the MUC
|
|
6
|
+
// server, so the real xmppUsername travels in the <received> `from` attr.
|
|
7
|
+
export function sendReceivedReceipt(
|
|
8
|
+
client: Client,
|
|
9
|
+
messageId: string,
|
|
10
|
+
roomJid: string
|
|
11
|
+
) {
|
|
12
|
+
const id = `message-received:${Date.now().toString()}`;
|
|
13
|
+
|
|
14
|
+
const message = xml(
|
|
15
|
+
'message',
|
|
16
|
+
{
|
|
17
|
+
id,
|
|
18
|
+
type: 'groupchat',
|
|
19
|
+
from: client.jid?.toString(),
|
|
20
|
+
to: roomJid,
|
|
21
|
+
},
|
|
22
|
+
xml('received', {
|
|
23
|
+
xmlns: 'urn:xmpp:receipts',
|
|
24
|
+
id: messageId,
|
|
25
|
+
from: client.jid?.toString(),
|
|
26
|
+
}),
|
|
27
|
+
xml('store', { xmlns: 'urn:xmpp:hints' })
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
client.send(message);
|
|
31
|
+
}
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from './xmpp/getHistory.xmpp';
|
|
18
18
|
import { sendTextMessage } from './xmpp/sendTextMessage.xmpp';
|
|
19
19
|
import { sendMessageReaction } from './xmpp/sendMessageReaction.xmpp';
|
|
20
|
+
import { sendReceivedReceipt } from './xmpp/sendReceivedReceipt.xmpp';
|
|
21
|
+
import { sendDisplayedMarker } from './xmpp/sendDisplayedMarker.xmpp';
|
|
20
22
|
import { sendTextMessageWithTranslateTag } from './xmpp/sendTextMessageWithTranslateTag.xmpp';
|
|
21
23
|
import { deleteMessage } from './xmpp/deleteMessage.xmpp';
|
|
22
24
|
import { RoomJoinManager } from './xmpp/roomJoinSweep';
|
|
@@ -48,6 +50,8 @@ import {
|
|
|
48
50
|
|
|
49
51
|
// Canonical production XMPP WSS endpoint (standard wss/443, no port suffix).
|
|
50
52
|
const DEFAULT_DEV_SERVER = 'xmpp.chat.ethora.com';
|
|
53
|
+
// Upper bound on remembered XEP-0184 acks (oldest forgotten first).
|
|
54
|
+
const RECEIPT_ACKED_CAP = 2000;
|
|
51
55
|
|
|
52
56
|
// How long to wait for the old client's stop() during a reconnect before
|
|
53
57
|
// giving up on it. Short on purpose: we are already tearing this client
|
|
@@ -279,6 +283,13 @@ export class XmppClient {
|
|
|
279
283
|
}
|
|
280
284
|
disableLastRead = false;
|
|
281
285
|
private activeRoomJID: string | null = null;
|
|
286
|
+
// XEP-0184 acks already sent, as `room:messageId`. A message is acked once,
|
|
287
|
+
// whether it arrived live or again inside a MAM page. Oldest entries drop
|
|
288
|
+
// first once the set passes the cap.
|
|
289
|
+
private receiptAckedIds: Set<string> = new Set();
|
|
290
|
+
// XEP-0333: the last read watermark sent per room. A repeat flush for the
|
|
291
|
+
// same newest message sends nothing.
|
|
292
|
+
private lastWatermarkSentByRoom: Map<string, string> = new Map();
|
|
282
293
|
private activeRoomBoostUntil = 0;
|
|
283
294
|
private softPauseUntil = 0;
|
|
284
295
|
private mamInFlightByRoom: Map<string, MamInFlightEntry> = new Map();
|
|
@@ -1594,6 +1605,43 @@ export class XmppClient {
|
|
|
1594
1605
|
return typeof result === 'string' ? result : '';
|
|
1595
1606
|
}
|
|
1596
1607
|
|
|
1608
|
+
/**
|
|
1609
|
+
* XEP-0184 delivery receipt for a message from someone else. Sent for live
|
|
1610
|
+
* messages and for messages a MAM page brings in; a message already acked
|
|
1611
|
+
* is skipped. When the stream is down (or `config.disableReadReceipts` is
|
|
1612
|
+
* on) nothing is recorded, so a later page can still ack it.
|
|
1613
|
+
*/
|
|
1614
|
+
sendReceivedReceiptStanza(messageId: string, roomJid: string) {
|
|
1615
|
+
// Privacy switch: with read receipts off this client stays silent, so
|
|
1616
|
+
// nobody learns that (or when) it received their message.
|
|
1617
|
+
if (store.getState().chatSettingStore.config?.disableReadReceipts) {return;}
|
|
1618
|
+
if (!messageId || !roomJid || !this.isStreamReady()) {return;}
|
|
1619
|
+
const jid = normalizeRoomJid(roomJid, this.conference);
|
|
1620
|
+
const key = `${jid}:${messageId}`;
|
|
1621
|
+
if (this.receiptAckedIds.has(key)) {return;}
|
|
1622
|
+
this.receiptAckedIds.add(key);
|
|
1623
|
+
if (this.receiptAckedIds.size > RECEIPT_ACKED_CAP) {
|
|
1624
|
+
const oldest = this.receiptAckedIds.values().next().value;
|
|
1625
|
+
if (oldest !== undefined) {this.receiptAckedIds.delete(oldest);}
|
|
1626
|
+
}
|
|
1627
|
+
sendReceivedReceipt(this.client, messageId, jid);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* XEP-0333 read watermark for a room: the newest message the user has
|
|
1632
|
+
* displayed. Off with `disableLastRead` or `config.disableReadReceipts`.
|
|
1633
|
+
* The caller decides when the room counts as read (see computeReadWatermark).
|
|
1634
|
+
*/
|
|
1635
|
+
sendDisplayedMarkerStanza(messageId: string, roomJid: string) {
|
|
1636
|
+
if (this.disableLastRead) {return;}
|
|
1637
|
+
if (store.getState().chatSettingStore.config?.disableReadReceipts) {return;}
|
|
1638
|
+
if (!messageId || !roomJid || !this.isStreamReady()) {return;}
|
|
1639
|
+
const jid = normalizeRoomJid(roomJid, this.conference);
|
|
1640
|
+
if (this.lastWatermarkSentByRoom.get(jid) === messageId) {return;}
|
|
1641
|
+
this.lastWatermarkSentByRoom.set(jid, messageId);
|
|
1642
|
+
sendDisplayedMarker(this.client, messageId, jid);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1597
1645
|
sendMessageReactionStanza(
|
|
1598
1646
|
messageId: string,
|
|
1599
1647
|
roomJid: string,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { fromBase64, toBase64 } from '../e2ee/crypto';
|
|
2
|
+
import { loadSecureStore, type SecureStoreOptions } from '../helpers/secureStoreRuntime';
|
|
3
|
+
import { pushLog } from '../utils/devLogger';
|
|
4
|
+
import {
|
|
5
|
+
SEALED_BOX_PUBLIC_KEY_BYTES,
|
|
6
|
+
SEALED_BOX_SECRET_KEY_BYTES,
|
|
7
|
+
generateSealedBoxKeyPair,
|
|
8
|
+
sealedBoxKeyId,
|
|
9
|
+
sealedBoxPublicKeyOf,
|
|
10
|
+
} from './sealedBox';
|
|
11
|
+
import type { SealedPushKey } from './sealedPush';
|
|
12
|
+
|
|
13
|
+
// The device key for encrypted pushes: one X25519 pair per install, in the
|
|
14
|
+
// platform keychain / keystore through expo-secure-store. The public half
|
|
15
|
+
// goes to the push service with the token (`pushKey`); the secret half
|
|
16
|
+
// never leaves the device and is read by whoever renders a sealed push —
|
|
17
|
+
// the Android background task (`sealedPushTask.ts`, same process, same
|
|
18
|
+
// store) and the iOS notification service extension, which reads the
|
|
19
|
+
// keychain item directly (plugins/notification-service), so the item's
|
|
20
|
+
// attributes below are a contract with the Swift side:
|
|
21
|
+
// service `ethora.push` (expo-secure-store stores it as `ethora.push:no-auth`)
|
|
22
|
+
// account `ethora_push_secret_key`, UTF-8
|
|
23
|
+
// value base64 of the 32-byte secret key, UTF-8
|
|
24
|
+
// group the app's first keychain access group, which the config
|
|
25
|
+
// plugin shares with the extension
|
|
26
|
+
// Readable after the first unlock so a push that arrives while the phone
|
|
27
|
+
// is locked still decrypts; this device only, so an iCloud keychain copy
|
|
28
|
+
// on another device is never a key for pushes that go to this one.
|
|
29
|
+
|
|
30
|
+
export const PUSH_KEY_KEYCHAIN_SERVICE = 'ethora.push';
|
|
31
|
+
export const PUSH_SECRET_KEY_ITEM = 'ethora_push_secret_key';
|
|
32
|
+
export const PUSH_PUBLIC_KEY_ITEM = 'ethora_push_public_key';
|
|
33
|
+
|
|
34
|
+
export interface PushKeyPair {
|
|
35
|
+
/** Base64 of the raw 32-byte X25519 public key — the `pushKey` of the registration. */
|
|
36
|
+
publicKey: string;
|
|
37
|
+
/** The `pushKeyId` the service derives for it, for logs and the kid check. */
|
|
38
|
+
keyId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let warnedMissingStore = false;
|
|
42
|
+
const warnMissingStoreOnce = () => {
|
|
43
|
+
if (warnedMissingStore) {return;}
|
|
44
|
+
warnedMissingStore = true;
|
|
45
|
+
pushLog(
|
|
46
|
+
'warn',
|
|
47
|
+
'push: expo-secure-store is not installed, so no device key for encrypted ' +
|
|
48
|
+
'pushes can be kept; pushes stay in the clear.'
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const storeOptions = (): SecureStoreOptions | null => {
|
|
53
|
+
const store = loadSecureStore();
|
|
54
|
+
if (!store) {return null;}
|
|
55
|
+
const accessible = store.accessible.afterFirstUnlockThisDeviceOnly;
|
|
56
|
+
return {
|
|
57
|
+
keychainService: PUSH_KEY_KEYCHAIN_SERVICE,
|
|
58
|
+
...(accessible !== undefined ? { keychainAccessible: accessible } : {}),
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const readItem = async (item: string): Promise<string | null> => {
|
|
63
|
+
const store = loadSecureStore();
|
|
64
|
+
const options = storeOptions();
|
|
65
|
+
if (!store || !options) {return null;}
|
|
66
|
+
try {
|
|
67
|
+
return await store.getItemAsync(item, options);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
pushLog('warn', `push: reading ${item} failed`, e);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const decodeKey = (raw: string | null, length: number): Uint8Array | null => {
|
|
75
|
+
if (!raw) {return null;}
|
|
76
|
+
try {
|
|
77
|
+
const bytes = fromBase64(raw);
|
|
78
|
+
return bytes.length === length ? bytes : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The stored pair, or null when there is none (or it is unreadable).
|
|
86
|
+
* The public key is re-derived from the secret, so the pair is consistent
|
|
87
|
+
* even if only the secret item survived.
|
|
88
|
+
*/
|
|
89
|
+
export async function loadPushSecretKey(): Promise<SealedPushKey | null> {
|
|
90
|
+
const secretKey = decodeKey(await readItem(PUSH_SECRET_KEY_ITEM), SEALED_BOX_SECRET_KEY_BYTES);
|
|
91
|
+
if (!secretKey) {return null;}
|
|
92
|
+
const stored = decodeKey(await readItem(PUSH_PUBLIC_KEY_ITEM), SEALED_BOX_PUBLIC_KEY_BYTES);
|
|
93
|
+
return { publicKey: stored ?? sealedBoxPublicKeyOf(secretKey), secretKey };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const describe = (publicKey: Uint8Array): PushKeyPair => ({
|
|
97
|
+
publicKey: toBase64(publicKey),
|
|
98
|
+
keyId: sealedBoxKeyId(publicKey),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** The public half of the stored pair, null when the device has no key. */
|
|
102
|
+
export async function getPushKeyPair(): Promise<PushKeyPair | null> {
|
|
103
|
+
const key = await loadPushSecretKey();
|
|
104
|
+
return key ? describe(key.publicKey) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate and store a new pair, replacing any existing one. Pushes sealed
|
|
109
|
+
* for the old key stop opening at once, so re-register the token with the
|
|
110
|
+
* new `publicKey` right after. Null when expo-secure-store is missing or
|
|
111
|
+
* the keychain refused the write.
|
|
112
|
+
*/
|
|
113
|
+
export async function resetPushKeyPair(): Promise<PushKeyPair | null> {
|
|
114
|
+
const store = loadSecureStore();
|
|
115
|
+
const options = storeOptions();
|
|
116
|
+
if (!store || !options) {
|
|
117
|
+
warnMissingStoreOnce();
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const pair = generateSealedBoxKeyPair();
|
|
121
|
+
try {
|
|
122
|
+
await store.setItemAsync(PUSH_SECRET_KEY_ITEM, toBase64(pair.secretKey), options);
|
|
123
|
+
await store.setItemAsync(PUSH_PUBLIC_KEY_ITEM, toBase64(pair.publicKey), options);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
pushLog('warn', 'push: storing the device key failed', e);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const check = await loadPushSecretKey();
|
|
129
|
+
if (!check || toBase64(check.publicKey) !== toBase64(pair.publicKey)) {
|
|
130
|
+
pushLog('warn', 'push: the device key was not kept');
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
pushLog('rn', `push: new device key ${sealedBoxKeyId(pair.publicKey)}`);
|
|
134
|
+
return describe(pair.publicKey);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The device key for encrypted pushes, created on first call. Hand
|
|
139
|
+
* `publicKey` to `registerPushToken` (or send it as `pushKey` yourself).
|
|
140
|
+
*/
|
|
141
|
+
export async function ensurePushKeyPair(): Promise<PushKeyPair | null> {
|
|
142
|
+
const existing = await getPushKeyPair();
|
|
143
|
+
return existing ?? resetPushKeyPair();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Remove the pair; the next `ensurePushKeyPair` creates a new one. */
|
|
147
|
+
export async function clearPushKeyPair(): Promise<void> {
|
|
148
|
+
const store = loadSecureStore();
|
|
149
|
+
const options = storeOptions();
|
|
150
|
+
if (!store || !options) {return;}
|
|
151
|
+
for (const item of [PUSH_SECRET_KEY_ITEM, PUSH_PUBLIC_KEY_ITEM]) {
|
|
152
|
+
try {
|
|
153
|
+
await store.deleteItemAsync(item, options);
|
|
154
|
+
} catch {
|
|
155
|
+
// Not there — nothing to remove.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Test seam only. */
|
|
161
|
+
export const __resetPushKeyStoreForTests = (): void => {
|
|
162
|
+
warnedMissingStore = false;
|
|
163
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { hsalsa, xsalsa20poly1305 } from '@noble/ciphers/salsa.js';
|
|
2
|
+
import { clean, concatBytes, u32, u8, utf8ToBytes } from '@noble/ciphers/utils.js';
|
|
3
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
4
|
+
import { blake2b } from '@noble/hashes/blake2.js';
|
|
5
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
6
|
+
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
7
|
+
|
|
8
|
+
// libsodium's sealed box (`crypto_box_seal` / `crypto_box_seal_open`) on
|
|
9
|
+
// the noble primitives the e2ee code already ships, so the JS side needs
|
|
10
|
+
// no native libsodium. The push service seals every chat push for a
|
|
11
|
+
// device that registered an X25519 public key (ethora-backend,
|
|
12
|
+
// services/push/README.md, "Encrypted pushes"); this is the matching
|
|
13
|
+
// receiver. The iOS notification service extension decrypts the same
|
|
14
|
+
// boxes in Swift with libsodium (plugins/notification-service).
|
|
15
|
+
//
|
|
16
|
+
// Layout: ephemeral public key (32) || crypto_box(message, nonce,
|
|
17
|
+
// recipient pk, ephemeral sk), with crypto_box = XSalsa20-Poly1305 under
|
|
18
|
+
// HSalsa20(X25519 shared secret) and nonce = BLAKE2b-192(epk || rpk).
|
|
19
|
+
// `__tests__/sealedBox.test.ts` pins this against libsodium itself.
|
|
20
|
+
|
|
21
|
+
export const SEALED_BOX_PUBLIC_KEY_BYTES = 32;
|
|
22
|
+
export const SEALED_BOX_SECRET_KEY_BYTES = 32;
|
|
23
|
+
/** Ephemeral public key (32) + Poly1305 tag (16). */
|
|
24
|
+
export const SEALED_BOX_OVERHEAD_BYTES = 48;
|
|
25
|
+
|
|
26
|
+
const NONCE_BYTES = 24;
|
|
27
|
+
const SIGMA = u32(utf8ToBytes('expand 32-byte k'));
|
|
28
|
+
const ZERO_NONCE_PREFIX = new Uint32Array(4);
|
|
29
|
+
|
|
30
|
+
export interface SealedBoxKeyPair {
|
|
31
|
+
publicKey: Uint8Array;
|
|
32
|
+
secretKey: Uint8Array;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const generateSealedBoxKeyPair = (): SealedBoxKeyPair => {
|
|
36
|
+
const { secretKey, publicKey } = x25519.keygen();
|
|
37
|
+
return { publicKey, secretKey };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const sealedBoxPublicKeyOf = (secretKey: Uint8Array): Uint8Array =>
|
|
41
|
+
x25519.getPublicKey(secretKey);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The key id the push service derives when the registration carries none:
|
|
45
|
+
* the first 16 hex characters of SHA-256 over the raw public key. Sent back
|
|
46
|
+
* as `kid` in every sealed push.
|
|
47
|
+
*/
|
|
48
|
+
export const sealedBoxKeyId = (publicKey: Uint8Array): string =>
|
|
49
|
+
bytesToHex(sha256(publicKey)).slice(0, 16);
|
|
50
|
+
|
|
51
|
+
// crypto_box_beforenm: HSalsa20 over the X25519 shared secret.
|
|
52
|
+
const boxSharedKey = (theirPublicKey: Uint8Array, mySecretKey: Uint8Array): Uint8Array => {
|
|
53
|
+
const shared = x25519.getSharedSecret(mySecretKey, theirPublicKey);
|
|
54
|
+
const words = new Uint32Array(8);
|
|
55
|
+
hsalsa(SIGMA, u32(shared), ZERO_NONCE_PREFIX, words);
|
|
56
|
+
clean(shared);
|
|
57
|
+
return u8(words);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const sealNonce = (ephemeralPublicKey: Uint8Array, recipientPublicKey: Uint8Array): Uint8Array =>
|
|
61
|
+
blake2b(concatBytes(ephemeralPublicKey, recipientPublicKey), { dkLen: NONCE_BYTES });
|
|
62
|
+
|
|
63
|
+
/** `crypto_box_seal`: used by the tests and the key tool, not by the app. */
|
|
64
|
+
export function sealedBoxSeal(message: Uint8Array, recipientPublicKey: Uint8Array): Uint8Array {
|
|
65
|
+
if (recipientPublicKey.length !== SEALED_BOX_PUBLIC_KEY_BYTES) {
|
|
66
|
+
throw new Error('sealedBoxSeal: the public key must be 32 bytes');
|
|
67
|
+
}
|
|
68
|
+
const ephemeral = generateSealedBoxKeyPair();
|
|
69
|
+
const key = boxSharedKey(recipientPublicKey, ephemeral.secretKey);
|
|
70
|
+
const nonce = sealNonce(ephemeral.publicKey, recipientPublicKey);
|
|
71
|
+
const box = xsalsa20poly1305(key, nonce).encrypt(message);
|
|
72
|
+
clean(key, ephemeral.secretKey);
|
|
73
|
+
return concatBytes(ephemeral.publicKey, box);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `crypto_box_seal_open`. Returns null for anything that does not open:
|
|
78
|
+
* a wrong key, a box for another key, a damaged or truncated box.
|
|
79
|
+
*/
|
|
80
|
+
export function sealedBoxOpen(
|
|
81
|
+
sealed: Uint8Array,
|
|
82
|
+
publicKey: Uint8Array,
|
|
83
|
+
secretKey: Uint8Array
|
|
84
|
+
): Uint8Array | null {
|
|
85
|
+
if (
|
|
86
|
+
sealed.length < SEALED_BOX_OVERHEAD_BYTES ||
|
|
87
|
+
publicKey.length !== SEALED_BOX_PUBLIC_KEY_BYTES ||
|
|
88
|
+
secretKey.length !== SEALED_BOX_SECRET_KEY_BYTES
|
|
89
|
+
) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const ephemeralPublicKey = sealed.subarray(0, SEALED_BOX_PUBLIC_KEY_BYTES);
|
|
93
|
+
const box = sealed.subarray(SEALED_BOX_PUBLIC_KEY_BYTES);
|
|
94
|
+
let key: Uint8Array | null = null;
|
|
95
|
+
try {
|
|
96
|
+
key = boxSharedKey(ephemeralPublicKey, secretKey);
|
|
97
|
+
const nonce = sealNonce(ephemeralPublicKey, publicKey);
|
|
98
|
+
return xsalsa20poly1305(key, nonce).decrypt(box);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
} finally {
|
|
102
|
+
if (key) {clean(key);}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { fromBase64 } from '../e2ee/crypto';
|
|
2
|
+
import { sealedBoxKeyId, sealedBoxOpen } from './sealedBox';
|
|
3
|
+
|
|
4
|
+
// The envelope of an encrypted chat push (ethora-backend,
|
|
5
|
+
// services/push/README.md, "Encrypted pushes"): the FCM `data` map on
|
|
6
|
+
// Android, the custom keys next to `aps` on iOS. Only ids travel in the
|
|
7
|
+
// clear; everything shown to the user is inside `enc`, a sealed box for
|
|
8
|
+
// the key the device registered with its token.
|
|
9
|
+
|
|
10
|
+
export const SEALED_PUSH_ALG = 'sealed-box-x25519';
|
|
11
|
+
export const SEALED_PUSH_VERSION = '1';
|
|
12
|
+
|
|
13
|
+
export interface SealedPushEnvelope {
|
|
14
|
+
v: string;
|
|
15
|
+
alg: string;
|
|
16
|
+
/** Id of the key the box was sealed for. */
|
|
17
|
+
kid: string;
|
|
18
|
+
/** Room JID. */
|
|
19
|
+
jid: string;
|
|
20
|
+
msgID: string;
|
|
21
|
+
/** Recipient. */
|
|
22
|
+
userJid: string;
|
|
23
|
+
/** Base64 of the sealed box; absent when the service could not seal. */
|
|
24
|
+
enc?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What the box holds: the notification the service would have sent in the clear. */
|
|
28
|
+
export interface SealedPushContent {
|
|
29
|
+
title: string;
|
|
30
|
+
body: string;
|
|
31
|
+
sender: string;
|
|
32
|
+
senderJid: string;
|
|
33
|
+
chat: string;
|
|
34
|
+
chatJid: string;
|
|
35
|
+
msgID: string;
|
|
36
|
+
/** Sender avatar URL, '' when none. */
|
|
37
|
+
image: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type SealedPushFailure =
|
|
41
|
+
/** No key on this device (never generated, or the store is not readable). */
|
|
42
|
+
| 'no-key'
|
|
43
|
+
/** Sealed for another key — the app was reinstalled or the key rotated after registration. */
|
|
44
|
+
| 'key-mismatch'
|
|
45
|
+
/** The service sent ids only (it could not seal). */
|
|
46
|
+
| 'no-ciphertext'
|
|
47
|
+
/** Not a box for our key, or damaged. */
|
|
48
|
+
| 'bad-ciphertext'
|
|
49
|
+
/** Opened, but the plaintext is not the expected JSON. */
|
|
50
|
+
| 'bad-content';
|
|
51
|
+
|
|
52
|
+
export type OpenSealedPushResult =
|
|
53
|
+
| { ok: true; content: SealedPushContent; envelope: SealedPushEnvelope }
|
|
54
|
+
| { ok: false; reason: SealedPushFailure; envelope: SealedPushEnvelope };
|
|
55
|
+
|
|
56
|
+
const str = (value: unknown): string => (typeof value === 'string' ? value : '');
|
|
57
|
+
|
|
58
|
+
/** The envelope of `data` when it is a sealed chat push, else null. */
|
|
59
|
+
export const sealedPushEnvelopeOf = (
|
|
60
|
+
data: Record<string, unknown> | null | undefined
|
|
61
|
+
): SealedPushEnvelope | null => {
|
|
62
|
+
if (!data || data.alg !== SEALED_PUSH_ALG) {return null;}
|
|
63
|
+
const enc = str(data.enc);
|
|
64
|
+
return {
|
|
65
|
+
v: str(data.v) || SEALED_PUSH_VERSION,
|
|
66
|
+
alg: SEALED_PUSH_ALG,
|
|
67
|
+
kid: str(data.kid),
|
|
68
|
+
jid: str(data.jid),
|
|
69
|
+
msgID: str(data.msgID),
|
|
70
|
+
userJid: str(data.userJid),
|
|
71
|
+
...(enc ? { enc } : {}),
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const isSealedPush = (data: Record<string, unknown> | null | undefined): boolean =>
|
|
76
|
+
sealedPushEnvelopeOf(data) !== null;
|
|
77
|
+
|
|
78
|
+
const parseContent = (plaintext: Uint8Array): SealedPushContent | null => {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(new TextDecoder().decode(plaintext));
|
|
81
|
+
if (!parsed || typeof parsed !== 'object') {return null;}
|
|
82
|
+
return {
|
|
83
|
+
title: str(parsed.title),
|
|
84
|
+
body: str(parsed.body),
|
|
85
|
+
sender: str(parsed.sender),
|
|
86
|
+
senderJid: str(parsed.senderJid),
|
|
87
|
+
chat: str(parsed.chat),
|
|
88
|
+
chatJid: str(parsed.chatJid),
|
|
89
|
+
msgID: str(parsed.msgID),
|
|
90
|
+
image: str(parsed.image),
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export interface SealedPushKey {
|
|
98
|
+
publicKey: Uint8Array;
|
|
99
|
+
secretKey: Uint8Array;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Open a sealed push with the device key. `key` null means there is no key
|
|
104
|
+
* on this device. Never throws: every failure names why, so the caller can
|
|
105
|
+
* show the placeholder and log the reason.
|
|
106
|
+
*/
|
|
107
|
+
export function openSealedPush(
|
|
108
|
+
envelope: SealedPushEnvelope,
|
|
109
|
+
key: SealedPushKey | null
|
|
110
|
+
): OpenSealedPushResult {
|
|
111
|
+
if (!key) {return { ok: false, reason: 'no-key', envelope };}
|
|
112
|
+
if (envelope.kid && envelope.kid !== sealedBoxKeyId(key.publicKey)) {
|
|
113
|
+
return { ok: false, reason: 'key-mismatch', envelope };
|
|
114
|
+
}
|
|
115
|
+
if (!envelope.enc) {return { ok: false, reason: 'no-ciphertext', envelope };}
|
|
116
|
+
let sealed: Uint8Array;
|
|
117
|
+
try {
|
|
118
|
+
sealed = fromBase64(envelope.enc);
|
|
119
|
+
} catch {
|
|
120
|
+
return { ok: false, reason: 'bad-ciphertext', envelope };
|
|
121
|
+
}
|
|
122
|
+
const plaintext = sealedBoxOpen(sealed, key.publicKey, key.secretKey);
|
|
123
|
+
if (!plaintext) {return { ok: false, reason: 'bad-ciphertext', envelope };}
|
|
124
|
+
const content = parseContent(plaintext);
|
|
125
|
+
if (!content) {return { ok: false, reason: 'bad-content', envelope };}
|
|
126
|
+
return { ok: true, content, envelope };
|
|
127
|
+
}
|