From e388d9ba42f119949d0d55dc2b635fec5679193d Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Tue, 25 Apr 2023 14:12:30 -0700 Subject: [PATCH 01/27] client side generation of thumbs for e2e based com --- net/web/package.json | 1 + .../conversation/addTopic/AddTopic.jsx | 12 ++-- .../conversation/addTopic/useAddTopic.hook.js | 70 +++++++++++++++++-- net/web/yarn.lock | 5 ++ 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/net/web/package.json b/net/web/package.json index c493c3b2..34beeaae 100644 --- a/net/web/package.json +++ b/net/web/package.json @@ -29,6 +29,7 @@ "react-dom": "^18.2.0", "react-easy-crop": "^4.1.4", "react-icons": "^4.8.0", + "react-image-file-resizer": "^0.4.8", "react-player": "^2.10.0", "react-resize-detector": "^7.0.0", "react-router-dom": "^6.2.2", diff --git a/net/web/src/session/conversation/addTopic/AddTopic.jsx b/net/web/src/session/conversation/addTopic/AddTopic.jsx index 52936eb4..9d1bc3f5 100644 --- a/net/web/src/session/conversation/addTopic/AddTopic.jsx +++ b/net/web/src/session/conversation/addTopic/AddTopic.jsx @@ -10,7 +10,7 @@ import { Carousel } from 'carousel/Carousel'; export function AddTopic({ contentKey }) { - const { state, actions } = useAddTopic(); + const { state, actions } = useAddTopic(contentKey); const [modal, modalContext] = Modal.useModal(); const attachImage = useRef(null); @@ -28,7 +28,7 @@ export function AddTopic({ contentKey }) { const addTopic = async () => { if (state.messageText || state.assets.length) { try { - await actions.addTopic(contentKey); + await actions.addTopic(); } catch (err) { console.log(err); @@ -108,22 +108,22 @@ export function AddTopic({ contentKey }) { value={state.messageText} autocapitalize="none" />
- { !contentKey && state.enableImage && ( + { state.enableImage && (
attachImage.current.click()}>
)} - { !contentKey && state.enableVideo && ( + { state.enableVideo && (
attachVideo.current.click()}>
)} - { !contentKey && state.enableAudio && ( + { state.enableAudio && (
attachAudio.current.click()}>
)} - { !contentKey && ( + { (state.enableImage || state.enableVideo || state.enableAudio) && (
)}
diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index e99b2a1d..5b00f46b 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -1,8 +1,9 @@ -import { useContext, useState, useEffect } from 'react'; +import { useContext, useState, useRef, useEffect } from 'react'; import { ConversationContext } from 'context/ConversationContext'; import { encryptTopicSubject } from 'context/sealUtil'; +import Resizer from "react-image-file-resizer"; -export function useAddTopic() { +export function useAddTopic(contentKey) { const [state, setState] = useState({ enableImage: null, @@ -18,6 +19,7 @@ export function useAddTopic() { }); const conversation = useContext(ConversationContext); + const objects = useRef([]); const updateState = (value) => { setState((s) => ({ ...s, ...value })); @@ -45,19 +47,33 @@ export function useAddTopic() { }); } + const clearObjects = () => { + objects.current.forEach(object => { + URL.revokeObjectURL(object); + }); + objects.current = []; + } + + useEffect(() => { + updateState({ assets: [] }); + return () => { console.log("RETURN CLEAR"); clearObjects() }; + }, [contentKey]); + useEffect(() => { const { enableImage, enableAudio, enableVideo } = conversation.state.channel?.data?.channelDetail || {}; updateState({ enableImage, enableAudio, enableVideo }); }, [conversation.state.channel?.data?.channelDetail]); const actions = { - addImage: (image) => { + addImage: async (image) => { let url = URL.createObjectURL(image); - addAsset({ image, url }) + addAsset({ image, url }); + objects.current.push(url); }, - addVideo: (video) => { + addVideo: async (video) => { let url = URL.createObjectURL(video); addAsset({ video, url, position: 0 }) + objects.current.push(url); }, addAudio: (audio) => { let url = URL.createObjectURL(audio); @@ -81,7 +97,7 @@ export function useAddTopic() { setTextSize: (value) => { updateState({ textSizeSet: true, textSize: value }); }, - addTopic: async (contentKey) => { + addTopic: async () => { if (!state.busy) { try { updateState({ busy: true }); @@ -119,6 +135,7 @@ export function useAddTopic() { await conversation.actions.addTopic(type, message, state.assets); updateState({ busy: false, messageText: null, textColor: '#444444', textColorSet: false, textSize: 12, textSizeSet: false, assets: [] }); + clearObjects(); } catch(err) { console.log(err); @@ -135,3 +152,44 @@ export function useAddTopic() { return { state, actions }; } +async function getImageThumb(url) { + return new Promise(resolve => { + Resizer.imageFileResizer(url, 192, 192, 'JPEG', 50, 0, + uri => { + resolve(uri); + }, 'base64', 128, 128 ); + }); +} + +async function getVideoThumb(url, pos) { + return new Promise((resolve, reject) => { + var video = document.createElement("video"); + var timeupdate = function (ev) { + video.removeEventListener("timeupdate", timeupdate); + video.pause(); + setTimeout(() => { + var canvas = document.createElement("canvas"); + if (video.videoWidth > video.videoHeight) { + canvas.width = 192; + canvas.height = Math.floor((192 * video.videoHeight / video.videoWidth)); + } + else { + canvas.height = 192; + canvas.width = Math.floor((192 * video.videoWidth / video.videoHeight)); + } + canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height); + var image = canvas.toDataURL("image/jpeg", 0.75); + resolve(image); + canvas.remove(); + video.remove(); + }, 1000); + }; + video.addEventListener("timeupdate", timeupdate); + video.preload = "metadata"; + video.src = url; + video.muted = true; + video.playsInline = true; + video.currentTime = pos; + video.play(); + }); +} diff --git a/net/web/yarn.lock b/net/web/yarn.lock index 76474821..267eb3eb 100644 --- a/net/web/yarn.lock +++ b/net/web/yarn.lock @@ -9139,6 +9139,11 @@ react-icons@^4.8.0: resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445" integrity sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg== +react-image-file-resizer@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz#85f4ae4469fd2867d961568af660ef403d7a79af" + integrity sha512-Ue7CfKnSlsfJ//SKzxNMz8avDgDSpWQDOnTKOp/GNRFJv4dO9L5YGHNEnj40peWkXXAK2OK0eRIoXhOYpUzUTQ== + react-is@^16.12.0, "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" From 7782ace9c425fbdf2914ff93408162de8c8dbd26 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Wed, 26 Apr 2023 13:05:55 -0700 Subject: [PATCH 02/27] encrypt and upload asset slices --- net/web/src/context/sealUtil.js | 21 ++++++ net/web/src/context/useUploadContext.hook.js | 38 ++++++++-- .../conversation/addTopic/useAddTopic.hook.js | 71 +++++++++++-------- 3 files changed, 96 insertions(+), 34 deletions(-) diff --git a/net/web/src/context/sealUtil.js b/net/web/src/context/sealUtil.js index 32f83157..5483a501 100644 --- a/net/web/src/context/sealUtil.js +++ b/net/web/src/context/sealUtil.js @@ -56,6 +56,27 @@ export function updateChannelSubject(subject, contentKey) { return { subjectEncrypted, subjectIv }; } +export function encryptBlock(block, contentKey) { + const key = CryptoJS.enc.Hex.parse(contentKey); + const iv = CryptoJS.lib.WordArray.random(128 / 8); + const encrypted = CryptoJS.AES.encrypt(block, key, { iv: iv }); + const blockEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64) + const blockIv = iv.toString(); + + return { blockEncrypted, blockIv }; +} + +export function decryptBlock(blockEncrypted, blockIv, contentKey) { + const iv = CryptoJS.enc.Hex.parse(blockIv); + const key = CryptoJS.enc.Hex.parse(contentKey); + const enc = CryptoJS.enc.Base64.parse(blockEncrypted); + const cipher = CryptoJS.lib.CipherParams.create({ ciphertext: enc, iv: iv }); + const dec = CryptoJS.AES.decrypt(cipher, key, { iv: iv }); + const block = dec.toString(CryptoJS.enc.Utf8); + + return block; +} + export function decryptChannelSubject(subject, contentKey) { const { subjectEncrypted, subjectIv } = JSON.parse(subject); const iv = CryptoJS.enc.Hex.parse(subjectIv); diff --git a/net/web/src/context/useUploadContext.hook.js b/net/web/src/context/useUploadContext.hook.js index 9f2174e5..d685a4b4 100644 --- a/net/web/src/context/useUploadContext.hook.js +++ b/net/web/src/context/useUploadContext.hook.js @@ -1,6 +1,8 @@ import { useState, useRef } from 'react'; import axios from 'axios'; +const ENCRYPTED_BLOCK_SIZE = (128 * 1024); //110k + export function useUploadContext() { const [state, setState] = useState({ @@ -69,7 +71,8 @@ export function useUploadContext() { const controller = new AbortController(); const entry = { index: index.current, - url: `${host}/content/channels/${channelId}/topics/${topicId}/assets?contact=${token}`, + baseUrl: `${host}/content/channels/${channelId}/topics/${topicId}/`, + urlParams: `?contact=${token}`, files, assets: [], current: null, @@ -91,7 +94,8 @@ export function useUploadContext() { const controller = new AbortController(); const entry = { index: index.current, - url: `/content/channels/${channelId}/topics/${topicId}/assets?agent=${token}`, + baseUrl: `/content/channels/${channelId}/topics/${topicId}/`, + urlParams: `?agent=${token}`, files, assets: [], current: null, @@ -154,11 +158,33 @@ async function upload(entry, update, complete) { const file = entry.files.shift(); entry.active = {}; try { - if (file.image) { + if (file.encrypted) { + const { size, getThumb, getEncryptedBlock, position } = file; + const type = file.image ? 'image' : file.video ? 'video' : file.audio ? 'audio' : ''; + const thumb = getThumb(position); + const parts = []; + for (let pos = 0; pos < size; pos += ENCRYPTED_BLOCK_SIZE) { + const { blockEncrypted, blockIv } = await getEncryptedBlock(pos, ENCRYPTED_BLOCK_SIZE); + const partId = await axios.post(`${entry.baseUrl}block${entry.urlParams}`, blockEncrypted, { + signal: entry.cancel.signal, + onUploadProgress: (ev) => { + const { loaded, total } = ev; + const partLoaded = pos + Math.floor(blockEncrypted.length * loaded / total); + entry.active = { partLoaded, size } + update(); + } + }); + parts.push({ blockIv, partId }); + } + entry.assets.push({ + encrypted: { type, thumb, parts } + }); + } + else if (file.image) { const formData = new FormData(); formData.append('asset', file.image); let transform = encodeURIComponent(JSON.stringify(["ithumb;photo", "ilg;photo"])); - let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, { signal: entry.cancel.signal, onUploadProgress: (ev) => { const { loaded, total } = ev; @@ -178,7 +204,7 @@ async function upload(entry, update, complete) { formData.append('asset', file.video); let thumb = 'vthumb;video;' + file.position; let transform = encodeURIComponent(JSON.stringify(["vlq;video", "vhd;video", thumb])); - let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, { signal: entry.cancel.signal, onUploadProgress: (ev) => { const { loaded, total } = ev; @@ -198,7 +224,7 @@ async function upload(entry, update, complete) { const formData = new FormData(); formData.append('asset', file.audio); let transform = encodeURIComponent(JSON.stringify(["acopy;audio"])); - let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, { signal: entry.cancel.signal, onUploadProgress: (ev) => { const { loaded, total } = ev; diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 5b00f46b..610364dd 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -1,6 +1,6 @@ import { useContext, useState, useRef, useEffect } from 'react'; import { ConversationContext } from 'context/ConversationContext'; -import { encryptTopicSubject } from 'context/sealUtil'; +import { encryptBlock, encryptTopicSubject } from 'context/sealUtil'; import Resizer from "react-image-file-resizer"; export function useAddTopic(contentKey) { @@ -64,20 +64,48 @@ export function useAddTopic(contentKey) { updateState({ enableImage, enableAudio, enableVideo }); }, [conversation.state.channel?.data?.channelDetail]); + const setUrl = async (url, getThumb) => { + if (contentKey) { + const buffer = await url.arrayBuffer(); + const getEncryptedBlock = (pos, len) => { + if (pos + len > buffer.byteLen) { + return null; + } + const block = btoa(String.fromCharCode.apply(null, buffer.slice(pos, len))); + return getEncryptedBlock(block, contentKey); + } + return { url, position: 0, label: '', encrypted: true, size: buffer.byteLength, getEncryptedBlock, getThumb }; + } + else { + return { url, position: 0, label: '', encrypted: false }; + } + } + const actions = { addImage: async (image) => { - let url = URL.createObjectURL(image); - addAsset({ image, url }); + const url = URL.createObjectURL(image); objects.current.push(url); + const getThumb = async () => { + return await getImageThumb(url); + } + const asset = setUrl(url, getThumb); + addAsset({ image, ...asset }); }, addVideo: async (video) => { - let url = URL.createObjectURL(video); - addAsset({ video, url, position: 0 }) + const url = URL.createObjectURL(video); objects.current.push(url); + const getThumb = async (position) => { + return await getVideoThumb(url, position); + } + const asset = setUrl(url, getThumb); + addAsset({ video, ...asset }); }, - addAudio: (audio) => { - let url = URL.createObjectURL(audio); - addAsset({ audio, url, label: '' }) + addAudio: async (audio) => { + const url = URL.createObjectURL(audio); + objects.current.push(url); + const getThumb = async () => { return null }; + const asset = setUrl(url, getThumb); + addAsset({ audio, ...asset }); }, setLabel: (index, label) => { updateAsset(index, { label }); @@ -103,32 +131,19 @@ export function useAddTopic(contentKey) { updateState({ busy: true }); const type = contentKey ? 'sealedtopic' : 'superbasictopic'; const message = (assets) => { - if (contentKey) { - if (assets?.length) { - console.log('assets not yet supported on sealed channels'); - } - const message = { + if (assets?.length) { + return { + assets, text: state.messageText, textColor: state.textColorSet ? state.textColor : null, textSize: state.textSizeSet ? state.textSize : null, } - return encryptTopicSubject({ message }, contentKey); } else { - if (assets?.length) { - return { - assets, - text: state.messageText, - textColor: state.textColorSet ? state.textColor : null, - textSize: state.textSizeSet ? state.textSize : null, - } - } - else { - return { - text: state.messageText, - textColor: state.textColorSet ? state.textColor : null, - textSize: state.textSizeSet ? state.textSize : null, - } + return { + text: state.messageText, + textColor: state.textColorSet ? state.textColor : null, + textSize: state.textSizeSet ? state.textSize : null, } } }; From ea349dffbfd0172fa201f13ae7209667816405d3 Mon Sep 17 00:00:00 2001 From: balzack Date: Wed, 26 Apr 2023 22:54:06 -0700 Subject: [PATCH 03/27] adding tools for client side thumbnail generation --- app/mobile/ios/Podfile.lock | 18 +++++++++++++ app/mobile/package.json | 3 +++ app/mobile/src/package.json | 5 +++- .../conversation/addTopic/useAddTopic.hook.js | 17 +++++++++++-- app/mobile/yarn.lock | 25 ++++++++++++++++++- net/container/Dockerfile | 2 +- 6 files changed, 65 insertions(+), 5 deletions(-) diff --git a/app/mobile/ios/Podfile.lock b/app/mobile/ios/Podfile.lock index c32f9a84..d726be19 100644 --- a/app/mobile/ios/Podfile.lock +++ b/app/mobile/ios/Podfile.lock @@ -321,8 +321,12 @@ PODS: - React-jsinspector (0.71.3) - React-logger (0.71.3): - glog + - react-native-create-thumbnail (1.6.4): + - React-Core - react-native-document-picker (8.1.3): - React-Core + - react-native-image-resizer (3.0.5): + - React-Core - react-native-keep-awake (1.1.0): - React-Core - react-native-receive-sharing-intent (2.0.0): @@ -445,6 +449,8 @@ PODS: - FirebaseCoreExtension (= 10.5.0) - React-Core - RNFBApp + - RNFS (2.20.0): + - React-Core - RNGestureHandler (2.9.0): - React-Core - RNImageCropPicker (0.39.0): @@ -516,7 +522,9 @@ DEPENDENCIES: - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - react-native-create-thumbnail (from `../node_modules/react-native-create-thumbnail`) - react-native-document-picker (from `../node_modules/react-native-document-picker`) + - "react-native-image-resizer (from `../node_modules/@bam.tech/react-native-image-resizer`)" - "react-native-keep-awake (from `../node_modules/@sayem314/react-native-keep-awake`)" - react-native-receive-sharing-intent (from `../node_modules/react-native-receive-sharing-intent`) - react-native-rsa-native (from `../node_modules/react-native-rsa-native`) @@ -543,6 +551,7 @@ DEPENDENCIES: - RNDeviceInfo (from `../node_modules/react-native-device-info`) - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" + - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -609,8 +618,12 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsinspector" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" + react-native-create-thumbnail: + :path: "../node_modules/react-native-create-thumbnail" react-native-document-picker: :path: "../node_modules/react-native-document-picker" + react-native-image-resizer: + :path: "../node_modules/@bam.tech/react-native-image-resizer" react-native-keep-awake: :path: "../node_modules/@sayem314/react-native-keep-awake" react-native-receive-sharing-intent: @@ -663,6 +676,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-firebase/app" RNFBMessaging: :path: "../node_modules/@react-native-firebase/messaging" + RNFS: + :path: "../node_modules/react-native-fs" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNImageCropPicker: @@ -712,7 +727,9 @@ SPEC CHECKSUMS: React-jsiexecutor: 515b703d23ffadeac7687bc2d12fb08b90f0aaa1 React-jsinspector: 9f7c9137605e72ca0343db4cea88006cb94856dd React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207 + react-native-create-thumbnail: e022bcdcba8a0b4529a50d3fa1a832ec921be39d react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c + react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa react-native-keep-awake: acbee258db16483744910f0da3ace39eb9ab47fd react-native-receive-sharing-intent: 62ab28c50e6ae56d32b9e841d7452091312a0bc7 react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a @@ -739,6 +756,7 @@ SPEC CHECKSUMS: RNDeviceInfo: 749f2e049dcd79e2e44f134f66b73a06951b5066 RNFBApp: 4f8ea53443d52c7db793234d2398a357fc6cfbf1 RNFBMessaging: c686471358d20d54f716a8b7b7f10f8944c966ec + RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39 RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332 RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128 diff --git a/app/mobile/package.json b/app/mobile/package.json index 8107ce7d..12ca38f1 100644 --- a/app/mobile/package.json +++ b/app/mobile/package.json @@ -10,6 +10,7 @@ "test": "jest" }, "dependencies": { + "@bam.tech/react-native-image-resizer": "^3.0.5", "@braintree/sanitize-url": "^6.0.2", "@react-native-clipboard/clipboard": "^1.11.1", "@react-native-firebase/app": "^17.2.0", @@ -27,8 +28,10 @@ "react": "18.2.0", "react-native": "0.71.3", "react-native-base64": "^0.2.1", + "react-native-create-thumbnail": "^1.6.4", "react-native-device-info": "^10.4.0", "react-native-document-picker": "^8.1.3", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.9.0", "react-native-image-crop-picker": "^0.39.0", "react-native-incall-manager": "^4.0.1", diff --git a/app/mobile/src/package.json b/app/mobile/src/package.json index 0e71c241..0312d2f0 100644 --- a/app/mobile/src/package.json +++ b/app/mobile/src/package.json @@ -1,3 +1,6 @@ { - "name": "src" + "name": "src", + "dependencies": { + "react-native-fs": "^2.20.0" + } } diff --git a/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js index f68f9b23..3d92ed2d 100644 --- a/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js @@ -5,6 +5,9 @@ import { Image } from 'react-native'; import Colors from 'constants/Colors'; import { getChannelSeals, getContentKey, encryptTopicSubject } from 'context/sealUtil'; import { AccountContext } from 'context/AccountContext'; +import { createThumbnail } from "react-native-create-thumbnail"; +import ImageResizer from '@bam.tech/react-native-image-resizer'; +import RNFS from 'react-native-fs'; export function useAddTopic(contentKey) { @@ -104,7 +107,7 @@ export function useAddTopic(contentKey) { setMessage: (message) => { updateState({ message }); }, - addImage: (data) => { + addImage: async (data) => { const url = data.startsWith('file:') ? data : 'file://' + data; assetId.current++; @@ -113,8 +116,18 @@ export function useAddTopic(contentKey) { updateState({ assets: [ ...state.assets, asset ] }); }) }, - addVideo: (data) => { + addVideo: async (data) => { const url = data.startsWith('file:') ? data : 'file://' + data + + const shot = await createThumbnail({ url: url, timeStamp: 5000, }) + console.log(shot); + + const thumb = await ImageResizer.createResizedImage('file://' + shot.path, 192, 192, "JPEG", 50, 0, null); + console.log(thumb); + + const base = await RNFS.readFile(thumb.path, 'base64') + console.log('data:image/jpeg;base64,' + base); + assetId.current++; const asset = { key: assetId.current, type: 'video', data: url, ratio: 1, duration: 0, position: 0 }; updateState({ assets: [ ...state.assets, asset ] }); diff --git a/app/mobile/yarn.lock b/app/mobile/yarn.lock index 3f833abd..a102d53d 100644 --- a/app/mobile/yarn.lock +++ b/app/mobile/yarn.lock @@ -1072,6 +1072,11 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@bam.tech/react-native-image-resizer@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@bam.tech/react-native-image-resizer/-/react-native-image-resizer-3.0.5.tgz#6661ba020de156268f73bdc92fbb93ef86f88a13" + integrity sha512-u5QGUQGGVZiVCJ786k9/kd7pPRZ6eYfJCYO18myVCH8FbVI7J8b5GT2Svjj2x808DlWeqfaZOOzxPqo27XYvrQ== + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" @@ -2477,7 +2482,7 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-64@0.1.0: +base-64@0.1.0, base-64@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== @@ -6305,6 +6310,11 @@ react-native-codegen@^0.71.5: jscodeshift "^0.13.1" nullthrows "^1.1.1" +react-native-create-thumbnail@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/react-native-create-thumbnail/-/react-native-create-thumbnail-1.6.4.tgz#90f5b0a587de6e3738a7632fe3d9a9624ed83581" + integrity sha512-JWuKXswDXtqUPfuqh6rjCVMvTSSG3kUtwvSK/YdaNU0i+nZKxeqHmt/CO2+TyI/WSUFynGVmWT1xOHhCZAFsRQ== + react-native-device-info@^10.4.0: version "10.4.0" resolved "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.4.0.tgz" @@ -6322,6 +6332,14 @@ react-native-elevation@^1.0.0: resolved "https://registry.yarnpkg.com/react-native-elevation/-/react-native-elevation-1.0.0.tgz#2a091c688290ac9b08b5842d1a8e8a00fc84233e" integrity sha512-BWIKcEYtzjRV6GpkX0Km5/w2E7fgIcywiQOT7JZTc5NSbv/YI9kpFinB9lRFsOoRVGmiqq/O3VfP/oH2clIiBA== +react-native-fs@^2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" + integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== + dependencies: + base-64 "^0.1.0" + utf8 "^3.0.0" + react-native-gesture-handler@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz#2f63812e523c646f25b9ad660fc6f75948e51241" @@ -7538,6 +7556,11 @@ use@^3.1.0: resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf8@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" diff --git a/net/container/Dockerfile b/net/container/Dockerfile index 42732a60..3575dab1 100644 --- a/net/container/Dockerfile +++ b/net/container/Dockerfile @@ -36,7 +36,7 @@ RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then ARCHITECTURE=amd64; elif [ "$ && wget -P /app https://go.dev/dl/go1.17.5.linux-${ARCHITECTURE}.tar.gz \ && tar -C /usr/local -xzf /app/go1.17.5.linux-${ARCHITECTURE}.tar.gz -RUN git clone https://github.com/balzack/databag.git /app/databag +RUN git clone https://github.com/balzack/databag.git /app/databag RUN yarn config set network-timeout 300000 RUN yarn --cwd /app/databag/net/web install From 000d306bd1e391aa7429bbd1954c664a30fc9cae Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Thu, 27 Apr 2023 10:20:25 -0700 Subject: [PATCH 04/27] sending encrypted asset blocks from mobile app --- app/mobile/src/context/sealUtil.js | 21 ++++++ .../src/context/useUploadContext.hook.js | 45 +++++++++++- .../conversation/addTopic/useAddTopic.hook.js | 68 +++++++++++-------- .../conversation/addTopic/useAddTopic.hook.js | 2 +- 4 files changed, 107 insertions(+), 29 deletions(-) diff --git a/app/mobile/src/context/sealUtil.js b/app/mobile/src/context/sealUtil.js index 47027d8b..8ab8658d 100644 --- a/app/mobile/src/context/sealUtil.js +++ b/app/mobile/src/context/sealUtil.js @@ -57,6 +57,27 @@ export function updateChannelSubject(subject, contentKey) { return { subjectEncrypted, subjectIv }; } +export function encryptBlock(block, contentKey) { + const key = CryptoJS.enc.Hex.parse(contentKey); + const iv = CryptoJS.lib.WordArray.random(128 / 8); + const encrypted = CryptoJS.AES.encrypt(block, key, { iv: iv }); + const blockEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64) + const blockIv = iv.toString(); + + return { blockEncrypted, blockIv }; +} + +export function decryptBlock(blockEncrypted, blockIv, contentKey) { + const iv = CryptoJS.enc.Hex.parse(blockIv); + const key = CryptoJS.enc.Hex.parse(contentKey); + const enc = CryptoJS.enc.Base64.parse(blockEncrypted); + const cipher = CryptoJS.lib.CipherParams.create({ ciphertext: enc, iv: iv }); + const dec = CryptoJS.AES.decrypt(cipher, key, { iv: iv }); + const block = dec.toString(CryptoJS.enc.Utf8); + + return block; +} + export function decryptChannelSubject(subject, contentKey) { const { subjectEncrypted, subjectIv } = JSON.parse(subject); const iv = CryptoJS.enc.Hex.parse(subjectIv); diff --git a/app/mobile/src/context/useUploadContext.hook.js b/app/mobile/src/context/useUploadContext.hook.js index 20ddbd7e..5510fa8a 100644 --- a/app/mobile/src/context/useUploadContext.hook.js +++ b/app/mobile/src/context/useUploadContext.hook.js @@ -1,5 +1,10 @@ import { useState, useRef } from 'react'; import axios from 'axios'; +import { createThumbnail } from "react-native-create-thumbnail"; +import ImageResizer from '@bam.tech/react-native-image-resizer'; +import RNFS from 'react-native-fs'; + +const ENCRYPTED_BLOCK_SIZE = (128 * 1024); //100k export function useUploadContext() { @@ -116,6 +121,23 @@ export function useUploadContext() { return { state, actions } } +async function getThumb(file, type, position) { + if (type === 'image') { + const thumb = await ImageResizer.createResizedImage(file, 192, 192, "JPEG", 50, 0, null); + const base = await RNFS.readFile(thumb.path, 'base64') + return `data:image/jpeg;base64,${base}`; + } + else if (type === 'video') { + const shot = await createThumbnail({ url: url, timeStamp: position * 1000 }) + const thumb = await ImageResizer.createResizedImage('file://' + shot.path, 192, 192, "JPEG", 50, 0, null); + const base = await RNFS.readFile(thumb.path, 'base64') + return `data:image/jpeg;base64,${base}`; + } + else { + return null + } +} + async function upload(entry, update, complete) { if (!entry.files?.length) { try { @@ -133,7 +155,28 @@ async function upload(entry, update, complete) { const file = entry.files.shift(); entry.active = {}; try { - if (file.type === 'image') { + if (file.encrypted) { + const { data, type, size, getEncryptedBlock, position } = file; + const thumb = await getThumb(data, type, position); + const parts = []; + for (let pos = 0; pos < size; pos += ENCRYPTED_BLOCK_SIZE) { + const { blockEncrypted, blockIv } = await getEncryptedBlock(pos, ENCRYPTED_BLOCK_SIZE); + const partId = await axios.post(`${entry.baseUrl}block${entry.urlParams}`, blockEncrypted, { + signal: entry.cancel.signal, + onUploadProgress: (ev) => { + const { loaded, total } = ev; + const partLoaded = pos + Math.floor(blockEncrypted.length * loaded / total); + entry.active = { partLoaded, size } + update(); + } + }); + parts.push({ blockIv, partId }); + } + entry.assets.push({ + encrypted: { type, thumb, parts } + }); + } + else if (file.type === 'image') { const formData = new FormData(); if (file.data.startsWith('file:')) { formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'}); diff --git a/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js index 3d92ed2d..59759b29 100644 --- a/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js @@ -3,10 +3,8 @@ import { UploadContext } from 'context/UploadContext'; import { ConversationContext } from 'context/ConversationContext'; import { Image } from 'react-native'; import Colors from 'constants/Colors'; -import { getChannelSeals, getContentKey, encryptTopicSubject } from 'context/sealUtil'; +import { encryptBlock, decryptBlock, getChannelSeals, getContentKey, encryptTopicSubject } from 'context/sealUtil'; import { AccountContext } from 'context/AccountContext'; -import { createThumbnail } from "react-native-create-thumbnail"; -import ImageResizer from '@bam.tech/react-native-image-resizer'; import RNFS from 'react-native-fs'; export function useAddTopic(contentKey) { @@ -57,6 +55,10 @@ export function useAddTopic(contentKey) { updateState({ conflict }); }, [state.assets, state.locked, state.enableImage, state.enableAudio, state.enableVideo]); + useEffect(() => { + updateState({ assets: [] }); + }, [contentKey]); + useEffect(() => { const cardId = conversation.state.card?.card?.cardId; const channelId = conversation.state.channel?.channelId; @@ -103,39 +105,51 @@ export function useAddTopic(contentKey) { updateState({ enableImage, enableAudio, enableVideo, locked }); }, [conversation.state]); + const setAsset = async (file) => { + const url = file.startsWith('file:') ? file : `file://${file}`; + if (contentKey) { + const stat = await RNFS.stat(url); + const getEncryptedBlock = async (pos, len) => { + if (pos + len > stat.size) { + return null; + } + const block = await RNFS.read(file, len, pos, 'base64'); + return getEncryptedBlock(block, contentKey); + } + return { data: url, encrypted: true, size: stat.size, getEncryptedBlock }; + } + else { + return { data: url, encrypted: false }; + } + } + const actions = { setMessage: (message) => { updateState({ message }); }, addImage: async (data) => { - const url = data.startsWith('file:') ? data : 'file://' + data; - assetId.current++; - Image.getSize(url, (width, height) => { - const asset = { key: assetId.current, type: 'image', data: url, ratio: width/height }; - updateState({ assets: [ ...state.assets, asset ] }); - }) - }, - addVideo: async (data) => { - const url = data.startsWith('file:') ? data : 'file://' + data - - const shot = await createThumbnail({ url: url, timeStamp: 5000, }) - console.log(shot); - - const thumb = await ImageResizer.createResizedImage('file://' + shot.path, 192, 192, "JPEG", 50, 0, null); - console.log(thumb); - - const base = await RNFS.readFile(thumb.path, 'base64') - console.log('data:image/jpeg;base64,' + base); - - assetId.current++; - const asset = { key: assetId.current, type: 'video', data: url, ratio: 1, duration: 0, position: 0 }; + const asset = await setAsset(data); + asset.key = assetId.current; + asset.type = 'image'; + asset.ratio = 1; updateState({ assets: [ ...state.assets, asset ] }); }, - addAudio: (data, label) => { - const url = data.startsWith('file:') ? data : 'file://' + data + addVideo: async (data) => { assetId.current++; - const asset = { key: assetId.current, type: 'audio', data: url, label }; + const asset = await setAsset(data); + asset.key = assetId.current; + asset.type = 'video'; + asset.position = 0; + asset.ratio = 1; + updateState({ assets: [ ...state.assets, asset ] }); + }, + addAudio: async (data, label) => { + assetId.current++; + const asset = await setAsset(data); + asset.key = assetId.current; + asset.type = 'audio'; + asset.label = label; updateState({ assets: [ ...state.assets, asset ] }); }, setVideoPosition: (key, position) => { diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 610364dd..ad7d8d75 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -56,7 +56,7 @@ export function useAddTopic(contentKey) { useEffect(() => { updateState({ assets: [] }); - return () => { console.log("RETURN CLEAR"); clearObjects() }; + return () => { clearObjects() }; }, [contentKey]); useEffect(() => { From 681bea25f876a30b1e6ac67b17af51df10b01c2c Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Thu, 27 Apr 2023 12:16:14 -0700 Subject: [PATCH 05/27] browser app cleanup for sending encrypted blocks --- net/web/src/context/useUploadContext.hook.js | 62 ++++++++++++++- .../conversation/addTopic/useAddTopic.hook.js | 79 ++++--------------- 2 files changed, 75 insertions(+), 66 deletions(-) diff --git a/net/web/src/context/useUploadContext.hook.js b/net/web/src/context/useUploadContext.hook.js index d685a4b4..2164bdae 100644 --- a/net/web/src/context/useUploadContext.hook.js +++ b/net/web/src/context/useUploadContext.hook.js @@ -1,5 +1,6 @@ import { useState, useRef } from 'react'; import axios from 'axios'; +import Resizer from "react-image-file-resizer"; const ENCRYPTED_BLOCK_SIZE = (128 * 1024); //110k @@ -149,6 +150,61 @@ export function useUploadContext() { return { state, actions } } + function getImageThumb(url) { + return new Promise(resolve => { + Resizer.imageFileResizer(url, 192, 192, 'JPEG', 50, 0, + uri => { + resolve(uri); + }, 'base64', 128, 128 ); + }); +} + +function getVideoThumb(url, pos) { + return new Promise((resolve, reject) => { + var video = document.createElement("video"); + var timeupdate = function (ev) { + video.removeEventListener("timeupdate", timeupdate); + video.pause(); + setTimeout(() => { + var canvas = document.createElement("canvas"); + if (video.videoWidth > video.videoHeight) { + canvas.width = 192; + canvas.height = Math.floor((192 * video.videoHeight / video.videoWidth)); + } + else { + canvas.height = 192; + canvas.width = Math.floor((192 * video.videoWidth / video.videoHeight)); + } + canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height); + var image = canvas.toDataURL("image/jpeg", 0.75); + resolve(image); + canvas.remove(); + video.remove(); + }, 1000); + }; + video.addEventListener("timeupdate", timeupdate); + video.preload = "metadata"; + video.src = url; + video.muted = true; + video.playsInline = true; + video.currentTime = pos; + video.play(); + }); +} + +async function getThumb(url, type, position) { + + if (type === 'image') { + return await getImageThumb(url); + } + else if (type === 'video') { + return await getVideoThumb(url, position); + } + else { + return null; + } +} + async function upload(entry, update, complete) { if (!entry.files?.length) { entry.success(entry.assets); @@ -159,9 +215,9 @@ async function upload(entry, update, complete) { entry.active = {}; try { if (file.encrypted) { - const { size, getThumb, getEncryptedBlock, position } = file; - const type = file.image ? 'image' : file.video ? 'video' : file.audio ? 'audio' : ''; - const thumb = getThumb(position); + const { size, getEncryptedBlock, position, image, video, audio } = file; + const { url, type } = image ? { url: image, type: 'image' } : video ? { url: video, type: 'video' } : audio ? { url: audio, type: 'audio' } : {} + const thumb = await getThumb(url, type, position); const parts = []; for (let pos = 0; pos < size; pos += ENCRYPTED_BLOCK_SIZE) { const { blockEncrypted, blockIv } = await getEncryptedBlock(pos, ENCRYPTED_BLOCK_SIZE); diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index ad7d8d75..6e47843a 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -64,7 +64,9 @@ export function useAddTopic(contentKey) { updateState({ enableImage, enableAudio, enableVideo }); }, [conversation.state.channel?.data?.channelDetail]); - const setUrl = async (url, getThumb) => { + const setUrl = async (file) => { + const url = URL.createObjectURL(file); + objects.current.push(url); if (contentKey) { const buffer = await url.arrayBuffer(); const getEncryptedBlock = (pos, len) => { @@ -74,38 +76,30 @@ export function useAddTopic(contentKey) { const block = btoa(String.fromCharCode.apply(null, buffer.slice(pos, len))); return getEncryptedBlock(block, contentKey); } - return { url, position: 0, label: '', encrypted: true, size: buffer.byteLength, getEncryptedBlock, getThumb }; + return { url, encrypted: true, size: buffer.byteLength, getEncryptedBlock }; } else { - return { url, position: 0, label: '', encrypted: false }; + return { url, encrypted: false }; } } const actions = { addImage: async (image) => { - const url = URL.createObjectURL(image); - objects.current.push(url); - const getThumb = async () => { - return await getImageThumb(url); - } - const asset = setUrl(url, getThumb); - addAsset({ image, ...asset }); + const asset = await setUrl(image); + asset.image = image; + addAsset(asset); }, addVideo: async (video) => { - const url = URL.createObjectURL(video); - objects.current.push(url); - const getThumb = async (position) => { - return await getVideoThumb(url, position); - } - const asset = setUrl(url, getThumb); - addAsset({ video, ...asset }); + const asset = await setUrl(video); + asset.video = video; + asset.position = 0; + addAsset(asset); }, addAudio: async (audio) => { - const url = URL.createObjectURL(audio); - objects.current.push(url); - const getThumb = async () => { return null }; - const asset = setUrl(url, getThumb); - addAsset({ audio, ...asset }); + const asset = await setUrl(audio); + asset.audio = audio; + asset.label = ''; + addAsset(asset); }, setLabel: (index, label) => { updateAsset(index, { label }); @@ -167,44 +161,3 @@ export function useAddTopic(contentKey) { return { state, actions }; } -async function getImageThumb(url) { - return new Promise(resolve => { - Resizer.imageFileResizer(url, 192, 192, 'JPEG', 50, 0, - uri => { - resolve(uri); - }, 'base64', 128, 128 ); - }); -} - -async function getVideoThumb(url, pos) { - return new Promise((resolve, reject) => { - var video = document.createElement("video"); - var timeupdate = function (ev) { - video.removeEventListener("timeupdate", timeupdate); - video.pause(); - setTimeout(() => { - var canvas = document.createElement("canvas"); - if (video.videoWidth > video.videoHeight) { - canvas.width = 192; - canvas.height = Math.floor((192 * video.videoHeight / video.videoWidth)); - } - else { - canvas.height = 192; - canvas.width = Math.floor((192 * video.videoWidth / video.videoHeight)); - } - canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height); - var image = canvas.toDataURL("image/jpeg", 0.75); - resolve(image); - canvas.remove(); - video.remove(); - }, 1000); - }; - video.addEventListener("timeupdate", timeupdate); - video.preload = "metadata"; - video.src = url; - video.muted = true; - video.playsInline = true; - video.currentTime = pos; - video.play(); - }); -} From 7a21b8636f2ecda80df2b0eabc90980c70e0e2cf Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Thu, 27 Apr 2023 13:55:40 -0700 Subject: [PATCH 06/27] fix console errors --- net/web/src/session/conversation/addTopic/useAddTopic.hook.js | 2 +- net/web/src/session/conversation/useConversation.hook.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 6e47843a..7d72e797 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -141,7 +141,7 @@ export function useAddTopic(contentKey) { } } }; - await conversation.actions.addTopic(type, message, state.assets); + await conversation.actions.addTopic(type, message, [ ...state.assets ]); updateState({ busy: false, messageText: null, textColor: '#444444', textColorSet: false, textSize: 12, textSizeSet: false, assets: [] }); clearObjects(); diff --git a/net/web/src/session/conversation/useConversation.hook.js b/net/web/src/session/conversation/useConversation.hook.js index a8783b8f..492acd29 100644 --- a/net/web/src/session/conversation/useConversation.hook.js +++ b/net/web/src/session/conversation/useConversation.hook.js @@ -145,7 +145,7 @@ export function useConversation(cardId, channelId) { let group = ''; let clickable = []; - const words = text == null ? '' : DOMPurify.sanitize(text).split(' '); + const words = text == [] ? '' : DOMPurify.sanitize(text).split(' '); words.forEach((word, index) => { if (!!urlPattern.test(word)) { clickable.push({ group }); From 18c88c38b6a0c53387b5d062891be1877dd42753 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Thu, 27 Apr 2023 15:44:28 -0700 Subject: [PATCH 07/27] fixing error loading file data --- net/web/package.json | 1 - .../conversation/addTopic/useAddTopic.hook.js | 12 ++++++-- .../addTopic/videoFile/VideoFile.jsx | 16 +++++----- net/web/yarn.lock | 30 ++----------------- 4 files changed, 19 insertions(+), 40 deletions(-) diff --git a/net/web/package.json b/net/web/package.json index 34beeaae..226c2581 100644 --- a/net/web/package.json +++ b/net/web/package.json @@ -30,7 +30,6 @@ "react-easy-crop": "^4.1.4", "react-icons": "^4.8.0", "react-image-file-resizer": "^0.4.8", - "react-player": "^2.10.0", "react-resize-detector": "^7.0.0", "react-router-dom": "^6.2.2", "react-scripts": "5.0.0", diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 7d72e797..aefe21cd 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -64,17 +64,25 @@ export function useAddTopic(contentKey) { updateState({ enableImage, enableAudio, enableVideo }); }, [conversation.state.channel?.data?.channelDetail]); + const loadFile = (file) => { + return new Promise((resolve) => { + const reader = new FileReader() + reader.onloadend = () => resolve(reader.result) + reader.readAsArrayBuffer(file) + }) + }; + const setUrl = async (file) => { const url = URL.createObjectURL(file); objects.current.push(url); if (contentKey) { - const buffer = await url.arrayBuffer(); + const buffer = await loadFile(file) const getEncryptedBlock = (pos, len) => { if (pos + len > buffer.byteLen) { return null; } const block = btoa(String.fromCharCode.apply(null, buffer.slice(pos, len))); - return getEncryptedBlock(block, contentKey); + return encryptBlock(block, contentKey); } return { url, encrypted: true, size: buffer.byteLength, getEncryptedBlock }; } diff --git a/net/web/src/session/conversation/addTopic/videoFile/VideoFile.jsx b/net/web/src/session/conversation/addTopic/videoFile/VideoFile.jsx index d3e37c69..05ec82ef 100644 --- a/net/web/src/session/conversation/addTopic/videoFile/VideoFile.jsx +++ b/net/web/src/session/conversation/addTopic/videoFile/VideoFile.jsx @@ -1,5 +1,4 @@ import React, { useState, useRef } from 'react'; -import ReactPlayer from 'react-player' import ReactResizeDetector from 'react-resize-detector'; import { RightOutlined, LeftOutlined } from '@ant-design/icons'; import { VideoFileWrapper } from './VideoFile.styled'; @@ -17,22 +16,22 @@ export function VideoFile({ url, onPosition }) { const onSeek = (offset) => { if (player.current) { - let len = player.current.getDuration(); - if (len > 128) { - offset *= Math.floor(len / 128); + const len = player.current.duration; + if (len > 16) { + offset *= Math.floor(len / 16); } seek.current += offset; if (seek.current < 0 || seek.current >= len) { seek.current = 0; } onPosition(seek.current); - player.current.seekTo(seek.current, 'seconds'); - setPlaying(true); + player.current.currentTime = seek.current; + player.current.play(); } } const onPause = () => { - setPlaying(false); + player.current.pause(); } return ( @@ -42,8 +41,7 @@ export function VideoFile({ url, onPosition }) { if (width !== state.width || height !== state.height) { updateState({ width, height }); } - return onPause()} onPlay={() => onPause()} /> + return