mirror of
https://github.com/balzack/databag.git
synced 2025-02-12 03:29:16 +00:00
Merge branch 'asset'
This commit is contained in:
commit
8b4664f90f
@ -622,7 +622,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.12;
|
MARKETING_VERSION = 1.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@ -656,7 +656,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.12;
|
MARKETING_VERSION = 1.13;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
|
@ -64,6 +64,15 @@ PODS:
|
|||||||
- hermes-engine/Pre-built (0.71.3)
|
- hermes-engine/Pre-built (0.71.3)
|
||||||
- JitsiWebRTC (106.0.0)
|
- JitsiWebRTC (106.0.0)
|
||||||
- libevent (2.1.12)
|
- libevent (2.1.12)
|
||||||
|
- libwebp (1.2.4):
|
||||||
|
- libwebp/demux (= 1.2.4)
|
||||||
|
- libwebp/mux (= 1.2.4)
|
||||||
|
- libwebp/webp (= 1.2.4)
|
||||||
|
- libwebp/demux (1.2.4):
|
||||||
|
- libwebp/webp
|
||||||
|
- libwebp/mux (1.2.4):
|
||||||
|
- libwebp/demux
|
||||||
|
- libwebp/webp (1.2.4)
|
||||||
- nanopb (2.30909.0):
|
- nanopb (2.30909.0):
|
||||||
- nanopb/decode (= 2.30909.0)
|
- nanopb/decode (= 2.30909.0)
|
||||||
- nanopb/encode (= 2.30909.0)
|
- nanopb/encode (= 2.30909.0)
|
||||||
@ -321,8 +330,12 @@ PODS:
|
|||||||
- React-jsinspector (0.71.3)
|
- React-jsinspector (0.71.3)
|
||||||
- React-logger (0.71.3):
|
- React-logger (0.71.3):
|
||||||
- glog
|
- glog
|
||||||
|
- react-native-create-thumbnail (1.6.4):
|
||||||
|
- React-Core
|
||||||
- react-native-document-picker (8.1.3):
|
- react-native-document-picker (8.1.3):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- react-native-image-resizer (3.0.5):
|
||||||
|
- React-Core
|
||||||
- react-native-keep-awake (1.1.0):
|
- react-native-keep-awake (1.1.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-receive-sharing-intent (2.0.0):
|
- react-native-receive-sharing-intent (2.0.0):
|
||||||
@ -437,6 +450,10 @@ PODS:
|
|||||||
- React-Core
|
- React-Core
|
||||||
- RNDeviceInfo (10.4.0):
|
- RNDeviceInfo (10.4.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- RNFastImage (8.6.3):
|
||||||
|
- React-Core
|
||||||
|
- SDWebImage (~> 5.11.1)
|
||||||
|
- SDWebImageWebPCoder (~> 0.8.4)
|
||||||
- RNFBApp (17.2.0):
|
- RNFBApp (17.2.0):
|
||||||
- Firebase/CoreOnly (= 10.5.0)
|
- Firebase/CoreOnly (= 10.5.0)
|
||||||
- React-Core
|
- React-Core
|
||||||
@ -445,6 +462,8 @@ PODS:
|
|||||||
- FirebaseCoreExtension (= 10.5.0)
|
- FirebaseCoreExtension (= 10.5.0)
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNFBApp
|
- RNFBApp
|
||||||
|
- RNFS (2.20.0):
|
||||||
|
- React-Core
|
||||||
- RNGestureHandler (2.9.0):
|
- RNGestureHandler (2.9.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNImageCropPicker (0.39.0):
|
- RNImageCropPicker (0.39.0):
|
||||||
@ -490,6 +509,12 @@ PODS:
|
|||||||
- React-Core
|
- React-Core
|
||||||
- RNVectorIcons (9.2.0):
|
- RNVectorIcons (9.2.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- SDWebImage (5.11.1):
|
||||||
|
- SDWebImage/Core (= 5.11.1)
|
||||||
|
- SDWebImage/Core (5.11.1)
|
||||||
|
- SDWebImageWebPCoder (0.8.5):
|
||||||
|
- libwebp (~> 1.0)
|
||||||
|
- SDWebImage/Core (~> 5.10)
|
||||||
- TOCropViewController (2.6.1)
|
- TOCropViewController (2.6.1)
|
||||||
- Yoga (1.14.0)
|
- Yoga (1.14.0)
|
||||||
|
|
||||||
@ -516,7 +541,9 @@ DEPENDENCIES:
|
|||||||
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
||||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
- 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-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-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-receive-sharing-intent (from `../node_modules/react-native-receive-sharing-intent`)
|
||||||
- react-native-rsa-native (from `../node_modules/react-native-rsa-native`)
|
- react-native-rsa-native (from `../node_modules/react-native-rsa-native`)
|
||||||
@ -541,8 +568,10 @@ DEPENDENCIES:
|
|||||||
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
|
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
|
||||||
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
|
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
|
||||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||||
|
- RNFastImage (from `../node_modules/react-native-fast-image`)
|
||||||
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
|
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
|
||||||
- "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)"
|
- "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)"
|
||||||
|
- RNFS (from `../node_modules/react-native-fs`)
|
||||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||||
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
||||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||||
@ -564,8 +593,11 @@ SPEC REPOS:
|
|||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- JitsiWebRTC
|
- JitsiWebRTC
|
||||||
- libevent
|
- libevent
|
||||||
|
- libwebp
|
||||||
- nanopb
|
- nanopb
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
|
- SDWebImage
|
||||||
|
- SDWebImageWebPCoder
|
||||||
- TOCropViewController
|
- TOCropViewController
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
@ -609,8 +641,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native/ReactCommon/jsinspector"
|
:path: "../node_modules/react-native/ReactCommon/jsinspector"
|
||||||
React-logger:
|
React-logger:
|
||||||
:path: "../node_modules/react-native/ReactCommon/logger"
|
:path: "../node_modules/react-native/ReactCommon/logger"
|
||||||
|
react-native-create-thumbnail:
|
||||||
|
:path: "../node_modules/react-native-create-thumbnail"
|
||||||
react-native-document-picker:
|
react-native-document-picker:
|
||||||
:path: "../node_modules/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:
|
react-native-keep-awake:
|
||||||
:path: "../node_modules/@sayem314/react-native-keep-awake"
|
:path: "../node_modules/@sayem314/react-native-keep-awake"
|
||||||
react-native-receive-sharing-intent:
|
react-native-receive-sharing-intent:
|
||||||
@ -659,10 +695,14 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/@react-native-clipboard/clipboard"
|
:path: "../node_modules/@react-native-clipboard/clipboard"
|
||||||
RNDeviceInfo:
|
RNDeviceInfo:
|
||||||
:path: "../node_modules/react-native-device-info"
|
:path: "../node_modules/react-native-device-info"
|
||||||
|
RNFastImage:
|
||||||
|
:path: "../node_modules/react-native-fast-image"
|
||||||
RNFBApp:
|
RNFBApp:
|
||||||
:path: "../node_modules/@react-native-firebase/app"
|
:path: "../node_modules/@react-native-firebase/app"
|
||||||
RNFBMessaging:
|
RNFBMessaging:
|
||||||
:path: "../node_modules/@react-native-firebase/messaging"
|
:path: "../node_modules/@react-native-firebase/messaging"
|
||||||
|
RNFS:
|
||||||
|
:path: "../node_modules/react-native-fs"
|
||||||
RNGestureHandler:
|
RNGestureHandler:
|
||||||
:path: "../node_modules/react-native-gesture-handler"
|
:path: "../node_modules/react-native-gesture-handler"
|
||||||
RNImageCropPicker:
|
RNImageCropPicker:
|
||||||
@ -696,6 +736,7 @@ SPEC CHECKSUMS:
|
|||||||
hermes-engine: 38bfe887e456b33b697187570a08de33969f5db7
|
hermes-engine: 38bfe887e456b33b697187570a08de33969f5db7
|
||||||
JitsiWebRTC: f441eb0e2d67f0588bf24e21c5162e97342714fb
|
JitsiWebRTC: f441eb0e2d67f0588bf24e21c5162e97342714fb
|
||||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||||
|
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||||
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
|
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
|
||||||
PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb
|
PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb
|
||||||
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
||||||
@ -712,7 +753,9 @@ SPEC CHECKSUMS:
|
|||||||
React-jsiexecutor: 515b703d23ffadeac7687bc2d12fb08b90f0aaa1
|
React-jsiexecutor: 515b703d23ffadeac7687bc2d12fb08b90f0aaa1
|
||||||
React-jsinspector: 9f7c9137605e72ca0343db4cea88006cb94856dd
|
React-jsinspector: 9f7c9137605e72ca0343db4cea88006cb94856dd
|
||||||
React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207
|
React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207
|
||||||
|
react-native-create-thumbnail: e022bcdcba8a0b4529a50d3fa1a832ec921be39d
|
||||||
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
|
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
|
||||||
|
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
|
||||||
react-native-keep-awake: acbee258db16483744910f0da3ace39eb9ab47fd
|
react-native-keep-awake: acbee258db16483744910f0da3ace39eb9ab47fd
|
||||||
react-native-receive-sharing-intent: 62ab28c50e6ae56d32b9e841d7452091312a0bc7
|
react-native-receive-sharing-intent: 62ab28c50e6ae56d32b9e841d7452091312a0bc7
|
||||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||||
@ -737,14 +780,18 @@ SPEC CHECKSUMS:
|
|||||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||||
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
||||||
RNDeviceInfo: 749f2e049dcd79e2e44f134f66b73a06951b5066
|
RNDeviceInfo: 749f2e049dcd79e2e44f134f66b73a06951b5066
|
||||||
|
RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
|
||||||
RNFBApp: 4f8ea53443d52c7db793234d2398a357fc6cfbf1
|
RNFBApp: 4f8ea53443d52c7db793234d2398a357fc6cfbf1
|
||||||
RNFBMessaging: c686471358d20d54f716a8b7b7f10f8944c966ec
|
RNFBMessaging: c686471358d20d54f716a8b7b7f10f8944c966ec
|
||||||
|
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||||
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
|
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
|
||||||
RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332
|
RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332
|
||||||
RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128
|
RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128
|
||||||
RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f
|
RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f
|
||||||
RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c
|
RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c
|
||||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||||
|
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||||
|
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||||
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
|
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
|
||||||
Yoga: 5ed1699acbba8863755998a4245daa200ff3817b
|
Yoga: 5ed1699acbba8863755998a4245daa200ff3817b
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bam.tech/react-native-image-resizer": "^3.0.5",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@react-native-clipboard/clipboard": "^1.11.1",
|
"@react-native-clipboard/clipboard": "^1.11.1",
|
||||||
"@react-native-firebase/app": "^17.2.0",
|
"@react-native-firebase/app": "^17.2.0",
|
||||||
@ -27,8 +28,11 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.71.3",
|
"react-native": "0.71.3",
|
||||||
"react-native-base64": "^0.2.1",
|
"react-native-base64": "^0.2.1",
|
||||||
|
"react-native-create-thumbnail": "^1.6.4",
|
||||||
"react-native-device-info": "^10.4.0",
|
"react-native-device-info": "^10.4.0",
|
||||||
"react-native-document-picker": "^8.1.3",
|
"react-native-document-picker": "^8.1.3",
|
||||||
|
"react-native-fast-image": "^8.6.3",
|
||||||
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "^2.9.0",
|
"react-native-gesture-handler": "^2.9.0",
|
||||||
"react-native-image-crop-picker": "^0.39.0",
|
"react-native-image-crop-picker": "^0.39.0",
|
||||||
"react-native-incall-manager": "^4.0.1",
|
"react-native-incall-manager": "^4.0.1",
|
||||||
|
@ -57,6 +57,27 @@ export function updateChannelSubject(subject, contentKey) {
|
|||||||
return { subjectEncrypted, subjectIv };
|
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) {
|
export function decryptChannelSubject(subject, contentKey) {
|
||||||
const { subjectEncrypted, subjectIv } = JSON.parse(subject);
|
const { subjectEncrypted, subjectIv } = JSON.parse(subject);
|
||||||
const iv = CryptoJS.enc.Hex.parse(subjectIv);
|
const iv = CryptoJS.enc.Hex.parse(subjectIv);
|
||||||
|
@ -67,8 +67,9 @@ export function useAppContext() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setSession = async () => {
|
const setSession = async () => {
|
||||||
const { loginTimestamp } = access.current;
|
const { loginTimestamp, guid } = access.current;
|
||||||
updateState({ session: true, loginTimestamp, status: 'connecting' });
|
updateState({ session: true, loginTimestamp, status: 'connecting' });
|
||||||
|
await store.actions.updateDb(guid);
|
||||||
await account.actions.setSession(access.current);
|
await account.actions.setSession(access.current);
|
||||||
await profile.actions.setSession(access.current);
|
await profile.actions.setSession(access.current);
|
||||||
await card.actions.setSession(access.current);
|
await card.actions.setSession(access.current);
|
||||||
|
@ -493,6 +493,14 @@ export function useCardContext() {
|
|||||||
const { guid } = access.current || {};
|
const { guid } = access.current || {};
|
||||||
return await store.actions.getCardChannelTopicItems(guid, cardId, channelId);
|
return await store.actions.getCardChannelTopicItems(guid, cardId, channelId);
|
||||||
},
|
},
|
||||||
|
getTopicItemsId: async (cardId, channelId) => {
|
||||||
|
const { guid } = access.current || {};
|
||||||
|
return await store.actions.getCardChannelTopicItemsId(guid, cardId, channelId);
|
||||||
|
},
|
||||||
|
getTopicItemsById: async (cardId, channelId, topics) => {
|
||||||
|
const { guid } = access.current || {};
|
||||||
|
return await store.actions.getCardChannelTopicItemsById(guid, cardId, channelId, topics);
|
||||||
|
},
|
||||||
setTopicItem: async (cardId, channelId, topicId, topic) => {
|
setTopicItem: async (cardId, channelId, topicId, topic) => {
|
||||||
const { guid } = access.current || {};
|
const { guid } = access.current || {};
|
||||||
return await store.actions.setCardChannelTopicItem(guid, cardId, channelId, topicId, topic);
|
return await store.actions.setCardChannelTopicItem(guid, cardId, channelId, topicId, topic);
|
||||||
|
@ -263,6 +263,14 @@ export function useChannelContext() {
|
|||||||
const { guid } = access.current || {};
|
const { guid } = access.current || {};
|
||||||
return await store.actions.getChannelTopicItems(guid, channelId);
|
return await store.actions.getChannelTopicItems(guid, channelId);
|
||||||
},
|
},
|
||||||
|
getTopicItemsId: async (channelId) => {
|
||||||
|
const { guid } = access.current || {};
|
||||||
|
return await store.actions.getChannelTopicItemsId(guid, channelId);
|
||||||
|
},
|
||||||
|
getTopicItemsById: async (channelId, topics) => {
|
||||||
|
const { guid } = access.current || {};
|
||||||
|
return await store.actions.getChannelTopicItemsById(guid, channelId, topics);
|
||||||
|
},
|
||||||
setTopicItem: async (channelId, topic) => {
|
setTopicItem: async (channelId, topic) => {
|
||||||
const { guid } = access.current || {};
|
const { guid } = access.current || {};
|
||||||
return await store.actions.setChannelTopicItem(guid, channelId, topic);
|
return await store.actions.setChannelTopicItem(guid, channelId, topic);
|
||||||
|
@ -7,7 +7,7 @@ import { ProfileContext } from 'context/ProfileContext';
|
|||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
export function useConversationContext() {
|
export function useConversationContext() {
|
||||||
const COUNT = 48;
|
const COUNT = 32;
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
loaded: false,
|
loaded: false,
|
||||||
@ -25,6 +25,7 @@ export function useConversationContext() {
|
|||||||
const syncing = useRef(false);
|
const syncing = useRef(false);
|
||||||
const update = useRef(false);
|
const update = useRef(false);
|
||||||
const loaded = useRef(false);
|
const loaded = useRef(false);
|
||||||
|
const stored = useRef([]);
|
||||||
const conversationId = useRef(null);
|
const conversationId = useRef(null);
|
||||||
const topics = useRef(new Map());
|
const topics = useRef(new Map());
|
||||||
|
|
||||||
@ -63,7 +64,26 @@ export function useConversationContext() {
|
|||||||
|
|
||||||
if (channelValue) {
|
if (channelValue) {
|
||||||
if (!loaded.current) {
|
if (!loaded.current) {
|
||||||
const topicItems = await getTopicItems(cardId, channelId);
|
|
||||||
|
stored.current = await getTopicItemsId(cardId, channelId);
|
||||||
|
stored.current.sort((a,b) => {
|
||||||
|
if (a.created > b.created) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.created < b.created) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = [];
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
if (stored.current.length > 0) {
|
||||||
|
ids.push(stored.current.shift().topicId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const topicItems = await getTopicItemsById(cardId, channelId, ids);
|
||||||
for (let topic of topicItems) {
|
for (let topic of topicItems) {
|
||||||
topics.current.set(topic.topicId, topic);
|
topics.current.set(topic.topicId, topic);
|
||||||
}
|
}
|
||||||
@ -94,12 +114,27 @@ export function useConversationContext() {
|
|||||||
updateState({ loaded: true, offsync: false, topics: topics.current, card: cardValue, channel: channelValue });
|
updateState({ loaded: true, offsync: false, topics: topics.current, card: cardValue, channel: channelValue });
|
||||||
}
|
}
|
||||||
else if (loadMore) {
|
else if (loadMore) {
|
||||||
const delta = await getTopicDelta(cardId, channelId, null, COUNT, null, curTopicMarker.current);
|
if (stored.current.length > 0) {
|
||||||
const marker = delta.marker ? delta.marker : 1;
|
const ids = [];
|
||||||
await setTopicDelta(cardId, channelId, delta.topics);
|
for (let i = 0; i < COUNT; i++) {
|
||||||
await setTopicMarker(cardId, channelId, marker);
|
if (stored.current.length > 0) {
|
||||||
curTopicMarker.current = marker;
|
ids.push(stored.current.shift().topicId);
|
||||||
updateState({ loaded: true, offsync: false, topics: topics.current, card: cardValue, channel: channelValue });
|
}
|
||||||
|
}
|
||||||
|
const topicItems = await getTopicItemsById(cardId, channelId, ids);
|
||||||
|
for (let topic of topicItems) {
|
||||||
|
topics.current.set(topic.topicId, topic);
|
||||||
|
}
|
||||||
|
updateState({ loaded: true, topics: topics.current, card: cardValue, channel: channelValue });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const delta = await getTopicDelta(cardId, channelId, null, COUNT, null, curTopicMarker.current);
|
||||||
|
const marker = delta.marker ? delta.marker : 1;
|
||||||
|
await setTopicDelta(cardId, channelId, delta.topics);
|
||||||
|
await setTopicMarker(cardId, channelId, marker);
|
||||||
|
curTopicMarker.current = marker;
|
||||||
|
updateState({ loaded: true, offsync: false, topics: topics.current, card: cardValue, channel: channelValue });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (ignoreRevision || topicRevision > curSyncRevision.current) {
|
else if (ignoreRevision || topicRevision > curSyncRevision.current) {
|
||||||
const delta = await getTopicDelta(cardId, channelId, curSyncRevision.current, null, curTopicMarker.current, null);
|
const delta = await getTopicDelta(cardId, channelId, curSyncRevision.current, null, curTopicMarker.current, null);
|
||||||
@ -134,19 +169,19 @@ export function useConversationContext() {
|
|||||||
if (entry.data) {
|
if (entry.data) {
|
||||||
if (entry.data.topicDetail) {
|
if (entry.data.topicDetail) {
|
||||||
const item = mapTopicEntry(entry);
|
const item = mapTopicEntry(entry);
|
||||||
setTopicItem(cardId, channelId, item);
|
await setTopicItem(cardId, channelId, item);
|
||||||
topics.current.set(item.topicId, item);
|
topics.current.set(item.topicId, item);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const topic = await getTopic(cardId, channelId, entry.id);
|
const topic = await getTopic(cardId, channelId, entry.id);
|
||||||
const item = mapTopicEntry(topic);
|
const item = mapTopicEntry(topic);
|
||||||
setTopicItem(cardId, channelId, item);
|
await setTopicItem(cardId, channelId, item);
|
||||||
topics.current.set(item.topicId, item);
|
topics.current.set(item.topicId, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
topics.current.delete(entry.id);
|
topics.current.delete(entry.id);
|
||||||
clearTopicItem(entry.id);
|
clearTopicItem(cardId, channelId, entry.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -347,6 +382,20 @@ export function useConversationContext() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTopicItemsId = async (cardId, channelId) => {
|
||||||
|
if (cardId) {
|
||||||
|
return await card.actions.getTopicItemsId(cardId, channelId);
|
||||||
|
}
|
||||||
|
return await channel.actions.getTopicItemsId(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTopicItemsById = async (cardId, channelId, topics) => {
|
||||||
|
if (cardId) {
|
||||||
|
return await card.actions.getTopicItemsById(cardId, channelId, topics);
|
||||||
|
}
|
||||||
|
return await channel.actions.getTopicItemsById(channelId, topics);
|
||||||
|
}
|
||||||
|
|
||||||
const getTopicItems = async (cardId, channelId) => {
|
const getTopicItems = async (cardId, channelId) => {
|
||||||
if (cardId) {
|
if (cardId) {
|
||||||
return await card.actions.getTopicItems(cardId, channelId);
|
return await card.actions.getTopicItems(cardId, channelId);
|
||||||
|
@ -13,10 +13,23 @@ export function useStoreContext() {
|
|||||||
|
|
||||||
const initSession = async (guid) => {
|
const initSession = async (guid) => {
|
||||||
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_${guid} (channel_id text, revision integer, detail_revision integer, topic_revision integer, topic_marker integer, blocked integer, sync_revision integer, detail text, unsealed_detail text, summary text, unsealed_summary text, offsync integer, read_revision integer, unique(channel_id))`);
|
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_${guid} (channel_id text, revision integer, detail_revision integer, topic_revision integer, topic_marker integer, blocked integer, sync_revision integer, detail text, unsealed_detail text, summary text, unsealed_summary text, offsync integer, read_revision integer, unique(channel_id))`);
|
||||||
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_topic_${guid} (channel_id text, topic_id text, revision integer, detail_revision integer, blocked integer, detail text, unsealed_detail text, unique(channel_id, topic_id))`);
|
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_topic_${guid} (channel_id text, topic_id text, revision integer, created integer, detail_revision integer, blocked integer, detail text, unsealed_detail text, unique(channel_id, topic_id))`);
|
||||||
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_${guid} (card_id text, revision integer, detail_revision integer, profile_revision integer, detail text, profile text, notified_view integer, notified_article integer, notified_profile integer, notified_channel integer, offsync integer, blocked integer, unique(card_id))`);
|
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_${guid} (card_id text, revision integer, detail_revision integer, profile_revision integer, detail text, profile text, notified_view integer, notified_article integer, notified_profile integer, notified_channel integer, offsync integer, blocked integer, unique(card_id))`);
|
||||||
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_channel_${guid} (card_id text, channel_id text, revision integer, detail_revision integer, topic_revision integer, topic_marker integer, sync_revision integer, detail text, unsealed_detail text, summary text, unsealed_summary text, offsync integer, blocked integer, read_revision integer, unique(card_id, channel_id))`);
|
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_channel_${guid} (card_id text, channel_id text, revision integer, detail_revision integer, topic_revision integer, topic_marker integer, sync_revision integer, detail text, unsealed_detail text, summary text, unsealed_summary text, offsync integer, blocked integer, read_revision integer, unique(card_id, channel_id))`);
|
||||||
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_channel_topic_${guid} (card_id text, channel_id text, topic_id text, revision integer, detail_revision integer, blocked integer, detail text, unsealed_detail text, unique(card_id, channel_id, topic_id))`);
|
await db.current.executeSql(`CREATE TABLE IF NOT EXISTS card_channel_topic_${guid} (card_id text, channel_id text, topic_id text, revision integer, created integer, detail_revision integer, blocked integer, detail text, unsealed_detail text, unique(card_id, channel_id, topic_id))`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasColumn = async (table, column) => {
|
||||||
|
const pragma = await db.current.executeSql(`PRAGMA table_info(${table})`);
|
||||||
|
if (pragma?.length === 1) {
|
||||||
|
for (let i = 0; i < pragma[0].rows.length; i++) {
|
||||||
|
const col = pragma[0].rows.item(i);
|
||||||
|
if (col.name === column) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@ -28,6 +41,16 @@ export function useStoreContext() {
|
|||||||
await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values ('session', null);");
|
await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values ('session', null);");
|
||||||
return await getAppValue(db.current, 'session');
|
return await getAppValue(db.current, 'session');
|
||||||
},
|
},
|
||||||
|
updateDb: async (guid) => {
|
||||||
|
const hasChannel = await hasColumn(`channel_topic_${guid}`, 'created');
|
||||||
|
if (!hasChannel) {
|
||||||
|
await db.current.executeSql(`ALTER TABLE channel_topic_${guid} ADD COLUMN created integer default 0`);
|
||||||
|
}
|
||||||
|
const hasCardChannel = await hasColumn(`card_channel_topic_${guid}`, 'created');
|
||||||
|
if (!hasCardChannel) {
|
||||||
|
await db.current.executeSql(`ALTER TABLE card_channel_topic_${guid} ADD COLUMN created integer default 0`);
|
||||||
|
}
|
||||||
|
},
|
||||||
setSession: async (access) => {
|
setSession: async (access) => {
|
||||||
await initSession(access.guid);
|
await initSession(access.guid);
|
||||||
await db.current.executeSql("UPDATE app SET value=? WHERE key='session';", [encodeObject(access)]);
|
await db.current.executeSql("UPDATE app SET value=? WHERE key='session';", [encodeObject(access)]);
|
||||||
@ -240,9 +263,28 @@ export function useStoreContext() {
|
|||||||
unsealedDetail: decodeObject(topic.unsealed_detail),
|
unsealedDetail: decodeObject(topic.unsealed_detail),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
getChannelTopicItemsId: async (guid, channelId) => {
|
||||||
|
const values = await getAppValues(db.current, `SELECT topic_id, created FROM channel_topic_${guid} WHERE channel_id=?`, [channelId]);
|
||||||
|
return values.map(topic => ({
|
||||||
|
topicId: topic.topic_id,
|
||||||
|
created: topic.created,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
getChannelTopicItemsById: async (guid, channelId, topics) => {
|
||||||
|
const q = topics.map(() => '?');
|
||||||
|
const values = await getAppValues(db.current, `SELECT topic_id, revision, blocked, detail_revision, detail, unsealed_detail FROM channel_topic_${guid} WHERE channel_id=? AND topic_id in (${q.join(',')})`, [channelId, ...topics]);
|
||||||
|
return values.map(topic => ({
|
||||||
|
topicId: topic.topic_id,
|
||||||
|
revision: topic.revision,
|
||||||
|
blocked: topic.blocked,
|
||||||
|
detailRevision: topic.detail_revision,
|
||||||
|
detail: decodeObject(topic.detail),
|
||||||
|
unsealedDetail: decodeObject(topic.unsealed_detail),
|
||||||
|
}));
|
||||||
|
},
|
||||||
setChannelTopicItem: async (guid, channelId, topic) => {
|
setChannelTopicItem: async (guid, channelId, topic) => {
|
||||||
const { topicId, revision, detailRevision, detail } = topic;
|
const { topicId, revision, detailRevision, detail } = topic;
|
||||||
await db.current.executeSql(`INSERT OR REPLACE INTO channel_topic_${guid} (channel_id, topic_id, revision, detail_revision, blocked, detail, unsealed_detail) values (?, ?, ?, ?, false, ?, null);`, [channelId, topicId, revision, detailRevision, encodeObject(detail)]);
|
await db.current.executeSql(`INSERT OR REPLACE INTO channel_topic_${guid} (channel_id, topic_id, revision, created, detail_revision, blocked, detail, unsealed_detail) values (?, ?, ?, ?, ?, false, ?, null);`, [channelId, topicId, revision, detail?.created, detailRevision, encodeObject(detail)]);
|
||||||
},
|
},
|
||||||
setChannelTopicItemUnsealedDetail: async (guid, channelId, topicId, revision, unsealed) => {
|
setChannelTopicItemUnsealedDetail: async (guid, channelId, topicId, revision, unsealed) => {
|
||||||
await db.current.executeSql(`UPDATE channel_topic_${guid} set unsealed_detail=? where detail_revision=? AND channel_id=? AND topic_id=?`, [encodeObject(unsealed), revision, channelId, topicId]);
|
await db.current.executeSql(`UPDATE channel_topic_${guid} set unsealed_detail=? where detail_revision=? AND channel_id=? AND topic_id=?`, [encodeObject(unsealed), revision, channelId, topicId]);
|
||||||
@ -329,11 +371,30 @@ export function useStoreContext() {
|
|||||||
detailRevision: topic.detail_revision,
|
detailRevision: topic.detail_revision,
|
||||||
detail: decodeObject(topic.detail),
|
detail: decodeObject(topic.detail),
|
||||||
unsealedDetail: decodeObject(topic.unsealed_detail),
|
unsealedDetail: decodeObject(topic.unsealed_detail),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
getCardChannelTopicItemsId: async (guid, cardId, channelId) => {
|
||||||
|
const values = await getAppValues(db.current, `SELECT topic_id, created FROM card_channel_topic_${guid} WHERE card_id=? AND channel_id=?`, [cardId, channelId]);
|
||||||
|
return values.map(topic => ({
|
||||||
|
topicId: topic.topic_id,
|
||||||
|
created: topic.created,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
getCardChannelTopicItemsById: async (guid, cardId, channelId, topics) => {
|
||||||
|
const q = topics.map(() => '?');
|
||||||
|
const values = await getAppValues(db.current, `SELECT topic_id, revision, blocked, detail_revision, detail, unsealed_detail FROM card_channel_topic_${guid} WHERE card_id=? AND channel_id=? AND topic_id in (${q.join(',')})`, [cardId, channelId, ...topics]);
|
||||||
|
return values.map(topic => ({
|
||||||
|
topicId: topic.topic_id,
|
||||||
|
revision: topic.revision,
|
||||||
|
blocked: topic.blocked,
|
||||||
|
detailRevision: topic.detail_revision,
|
||||||
|
detail: decodeObject(topic.detail),
|
||||||
|
unsealedDetail: decodeObject(topic.unsealed_detail),
|
||||||
|
}));
|
||||||
|
},
|
||||||
setCardChannelTopicItem: async (guid, cardId, channelId, topic) => {
|
setCardChannelTopicItem: async (guid, cardId, channelId, topic) => {
|
||||||
const { topicId, revision, detailRevision, detail } = topic;
|
const { topicId, revision, detailRevision, detail } = topic;
|
||||||
await db.current.executeSql(`INSERT OR REPLACE INTO card_channel_topic_${guid} (card_id, channel_id, topic_id, revision, detail_revision, detail, unsealed_detail) values (?, ?, ?, ?, ?, ?, null);`, [cardId, channelId, topicId, revision, detailRevision, encodeObject(detail)]);
|
await db.current.executeSql(`INSERT OR REPLACE INTO card_channel_topic_${guid} (card_id, channel_id, topic_id, revision, created, detail_revision, detail, unsealed_detail) values (?, ?, ?, ?, ?, ?, ?, null);`, [cardId, channelId, topicId, revision, topic?.created, detailRevision, encodeObject(detail)]);
|
||||||
},
|
},
|
||||||
setCardChannelTopicItemUnsealedDetail: async (guid, cardId, channelId, topicId, revision, unsealed) => {
|
setCardChannelTopicItemUnsealedDetail: async (guid, cardId, channelId, topicId, revision, unsealed) => {
|
||||||
await db.current.executeSql(`UPDATE card_channel_topic_${guid} set unsealed_detail=? where detail_revision=? AND card_id=? AND channel_id=? AND topic_id=?`, [encodeObject(unsealed), revision, cardId, channelId, topicId]);
|
await db.current.executeSql(`UPDATE card_channel_topic_${guid} set unsealed_detail=? where detail_revision=? AND card_id=? AND channel_id=? AND topic_id=?`, [encodeObject(unsealed), revision, cardId, channelId, topicId]);
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import axios from 'axios';
|
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 = (1024 * 1024);
|
||||||
|
|
||||||
export function useUploadContext() {
|
export function useUploadContext() {
|
||||||
|
|
||||||
@ -58,14 +63,12 @@ export function useUploadContext() {
|
|||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
addTopic: (node, token, channelId, topicId, files, success, failure, cardId) => {
|
addTopic: (node, token, channelId, topicId, files, success, failure, cardId) => {
|
||||||
const url = cardId ?
|
|
||||||
`https://${node}/content/channels/${channelId}/topics/${topicId}/assets?contact=${token}` :
|
|
||||||
`https://${node}/content/channels/${channelId}/topics/${topicId}/assets?agent=${token}`;
|
|
||||||
const key = cardId ? `${cardId}:${channelId}` : `:${channelId}`;
|
const key = cardId ? `${cardId}:${channelId}` : `:${channelId}`;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const entry = {
|
const entry = {
|
||||||
index: index.current,
|
index: index.current,
|
||||||
url: url,
|
baseUrl: cardId ? `https://${node}/content/channels/${channelId}/topics/${topicId}/` : `https://${node}/content/channels/${channelId}/topics/${topicId}/`,
|
||||||
|
urlParams: cardId ? `?contact=${token}` : `?agent=${token}`,
|
||||||
files,
|
files,
|
||||||
assets: [],
|
assets: [],
|
||||||
current: null,
|
current: null,
|
||||||
@ -116,6 +119,23 @@ export function useUploadContext() {
|
|||||||
return { state, actions }
|
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) {
|
async function upload(entry, update, complete) {
|
||||||
if (!entry.files?.length) {
|
if (!entry.files?.length) {
|
||||||
try {
|
try {
|
||||||
@ -133,7 +153,30 @@ async function upload(entry, update, complete) {
|
|||||||
const file = entry.files.shift();
|
const file = entry.files.shift();
|
||||||
entry.active = {};
|
entry.active = {};
|
||||||
try {
|
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 len = pos + ENCRYPTED_BLOCK_SIZE > size ? size - pos : ENCRYPTED_BLOCK_SIZE;
|
||||||
|
const { blockEncrypted, blockIv } = await getEncryptedBlock(pos, len);
|
||||||
|
const part = await axios.post(`${entry.baseUrl}blocks${entry.urlParams}`, blockEncrypted, {
|
||||||
|
headers: {'Content-Type': 'text/plain'},
|
||||||
|
signal: entry.cancel.signal,
|
||||||
|
onUploadProgress: (ev) => {
|
||||||
|
const { loaded, total } = ev;
|
||||||
|
const partLoaded = pos + Math.floor(len * loaded / total);
|
||||||
|
entry.active = { loaded: partLoaded, total: size }
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parts.push({ blockIv, partId: part.data.assetId });
|
||||||
|
}
|
||||||
|
entry.assets.push({
|
||||||
|
encrypted: { type, thumb, parts }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (file.type === 'image') {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (file.data.startsWith('file:')) {
|
if (file.data.startsWith('file:')) {
|
||||||
formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'});
|
formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'});
|
||||||
@ -142,7 +185,7 @@ async function upload(entry, update, complete) {
|
|||||||
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
|
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
|
||||||
}
|
}
|
||||||
let transform = encodeURIComponent(JSON.stringify(["ithumb;photo", "ilg;photo"]));
|
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, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
signal: entry.cancel.signal,
|
signal: entry.cancel.signal,
|
||||||
onUploadProgress: (ev) => {
|
onUploadProgress: (ev) => {
|
||||||
@ -168,7 +211,7 @@ async function upload(entry, update, complete) {
|
|||||||
}
|
}
|
||||||
let thumb = 'vthumb;video;' + file.position;
|
let thumb = 'vthumb;video;' + file.position;
|
||||||
let transform = encodeURIComponent(JSON.stringify(["vlq;video", "vhd;video", thumb]));
|
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, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
signal: entry.cancel.signal,
|
signal: entry.cancel.signal,
|
||||||
onUploadProgress: (ev) => {
|
onUploadProgress: (ev) => {
|
||||||
@ -194,7 +237,7 @@ async function upload(entry, update, complete) {
|
|||||||
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
|
formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'});
|
||||||
}
|
}
|
||||||
let transform = encodeURIComponent(JSON.stringify(["acopy;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, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
signal: entry.cancel.signal,
|
signal: entry.cancel.signal,
|
||||||
onUploadProgress: (ev) => {
|
onUploadProgress: (ev) => {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "src"
|
"name": "src",
|
||||||
|
"dependencies": {
|
||||||
|
"react-native-fs": "^2.20.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,22 +154,22 @@ export function AddTopic({ contentKey, shareIntent, setShareIntent }) {
|
|||||||
blurOnSubmit={true} onSubmitEditing={sendMessage} returnKeyType="send"
|
blurOnSubmit={true} onSubmitEditing={sendMessage} returnKeyType="send"
|
||||||
autoCapitalize="sentences" placeholder="New Message" multiline={true} />
|
autoCapitalize="sentences" placeholder="New Message" multiline={true} />
|
||||||
<View style={styles.addButtons}>
|
<View style={styles.addButtons}>
|
||||||
{ !state.locked && state.enableImage && (
|
{ state.enableImage && (
|
||||||
<TouchableOpacity style={styles.addButton} onPress={addImage}>
|
<TouchableOpacity style={styles.addButton} onPress={addImage}>
|
||||||
<AntIcons name="picture" size={20} color={Colors.text} />
|
<AntIcons name="picture" size={20} color={Colors.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{ !state.locked && state.enableVideo && (
|
{ state.enableVideo && (
|
||||||
<TouchableOpacity style={styles.addButton} onPress={addVideo}>
|
<TouchableOpacity style={styles.addButton} onPress={addVideo}>
|
||||||
<MatIcons name="video-outline" size={24} color={Colors.text} />
|
<MatIcons name="video-outline" size={24} color={Colors.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{ !state.locked && state.enableAudio && (
|
{ state.enableAudio && (
|
||||||
<TouchableOpacity style={styles.addButton} onPress={addAudio}>
|
<TouchableOpacity style={styles.addButton} onPress={addAudio}>
|
||||||
<MatIcons name="music-box-outline" size={20} color={Colors.text} />
|
<MatIcons name="music-box-outline" size={20} color={Colors.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{ !state.locked && (
|
{ (state.enableImage || state.enableVideo || state.enableAudio) && (
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity style={styles.addButton} onPress={actions.showFontSize}>
|
<TouchableOpacity style={styles.addButton} onPress={actions.showFontSize}>
|
||||||
|
@ -3,8 +3,10 @@ import { UploadContext } from 'context/UploadContext';
|
|||||||
import { ConversationContext } from 'context/ConversationContext';
|
import { ConversationContext } from 'context/ConversationContext';
|
||||||
import { Image } from 'react-native';
|
import { Image } from 'react-native';
|
||||||
import Colors from 'constants/Colors';
|
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 { AccountContext } from 'context/AccountContext';
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
import ImageResizer from '@bam.tech/react-native-image-resizer';
|
||||||
|
|
||||||
export function useAddTopic(contentKey) {
|
export function useAddTopic(contentKey) {
|
||||||
|
|
||||||
@ -37,9 +39,6 @@ export function useAddTopic(contentKey) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let conflict = false;
|
let conflict = false;
|
||||||
if (state.locked && state.assets.length > 0) {
|
|
||||||
conflict = true;
|
|
||||||
}
|
|
||||||
state.assets.forEach(asset => {
|
state.assets.forEach(asset => {
|
||||||
if (asset.type === 'image' && !state.enableImage) {
|
if (asset.type === 'image' && !state.enableImage) {
|
||||||
conflict = true;
|
conflict = true;
|
||||||
@ -54,6 +53,10 @@ export function useAddTopic(contentKey) {
|
|||||||
updateState({ conflict });
|
updateState({ conflict });
|
||||||
}, [state.assets, state.locked, state.enableImage, state.enableAudio, state.enableVideo]);
|
}, [state.assets, state.locked, state.enableImage, state.enableAudio, state.enableVideo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateState({ assets: [] });
|
||||||
|
}, [contentKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cardId = conversation.state.card?.card?.cardId;
|
const cardId = conversation.state.card?.card?.cardId;
|
||||||
const channelId = conversation.state.channel?.channelId;
|
const channelId = conversation.state.channel?.channelId;
|
||||||
@ -100,29 +103,55 @@ export function useAddTopic(contentKey) {
|
|||||||
updateState({ enableImage, enableAudio, enableVideo, locked });
|
updateState({ enableImage, enableAudio, enableVideo, locked });
|
||||||
}, [conversation.state]);
|
}, [conversation.state]);
|
||||||
|
|
||||||
|
const setAsset = async (file, scale) => {
|
||||||
|
const url = file.startsWith('file:') ? file : `file://${file}`;
|
||||||
|
if (contentKey) {
|
||||||
|
const scaled = scale ? await scale(url) : url;
|
||||||
|
const stat = await RNFS.stat(scaled);
|
||||||
|
const getEncryptedBlock = async (pos, len) => {
|
||||||
|
if (pos + len > stat.size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const block = await RNFS.read(scaled, len, pos, 'base64');
|
||||||
|
return encryptBlock(block, contentKey);
|
||||||
|
}
|
||||||
|
return { data: url, encrypted: true, size: stat.size, getEncryptedBlock };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { data: url, encrypted: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
setMessage: (message) => {
|
setMessage: (message) => {
|
||||||
updateState({ message });
|
updateState({ message });
|
||||||
},
|
},
|
||||||
addImage: (data) => {
|
addImage: async (data) => {
|
||||||
const url = data.startsWith('file:') ? data : 'file://' + data;
|
|
||||||
|
|
||||||
assetId.current++;
|
assetId.current++;
|
||||||
Image.getSize(url, (width, height) => {
|
const asset = await setAsset(data, async (file) => {
|
||||||
const asset = { key: assetId.current, type: 'image', data: url, ratio: width/height };
|
const scaled = await ImageResizer.createResizedImage(file, 512, 512, "JPEG", 90, 0, null);
|
||||||
updateState({ assets: [ ...state.assets, asset ] });
|
return `file://${scaled.path}`;
|
||||||
})
|
});
|
||||||
},
|
asset.key = assetId.current;
|
||||||
addVideo: (data) => {
|
asset.type = 'image';
|
||||||
const url = data.startsWith('file:') ? data : 'file://' + data
|
asset.ratio = 1;
|
||||||
assetId.current++;
|
|
||||||
const asset = { key: assetId.current, type: 'video', data: url, ratio: 1, duration: 0, position: 0 };
|
|
||||||
updateState({ assets: [ ...state.assets, asset ] });
|
updateState({ assets: [ ...state.assets, asset ] });
|
||||||
},
|
},
|
||||||
addAudio: (data, label) => {
|
addVideo: async (data) => {
|
||||||
const url = data.startsWith('file:') ? data : 'file://' + data
|
|
||||||
assetId.current++;
|
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 ] });
|
updateState({ assets: [ ...state.assets, asset ] });
|
||||||
},
|
},
|
||||||
setVideoPosition: (key, position) => {
|
setVideoPosition: (key, position) => {
|
||||||
@ -181,24 +210,16 @@ export function useAddTopic(contentKey) {
|
|||||||
|
|
||||||
const assemble = (assets) => {
|
const assemble = (assets) => {
|
||||||
if (!state.locked) {
|
if (!state.locked) {
|
||||||
if (assets?.length) {
|
return {
|
||||||
return {
|
assets: assets?.length ? assets : null,
|
||||||
assets,
|
text: state.message,
|
||||||
text: state.message,
|
textColor: state.colorSet ? state.color : null,
|
||||||
textColor: state.colorSet ? state.color : null,
|
textSize: state.sizeSet ? state.size : null,
|
||||||
textSize: state.sizeSet ? state.size : null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return {
|
|
||||||
text: state.message,
|
|
||||||
textColor: state.colorSet ? state.color : null,
|
|
||||||
textSize: state.sizeSet ? state.size : null,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const message = {
|
const message = {
|
||||||
|
assets: assets?.length ? assets : null,
|
||||||
text: state.message,
|
text: state.message,
|
||||||
textColor: state.textColorSet ? state.textColor : null,
|
textColor: state.textColorSet ? state.textColor : null,
|
||||||
textSize: state.textSizeSet ? state.textSize : null,
|
textSize: state.textSizeSet ? state.textSize : null,
|
||||||
|
@ -13,7 +13,7 @@ import { ImageAsset } from './imageAsset/ImageAsset';
|
|||||||
import { AudioAsset } from './audioAsset/AudioAsset';
|
import { AudioAsset } from './audioAsset/AudioAsset';
|
||||||
import { VideoAsset } from './videoAsset/VideoAsset';
|
import { VideoAsset } from './videoAsset/VideoAsset';
|
||||||
import Carousel from 'react-native-reanimated-carousel';
|
import Carousel from 'react-native-reanimated-carousel';
|
||||||
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
export function TopicItem({ item, focused, focus, hosting, remove, update, block, report, contentKey }) {
|
export function TopicItem({ item, focused, focus, hosting, remove, update, block, report, contentKey }) {
|
||||||
|
|
||||||
const { state, actions } = useTopicItem(item, hosting, remove, contentKey);
|
const { state, actions } = useTopicItem(item, hosting, remove, contentKey);
|
||||||
@ -109,34 +109,17 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const renderAsset = (asset) => {
|
|
||||||
return (
|
|
||||||
<View style={styles.frame}>
|
|
||||||
{ asset.item.image && (
|
|
||||||
<ImageAsset topicId={item.topicId} asset={asset.item.image} dismiss={actions.hideCarousel} />
|
|
||||||
)}
|
|
||||||
{ asset.item.video && (
|
|
||||||
<VideoAsset topicId={item.topicId} asset={asset.item.video} dismiss={actions.hideCarousel} />
|
|
||||||
)}
|
|
||||||
{ asset.item.audio && (
|
|
||||||
<AudioAsset topicId={item.topicId} asset={asset.item.audio} dismiss={actions.hideCarousel} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderThumb = (thumb) => {
|
const renderThumb = (thumb) => {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
{ thumb.item.image && (
|
{ thumb.item.type === 'image' && (
|
||||||
<ImageThumb topicId={item.topicId} asset={thumb.item.image} onAssetView={() => actions.showCarousel(thumb.index)} />
|
<ImageThumb url={thumb.item.thumb} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||||
)}
|
)}
|
||||||
{ thumb.item.video && (
|
{ thumb.item.type === 'video' && (
|
||||||
<VideoThumb topicId={item.topicId} asset={thumb.item.video} onAssetView={() => actions.showCarousel(thumb.index)} />
|
<VideoThumb url={thumb.item.thumb} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||||
)}
|
)}
|
||||||
{ thumb.item.audio && (
|
{ thumb.item.type === 'audio' && (
|
||||||
<AudioThumb topicId={item.topicId} asset={thumb.item.audio} onAssetView={() => actions.showCarousel(thumb.index)} />
|
<AudioThumb labe={thumb.item.label} onAssetView={() => actions.showCarousel(thumb.index)} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@ -194,7 +177,7 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block
|
|||||||
{ state.sharing && (
|
{ state.sharing && (
|
||||||
<ActivityIndicator style={styles.share} color={Colors.white} size="small" />
|
<ActivityIndicator style={styles.share} color={Colors.white} size="small" />
|
||||||
)}
|
)}
|
||||||
{ !state.sharing && (
|
{ !state.sharing && contentKey == null && (
|
||||||
<TouchableOpacity style={styles.share} onPress={shareMessage}>
|
<TouchableOpacity style={styles.share} onPress={shareMessage}>
|
||||||
<MatIcons name="share-variant-outline" size={18} color={Colors.white} />
|
<MatIcons name="share-variant-outline" size={18} color={Colors.white} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -229,26 +212,29 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block
|
|||||||
onRequestClose={actions.hideCarousel}
|
onRequestClose={actions.hideCarousel}
|
||||||
>
|
>
|
||||||
<View style={styles.modal}>
|
<View style={styles.modal}>
|
||||||
<Carousel
|
<GestureHandlerRootView>
|
||||||
loop
|
<Carousel
|
||||||
width={state.width}
|
loop
|
||||||
autoPlay={false}
|
width={state.width}
|
||||||
data={state.assets}
|
autoPlay={false}
|
||||||
defaultIndex={state.carouselIndex}
|
data={state.assets}
|
||||||
scrollAnimationDuration={1000}
|
defaultIndex={state.carouselIndex}
|
||||||
renderItem={({ index }) => (
|
scrollAnimationDuration={1000}
|
||||||
<View style={styles.frame}>
|
onSnapToItem={(index) => console.log('current index:', index)}
|
||||||
{ state.assets[index].image && (
|
renderItem={({ index }) => (
|
||||||
<ImageAsset topicId={item.topicId} asset={state.assets[index].image} dismiss={actions.hideCarousel} />
|
<View style={styles.frame}>
|
||||||
)}
|
{ state.assets[index].type === 'image' && (
|
||||||
{ state.assets[index].video && (
|
<ImageAsset asset={state.assets[index]} dismiss={actions.hideCarousel} />
|
||||||
<VideoAsset topicId={item.topicId} asset={state.assets[index].video} dismiss={actions.hideCarousel} />
|
)}
|
||||||
)}
|
{ state.assets[index].type === 'video' && (
|
||||||
{ state.assets[index].audio && (
|
<VideoAsset asset={state.assets[index]} dismiss={actions.hideCarousel} />
|
||||||
<AudioAsset topicId={item.topicId} asset={state.assets[index].audio} dismiss={actions.hideCarousel} />
|
)}
|
||||||
)}
|
{ state.assets[index].type === 'audio' && (
|
||||||
</View>
|
<AudioAsset asset={state.assets[index]} dismiss={actions.hideCarousel} />
|
||||||
)} />
|
)}
|
||||||
|
</View>
|
||||||
|
)} />
|
||||||
|
</GestureHandlerRootView>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Image, View, Text, TouchableOpacity } from 'react-native';
|
import { ActivityIndicator, Image, View, Text, TouchableOpacity } from 'react-native';
|
||||||
import { useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import Colors from 'constants/Colors';
|
import Colors from 'constants/Colors';
|
||||||
import Video from 'react-native-video';
|
import Video from 'react-native-video';
|
||||||
import { useAudioAsset } from './useAudioAsset.hook';
|
import { useAudioAsset } from './useAudioAsset.hook';
|
||||||
@ -8,9 +8,9 @@ import Icons from 'react-native-vector-icons/MaterialCommunityIcons';
|
|||||||
import audio from 'images/audio.png';
|
import audio from 'images/audio.png';
|
||||||
import { useKeepAwake } from '@sayem314/react-native-keep-awake';
|
import { useKeepAwake } from '@sayem314/react-native-keep-awake';
|
||||||
|
|
||||||
export function AudioAsset({ topicId, asset, dismiss }) {
|
export function AudioAsset({ asset, dismiss }) {
|
||||||
|
|
||||||
const { state, actions } = useAudioAsset(topicId, asset);
|
const { state, actions } = useAudioAsset(asset);
|
||||||
|
|
||||||
const player = useRef(null);
|
const player = useRef(null);
|
||||||
|
|
||||||
@ -37,6 +37,14 @@ export function AudioAsset({ topicId, asset, dismiss }) {
|
|||||||
<Video ref={player} source={{ uri: state.url }} repeat={true}
|
<Video ref={player} source={{ uri: state.url }} repeat={true}
|
||||||
paused={!state.playing} onLoad={actions.loaded} style={styles.player} />
|
paused={!state.playing} onLoad={actions.loaded} style={styles.player} />
|
||||||
)}
|
)}
|
||||||
|
{ !state.loaded && (
|
||||||
|
<TouchableOpacity style={styles.loading} onPress={dismiss}>
|
||||||
|
<ActivityIndicator color={Colors.black} size="large" />
|
||||||
|
{ asset.total > 1 && (
|
||||||
|
<Text style={styles.decrypting}>{ asset.block } / { asset.total }</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -37,5 +37,18 @@ export const styles = StyleSheet.create({
|
|||||||
player: {
|
player: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
},
|
},
|
||||||
|
loading: {
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
flexAlign: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
decrypting: {
|
||||||
|
fontVariant: ["tabular-nums"],
|
||||||
|
paddingTop: 16,
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#888888',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { ConversationContext } from 'context/ConversationContext';
|
|||||||
import { Image } from 'react-native';
|
import { Image } from 'react-native';
|
||||||
import { useWindowDimensions } from 'react-native';
|
import { useWindowDimensions } from 'react-native';
|
||||||
|
|
||||||
export function useAudioAsset(topicId, asset) {
|
export function useAudioAsset(asset) {
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
width: 1,
|
width: 1,
|
||||||
@ -38,9 +38,13 @@ export function useAudioAsset(topicId, asset) {
|
|||||||
}, [dimensions]);
|
}, [dimensions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.full);
|
if (asset.encrypted) {
|
||||||
updateState({ url });
|
updateState({ url: asset.decrypted, failed: asset.error });
|
||||||
}, [topicId, conversation, asset]);
|
}
|
||||||
|
else {
|
||||||
|
updateState({ url: asset.full });
|
||||||
|
}
|
||||||
|
}, [asset]);
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
play: () => {
|
play: () => {
|
||||||
|
@ -4,14 +4,14 @@ import { styles } from './AudioThumb.styled';
|
|||||||
import Colors from 'constants/Colors';
|
import Colors from 'constants/Colors';
|
||||||
import audio from 'images/audio.png';
|
import audio from 'images/audio.png';
|
||||||
|
|
||||||
export function AudioThumb({ topicId, asset, onAssetView }) {
|
export function AudioThumb({ label, onAssetView }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
||||||
<Image source={audio} style={{ borderRadius: 4, width: 92, height: 92, marginRight: 16, backgroundColor: Colors.lightgrey }} resizeMode={'cover'} />
|
<Image source={audio} style={{ borderRadius: 4, width: 92, height: 92, marginRight: 16, backgroundColor: Colors.lightgrey }} resizeMode={'cover'} />
|
||||||
{ asset.label && (
|
{ label && (
|
||||||
<View style={styles.overlay}>
|
<View style={styles.overlay}>
|
||||||
<Text style={styles.label}>{ asset.label }</Text>
|
<Text style={styles.label}>{ label }</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
import { View, Image, ActivityIndicator, TouchableOpacity } from 'react-native';
|
import { Text, View, Image, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||||
import { useImageAsset } from './useImageAsset.hook';
|
import { useImageAsset } from './useImageAsset.hook';
|
||||||
import { styles } from './ImageAsset.styled';
|
import { styles } from './ImageAsset.styled';
|
||||||
import Colors from 'constants/Colors';
|
import Colors from 'constants/Colors';
|
||||||
import Ionicons from 'react-native-vector-icons/AntDesign';
|
import Ionicons from 'react-native-vector-icons/AntDesign';
|
||||||
|
import FastImage from 'react-native-fast-image'
|
||||||
|
|
||||||
export function ImageAsset({ topicId, asset, dismiss }) {
|
export function ImageAsset({ asset, dismiss }) {
|
||||||
const { state, actions } = useImageAsset(topicId, asset);
|
const { state, actions } = useImageAsset(asset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.container} activeOpacity={1} onPress={actions.showControls}>
|
<TouchableOpacity style={styles.container} activeOpacity={1} onPress={actions.showControls}>
|
||||||
|
<FastImage source={{ uri: asset.thumb }} onLoad={actions.setRatio}
|
||||||
|
style={{ ...styles.thumb, width: state.imageWidth, height: state.imageHeight }}
|
||||||
|
resizeMode={FastImage.resizeMode.contain} />
|
||||||
{ state.url && (
|
{ state.url && (
|
||||||
<Image source={{ uri: state.url }} onLoad={actions.loaded} onError={actions.failed}
|
<FastImage source={{ uri: state.url }} onLoad={actions.loaded}
|
||||||
style={{ borderRadius: 4, width: state.imageWidth, height: state.imageHeight }} resizeMode={'cover'} />
|
style={{ ...styles.main, width: state.imageWidth, height: state.imageHeight }}
|
||||||
|
resizeMode={FastImage.resizeMode.contain} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ state.loaded && state.controls && (
|
{ state.loaded && state.controls && (
|
||||||
@ -28,9 +33,11 @@ export function ImageAsset({ topicId, asset, dismiss }) {
|
|||||||
{ !state.loaded && !state.failed && (
|
{ !state.loaded && !state.failed && (
|
||||||
<TouchableOpacity style={styles.loading} onPress={dismiss}>
|
<TouchableOpacity style={styles.loading} onPress={dismiss}>
|
||||||
<ActivityIndicator color={Colors.white} size="large" />
|
<ActivityIndicator color={Colors.white} size="large" />
|
||||||
|
{ asset.total > 1 && (
|
||||||
|
<Text style={styles.decrypting}>{ asset.block } / { asset.total }</Text>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,16 @@ export const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
flexAlign: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
decrypting: {
|
||||||
|
fontVariant: ["tabular-nums"],
|
||||||
|
paddingTop: 16,
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#dddddd',
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
marginRight: 16,
|
marginRight: 16,
|
||||||
@ -21,6 +31,16 @@ export const styles = StyleSheet.create({
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: Colors.divider,
|
borderColor: Colors.divider,
|
||||||
},
|
},
|
||||||
|
thumb: {
|
||||||
|
borderRadius: 4,
|
||||||
|
opacity: 0.3,
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
borderRadius: 4,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
},
|
||||||
close: {
|
close: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
|
@ -3,14 +3,14 @@ import { ConversationContext } from 'context/ConversationContext';
|
|||||||
import { Image } from 'react-native';
|
import { Image } from 'react-native';
|
||||||
import { useWindowDimensions } from 'react-native';
|
import { useWindowDimensions } from 'react-native';
|
||||||
|
|
||||||
export function useImageAsset(topicId, asset) {
|
export function useImageAsset(asset) {
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
frameWidth: 1,
|
frameWidth: 1,
|
||||||
frameHeight: 1,
|
frameHeight: 1,
|
||||||
imageRatio: 1,
|
imageRatio: 1,
|
||||||
imageWidth: 1,
|
imageWidth: 1024,
|
||||||
imageHeight: 1,
|
imageHeight: 1024,
|
||||||
url: null,
|
url: null,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
failed: false,
|
failed: false,
|
||||||
@ -30,14 +30,15 @@ export function useImageAsset(topicId, asset) {
|
|||||||
const frameRatio = state.frameWidth / state.frameHeight;
|
const frameRatio = state.frameWidth / state.frameHeight;
|
||||||
if (frameRatio > state.imageRatio) {
|
if (frameRatio > state.imageRatio) {
|
||||||
//height constrained
|
//height constrained
|
||||||
const height = 0.9 * state.frameHeight;
|
const height = Math.floor(0.9 * state.frameHeight);
|
||||||
const width = height * state.imageRatio;
|
const width = Math.floor(height * state.imageRatio);
|
||||||
|
|
||||||
updateState({ imageWidth: width, imageHeight: height });
|
updateState({ imageWidth: width, imageHeight: height });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//width constrained
|
//width constrained
|
||||||
const width = 0.9 * state.frameWidth;
|
const width = Math.floor(0.9 * state.frameWidth);
|
||||||
const height = width / state.imageRatio;
|
const height = Math.floor(width / state.imageRatio);
|
||||||
updateState({ imageWidth: width, imageHeight: height });
|
updateState({ imageWidth: width, imageHeight: height });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,20 +46,32 @@ export function useImageAsset(topicId, asset) {
|
|||||||
}, [state.frameWidth, state.frameHeight, state.imageRatio, state.loaded]);
|
}, [state.frameWidth, state.frameHeight, state.imageRatio, state.loaded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateState({ frameWidth: dimensions.width, frameHeight: dimensions.height });
|
imageWidth = dimensions.width * 0.9 > state.imageWidth ? state.imageWidth : dimensions.width * 0.9;
|
||||||
|
imageHeight = dimensions.height * 0.9 > state.imageHeight ? state.imageHeight : dimensions.height * 0.9;
|
||||||
|
updateState({ frameWidth: dimensions.width, frameHeight: dimensions.height, imageWidth, imageHeight });
|
||||||
}, [dimensions]);
|
}, [dimensions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.full);
|
if (asset.encrypted) {
|
||||||
updateState({ url });
|
const now = Date.now();
|
||||||
}, [topicId, conversation, asset]);
|
const url = asset.decrypted ? `file://${asset.decrypted}?now=${now}` : null
|
||||||
|
updateState({ url, failed: asset.error });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updateState({ url: asset.full, failed: false });
|
||||||
|
}
|
||||||
|
}, [asset]);
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
loaded: (e) => {
|
setRatio: (e) => {
|
||||||
const { width, height } = e.nativeEvent.source;
|
const { width, height } = e.nativeEvent;
|
||||||
updateState({ loaded: true, imageRatio: width / height });
|
updateState({ imageRatio: width / height });
|
||||||
},
|
},
|
||||||
failed: () => {
|
loaded: () => {
|
||||||
|
updateState({ loaded: true });
|
||||||
|
},
|
||||||
|
failed: (e) => {
|
||||||
|
console.log("FAILEE!!!", e);
|
||||||
updateState({ failed: true });
|
updateState({ failed: true });
|
||||||
},
|
},
|
||||||
showControls: () => {
|
showControls: () => {
|
||||||
|
@ -4,12 +4,12 @@ import { useImageThumb } from './useImageThumb.hook';
|
|||||||
import { styles } from './ImageThumb.styled';
|
import { styles } from './ImageThumb.styled';
|
||||||
import Colors from 'constants/Colors';
|
import Colors from 'constants/Colors';
|
||||||
|
|
||||||
export function ImageThumb({ topicId, asset, onAssetView }) {
|
export function ImageThumb({ url, onAssetView }) {
|
||||||
const { state, actions } = useImageThumb(topicId, asset);
|
const { state, actions } = useImageThumb();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
||||||
<Image source={{ uri: state.url }} style={{ opacity: state.loaded ? 1 : 0, borderRadius: 4, width: 92 * state.ratio, height: 92, marginRight: 16 }}
|
<Image source={{ uri: url }} style={{ opacity: state.loaded ? 1 : 0, borderRadius: 4, width: 92 * state.ratio, height: 92, marginRight: 16 }}
|
||||||
onLoad={actions.loaded} resizeMode={'cover'} />
|
onLoad={actions.loaded} resizeMode={'cover'} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useContext } from 'react';
|
|||||||
import { ConversationContext } from 'context/ConversationContext';
|
import { ConversationContext } from 'context/ConversationContext';
|
||||||
import { Image } from 'react-native';
|
import { Image } from 'react-native';
|
||||||
|
|
||||||
export function useImageThumb(topicId, asset) {
|
export function useImageThumb() {
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
loaded: false,
|
loaded: false,
|
||||||
@ -16,11 +16,6 @@ export function useImageThumb(topicId, asset) {
|
|||||||
setState((s) => ({ ...s, ...value }));
|
setState((s) => ({ ...s, ...value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.thumb);
|
|
||||||
updateState({ url });
|
|
||||||
}, [topicId, conversation, asset]);
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
loaded: (e) => {
|
loaded: (e) => {
|
||||||
const { width, height } = e.nativeEvent.source;
|
const { width, height } = e.nativeEvent.source;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useContext } from 'react';
|
import { useRef, useState, useEffect, useContext } from 'react';
|
||||||
import { Linking } from 'react-native';
|
import { Linking } from 'react-native';
|
||||||
import { ConversationContext } from 'context/ConversationContext';
|
import { ConversationContext } from 'context/ConversationContext';
|
||||||
import { CardContext } from 'context/CardContext';
|
import { CardContext } from 'context/CardContext';
|
||||||
@ -8,10 +8,12 @@ import moment from 'moment';
|
|||||||
import { useWindowDimensions, Text } from 'react-native';
|
import { useWindowDimensions, Text } from 'react-native';
|
||||||
import Colors from 'constants/Colors';
|
import Colors from 'constants/Colors';
|
||||||
import { getCardByGuid } from 'context/cardUtil';
|
import { getCardByGuid } from 'context/cardUtil';
|
||||||
import { decryptTopicSubject } from 'context/sealUtil';
|
import { decryptBlock, decryptTopicSubject } from 'context/sealUtil';
|
||||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||||
import Share from 'react-native-share';
|
import Share from 'react-native-share';
|
||||||
import RNFetchBlob from "rn-fetch-blob";
|
import RNFetchBlob from "rn-fetch-blob";
|
||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
import { checkResponse, fetchWithTimeout } from 'api/fetchUtil';
|
||||||
|
|
||||||
export function useTopicItem(item, hosting, remove, contentKey) {
|
export function useTopicItem(item, hosting, remove, contentKey) {
|
||||||
|
|
||||||
@ -42,6 +44,8 @@ export function useTopicItem(item, hosting, remove, contentKey) {
|
|||||||
const account = useContext(AccountContext);
|
const account = useContext(AccountContext);
|
||||||
const dimensions = useWindowDimensions();
|
const dimensions = useWindowDimensions();
|
||||||
|
|
||||||
|
const cancel = useRef(false);
|
||||||
|
|
||||||
const updateState = (value) => {
|
const updateState = (value) => {
|
||||||
setState((s) => ({ ...s, ...value }));
|
setState((s) => ({ ...s, ...value }));
|
||||||
}
|
}
|
||||||
@ -50,6 +54,43 @@ export function useTopicItem(item, hosting, remove, contentKey) {
|
|||||||
updateState({ width: dimensions.width, height: dimensions.height });
|
updateState({ width: dimensions.width, height: dimensions.height });
|
||||||
}, [dimensions]);
|
}, [dimensions]);
|
||||||
|
|
||||||
|
const setAssets = (parsed) => {
|
||||||
|
const assets = [];
|
||||||
|
if (parsed?.length) {
|
||||||
|
for (let i = 0; i < parsed.length; i++) {
|
||||||
|
const asset = parsed[i];
|
||||||
|
if (asset.encrypted) {
|
||||||
|
const encrypted = true;
|
||||||
|
const { type, thumb, label, parts } = asset.encrypted;
|
||||||
|
assets.push({ type, thumb, label, encrypted, decrypted: null, parts });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const encrypted = false
|
||||||
|
if (asset.image) {
|
||||||
|
const type = 'image';
|
||||||
|
const thumb = conversation.actions.getTopicAssetUrl(item.topicId, asset.image.thumb);
|
||||||
|
const full = conversation.actions.getTopicAssetUrl(item.topicId, asset.image.full);
|
||||||
|
assets.push({ type, thumb, encrypted, full });
|
||||||
|
}
|
||||||
|
else if (asset.video) {
|
||||||
|
const type = 'video';
|
||||||
|
const thumb = conversation.actions.getTopicAssetUrl(item.topicId, asset.video.thumb);
|
||||||
|
const lq = conversation.actions.getTopicAssetUrl(item.topicId, asset.video.lq);
|
||||||
|
const hd = conversation.actions.getTopicAssetUrl(item.topicId, asset.video.hd);
|
||||||
|
assets.push({ type, thumb, encrypted, lq, hd });
|
||||||
|
}
|
||||||
|
else if (asset.audio) {
|
||||||
|
const type = 'audio';
|
||||||
|
const label = asset.audio.label;
|
||||||
|
const full = conversation.actions.getTopicAssetUrl(item.topicId, asset.audio.full);
|
||||||
|
assets.push({ type, label, encrypted, full });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
const { topicId, revision, detail, unsealedDetail } = item;
|
const { topicId, revision, detail, unsealedDetail } = item;
|
||||||
@ -103,7 +144,7 @@ export function useTopicItem(item, hosting, remove, contentKey) {
|
|||||||
parsed = JSON.parse(data);
|
parsed = JSON.parse(data);
|
||||||
message = parsed?.text;
|
message = parsed?.text;
|
||||||
clickable = clickableText(parsed.text);
|
clickable = clickableText(parsed.text);
|
||||||
assets = parsed.assets;
|
assets = setAssets(parsed.assets);
|
||||||
if (parsed.textSize === 'small') {
|
if (parsed.textSize === 'small') {
|
||||||
fontSize = 10;
|
fontSize = 10;
|
||||||
}
|
}
|
||||||
@ -145,6 +186,7 @@ export function useTopicItem(item, hosting, remove, contentKey) {
|
|||||||
if (unsealed) {
|
if (unsealed) {
|
||||||
sealed = false;
|
sealed = false;
|
||||||
parsed = unsealed.message;
|
parsed = unsealed.message;
|
||||||
|
assets = setAssets(parsed.assets);
|
||||||
message = parsed?.text;
|
message = parsed?.text;
|
||||||
clickable = clickableText(parsed?.text);
|
clickable = clickableText(parsed?.text);
|
||||||
if (parsed?.textSize === 'small') {
|
if (parsed?.textSize === 'small') {
|
||||||
@ -231,11 +273,55 @@ export function useTopicItem(item, hosting, remove, contentKey) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
showCarousel: (index) => {
|
showCarousel: async (index) => {
|
||||||
updateState({ carousel: true, carouselIndex: index });
|
const assets = state.assets.map((asset) => ({ ...asset, error: false, decrypted: null }));
|
||||||
|
updateState({ assets, carousel: true, carouselIndex: index });
|
||||||
|
|
||||||
|
try {
|
||||||
|
cancel.current = false;
|
||||||
|
const assets = state.assets;
|
||||||
|
for (let i = 0; i < assets.length; i++) {
|
||||||
|
const cur = (i + index) % assets.length
|
||||||
|
const asset = assets[cur];
|
||||||
|
if (asset.encrypted) {
|
||||||
|
const ext = asset.type === 'video' ? '.mp4' : asset.type === 'audio' ? '.mp3' : '';
|
||||||
|
const path = RNFS.DocumentDirectoryPath + `/${i}.asset${ext}`;
|
||||||
|
const exists = await RNFS.exists(path);
|
||||||
|
if (exists) {
|
||||||
|
RNFS.unlink(path);
|
||||||
|
}
|
||||||
|
assets[cur] = { ...asset, block: 0, total: asset.parts.length };
|
||||||
|
updateState({ assets: [ ...assets ]});
|
||||||
|
for (let j = 0; j < asset.parts.length; j++) {
|
||||||
|
const part = asset.parts[j];
|
||||||
|
const url = conversation.actions.getTopicAssetUrl(item.topicId, part.partId);
|
||||||
|
const response = await fetchWithTimeout(url, { method: 'GET' });
|
||||||
|
const block = await response.text();
|
||||||
|
const decrypted = decryptBlock(block, part.blockIv, contentKey);
|
||||||
|
if (cancel.current) {
|
||||||
|
throw new Error("unseal assets cancelled");
|
||||||
|
}
|
||||||
|
await RNFS.appendFile(path, decrypted, 'base64');
|
||||||
|
|
||||||
|
assets[cur] = { ...asset, block: j+1, total: asset.parts.length };
|
||||||
|
updateState({ assets: [ ...assets ]});
|
||||||
|
};
|
||||||
|
|
||||||
|
asset.decrypted = path;
|
||||||
|
assets[cur] = { ...asset };
|
||||||
|
updateState({ assets: [ ...assets ]});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
const assets = state.assets.map((asset) => ({ ...asset, error: true }));
|
||||||
|
updateState({ assets: [ ...assets ]});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
hideCarousel: () => {
|
hideCarousel: () => {
|
||||||
updateState({ carousel: false });
|
updateState({ carousel: false });
|
||||||
|
cancel.current = true;
|
||||||
},
|
},
|
||||||
setActive: (activeId) => {
|
setActive: (activeId) => {
|
||||||
updateState({ activeId });
|
updateState({ activeId });
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
import { ActivityIndicator, Image, View, TouchableOpacity } from 'react-native';
|
import { ActivityIndicator, Image, Text, View, TouchableOpacity } from 'react-native';
|
||||||
import Colors from 'constants/Colors';
|
import Colors from 'constants/Colors';
|
||||||
import Video from 'react-native-video';
|
import Video from 'react-native-video';
|
||||||
import { useVideoAsset } from './useVideoAsset.hook';
|
import { useVideoAsset } from './useVideoAsset.hook';
|
||||||
import { styles } from './VideoAsset.styled';
|
import { styles } from './VideoAsset.styled';
|
||||||
import Icons from 'react-native-vector-icons/MaterialCommunityIcons';
|
import Icons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import { useKeepAwake } from '@sayem314/react-native-keep-awake';
|
import { useKeepAwake } from '@sayem314/react-native-keep-awake';
|
||||||
|
import FastImage from 'react-native-fast-image'
|
||||||
|
|
||||||
export function VideoAsset({ topicId, asset, dismiss }) {
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const { state, actions } = useVideoAsset(topicId, asset);
|
export function VideoAsset({ asset, dismiss }) {
|
||||||
|
|
||||||
|
const { state, actions } = useVideoAsset(asset);
|
||||||
|
|
||||||
useKeepAwake();
|
useKeepAwake();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<TouchableOpacity activeOpacity={1} style={styles.container} onPress={actions.showControls}>
|
<TouchableOpacity activeOpacity={1} style={styles.container} onPress={actions.showControls}>
|
||||||
|
<FastImage source={{ uri: asset.thumb }} onLoad={actions.setRatio}
|
||||||
|
style={{ ...styles.thumb, width: state.thumbWidth, height: state.thumbHeight }}
|
||||||
|
resizeMode={FastImage.resizeMode.contain} />
|
||||||
{ state.url && (
|
{ state.url && (
|
||||||
<Video source={{ uri: state.url }} style={{ width: state.width, height: state.height }} resizeMode={'cover'}
|
<Video source={{ uri: state.url, type: 'video/mp4' }} style={{ ...styles.main, width: state.width, height: state.height }}
|
||||||
onReadyForDisplay={(e) => { console.log(e) }}
|
resizeMode={'cover'} onReadyForDisplay={(e) => { console.log(e) }}
|
||||||
onLoad={actions.loaded} repeat={true} paused={!state.playing} resizeMode="contain" />
|
onLoad={actions.loaded} repeat={true} paused={!state.playing} resizeMode="contain" />
|
||||||
)}
|
)}
|
||||||
{ (!state.playing || state.controls) && (
|
{ (!state.playing || state.controls) && (
|
||||||
@ -42,6 +48,9 @@ export function VideoAsset({ topicId, asset, dismiss }) {
|
|||||||
{ !state.loaded && (
|
{ !state.loaded && (
|
||||||
<TouchableOpacity style={styles.loading} onPress={dismiss}>
|
<TouchableOpacity style={styles.loading} onPress={dismiss}>
|
||||||
<ActivityIndicator color={Colors.white} size="large" />
|
<ActivityIndicator color={Colors.white} size="large" />
|
||||||
|
{ asset.total > 0 && (
|
||||||
|
<Text style={styles.decrypting}>{ asset.block } / { asset.total }</Text>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
@ -16,6 +16,13 @@ export const styles = StyleSheet.create({
|
|||||||
paddingRight: 8,
|
paddingRight: 8,
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
},
|
},
|
||||||
|
thumb: {
|
||||||
|
borderRadius: 4,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
position: 'absolute',
|
||||||
|
},
|
||||||
close: {
|
close: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -27,6 +34,16 @@ export const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
flexAlign: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
decrypting: {
|
||||||
|
fontVariant: ["tabular-nums"],
|
||||||
|
paddingTop: 16,
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#dddddd',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -3,13 +3,17 @@ import { ConversationContext } from 'context/ConversationContext';
|
|||||||
import { Image } from 'react-native';
|
import { Image } from 'react-native';
|
||||||
import { useWindowDimensions } from 'react-native';
|
import { useWindowDimensions } from 'react-native';
|
||||||
|
|
||||||
export function useVideoAsset(topicId, asset) {
|
export function useVideoAsset(asset) {
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
frameWidth: 1,
|
frameWidth: 1,
|
||||||
frameHeight: 1,
|
frameHeight: 1,
|
||||||
videoRatio: 1,
|
videoRatio: 1,
|
||||||
|
thumbRatio: 1,
|
||||||
width: 1,
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
thumbWidth: 64,
|
||||||
|
thumbHeight: 64,
|
||||||
url: null,
|
url: null,
|
||||||
playing: false,
|
playing: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
@ -27,6 +31,18 @@ export function useVideoAsset(topicId, asset) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const frameRatio = state.frameWidth / state.frameHeight;
|
const frameRatio = state.frameWidth / state.frameHeight;
|
||||||
|
if (frameRatio > state.thumbRatio) {
|
||||||
|
//thumbHeight constrained
|
||||||
|
const thumbHeight = 0.9 * state.frameHeight;
|
||||||
|
const thumbWidth = thumbHeight * state.thumbRatio;
|
||||||
|
updateState({ thumbWidth, thumbHeight });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//thumbWidth constrained
|
||||||
|
const thumbWidth = 0.9 * state.frameWidth;
|
||||||
|
const thumbHeight = thumbWidth / state.thumbRatio;
|
||||||
|
updateState({ thumbWidth, thumbHeight });
|
||||||
|
}
|
||||||
if (frameRatio > state.videoRatio) {
|
if (frameRatio > state.videoRatio) {
|
||||||
//height constrained
|
//height constrained
|
||||||
const height = 0.9 * state.frameHeight;
|
const height = 0.9 * state.frameHeight;
|
||||||
@ -39,18 +55,26 @@ export function useVideoAsset(topicId, asset) {
|
|||||||
const height = width / state.videoRatio;
|
const height = width / state.videoRatio;
|
||||||
updateState({ width, height });
|
updateState({ width, height });
|
||||||
}
|
}
|
||||||
}, [state.frameWidth, state.frameHeight, state.videoRatio]);
|
}, [state.frameWidth, state.frameHeight, state.videoRatio, state.thumbRatio]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateState({ frameWidth: dimensions.width, frameHeight: dimensions.height });
|
updateState({ frameWidth: dimensions.width, frameHeight: dimensions.height });
|
||||||
}, [dimensions]);
|
}, [dimensions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.hd);
|
if (asset.encrypted) {
|
||||||
updateState({ url });
|
updateState({ url: asset.decrypted, failed: asset.error });
|
||||||
}, [topicId, conversation, asset]);
|
}
|
||||||
|
else {
|
||||||
|
updateState({ url: asset.hd });
|
||||||
|
}
|
||||||
|
}, [asset]);
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
setRatio: (e) => {
|
||||||
|
const { width, height } = e.nativeEvent;
|
||||||
|
updateState({ thumbRatio: width / height });
|
||||||
|
},
|
||||||
setResolution: (width, height) => {
|
setResolution: (width, height) => {
|
||||||
updateState({ display: {}, videoRatio: width / height });
|
updateState({ display: {}, videoRatio: width / height });
|
||||||
},
|
},
|
||||||
|
@ -5,12 +5,13 @@ import { styles } from './VideoThumb.styled';
|
|||||||
import Colors from 'constants/Colors';
|
import Colors from 'constants/Colors';
|
||||||
import AntIcons from 'react-native-vector-icons/AntDesign';
|
import AntIcons from 'react-native-vector-icons/AntDesign';
|
||||||
|
|
||||||
export function VideoThumb({ topicId, asset, onAssetView }) {
|
export function VideoThumb({ url, onAssetView }) {
|
||||||
const { state, actions } = useVideoThumb(topicId, asset);
|
const { state, actions } = useVideoThumb();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
<TouchableOpacity activeOpacity={1} onPress={onAssetView}>
|
||||||
<Image source={{ uri: state.url }} style={{ borderRadius: 4, width: 92 * state.ratio, height: 92, marginRight: 16 }} resizeMode={'cover'} />
|
<Image source={{ uri: url }} style={{ opacity: state.loaded ? 1 : 0, borderRadius: 4, width: 92 * state.ratio, height: 92, marginRight: 16 }}
|
||||||
|
onLoad={actions.loaded} resizeMode={'cover'} />
|
||||||
<View style={styles.overlay}>
|
<View style={styles.overlay}>
|
||||||
<AntIcons name="caretright" size={20} color={Colors.white} />
|
<AntIcons name="caretright" size={20} color={Colors.white} />
|
||||||
</View>
|
</View>
|
||||||
|
@ -2,11 +2,10 @@ import { useState, useRef, useEffect, useContext } from 'react';
|
|||||||
import { ConversationContext } from 'context/ConversationContext';
|
import { ConversationContext } from 'context/ConversationContext';
|
||||||
import { Image } from 'react-native';
|
import { Image } from 'react-native';
|
||||||
|
|
||||||
export function useVideoThumb(topicId, asset) {
|
export function useVideoThumb() {
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
ratio: 1,
|
ratio: 1,
|
||||||
url: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const conversation = useContext(ConversationContext);
|
const conversation = useContext(ConversationContext);
|
||||||
@ -15,16 +14,11 @@ export function useVideoThumb(topicId, asset) {
|
|||||||
setState((s) => ({ ...s, ...value }));
|
setState((s) => ({ ...s, ...value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const url = conversation.actions.getTopicAssetUrl(topicId, asset.thumb);
|
|
||||||
if (url) {
|
|
||||||
Image.getSize(url, (width, height) => {
|
|
||||||
updateState({ url, ratio: width / height });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [topicId, conversation, asset]);
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
loaded: (e) => {
|
||||||
|
const { width, height } = e.nativeEvent.source;
|
||||||
|
updateState({ loaded: true, ratio: width / height });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { state, actions };
|
return { state, actions };
|
||||||
|
@ -1072,6 +1072,11 @@
|
|||||||
"@babel/helper-validator-identifier" "^7.19.1"
|
"@babel/helper-validator-identifier" "^7.19.1"
|
||||||
to-fast-properties "^2.0.0"
|
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":
|
"@bcoe/v8-coverage@^0.2.3":
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
|
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"
|
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
base-64@0.1.0:
|
base-64@0.1.0, base-64@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
|
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
|
||||||
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
|
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
|
||||||
@ -6305,6 +6310,11 @@ react-native-codegen@^0.71.5:
|
|||||||
jscodeshift "^0.13.1"
|
jscodeshift "^0.13.1"
|
||||||
nullthrows "^1.1.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:
|
react-native-device-info@^10.4.0:
|
||||||
version "10.4.0"
|
version "10.4.0"
|
||||||
resolved "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.4.0.tgz"
|
resolved "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.4.0.tgz"
|
||||||
@ -6322,6 +6332,19 @@ react-native-elevation@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-native-elevation/-/react-native-elevation-1.0.0.tgz#2a091c688290ac9b08b5842d1a8e8a00fc84233e"
|
resolved "https://registry.yarnpkg.com/react-native-elevation/-/react-native-elevation-1.0.0.tgz#2a091c688290ac9b08b5842d1a8e8a00fc84233e"
|
||||||
integrity sha512-BWIKcEYtzjRV6GpkX0Km5/w2E7fgIcywiQOT7JZTc5NSbv/YI9kpFinB9lRFsOoRVGmiqq/O3VfP/oH2clIiBA==
|
integrity sha512-BWIKcEYtzjRV6GpkX0Km5/w2E7fgIcywiQOT7JZTc5NSbv/YI9kpFinB9lRFsOoRVGmiqq/O3VfP/oH2clIiBA==
|
||||||
|
|
||||||
|
react-native-fast-image@^8.6.3:
|
||||||
|
version "8.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz#6edc3f9190092a909d636d93eecbcc54a8822255"
|
||||||
|
integrity sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==
|
||||||
|
|
||||||
|
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:
|
react-native-gesture-handler@^2.9.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz#2f63812e523c646f25b9ad660fc6f75948e51241"
|
||||||
@ -7538,6 +7561,11 @@ use@^3.1.0:
|
|||||||
resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz"
|
resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz"
|
||||||
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
|
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:
|
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||||
|
46
doc/api.oa3
46
doc/api.oa3
@ -3248,6 +3248,52 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
|
|
||||||
|
/content/channels/{channelId}/topics/{topicId}/blocks:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- content
|
||||||
|
description: Add a asset to the channel. Payload is a file block encoded as bas64 string. This is to support e2e as the client side will encrypt the file block before applying the base64 encoding.
|
||||||
|
operationId: add-channel-topic-block
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: channelId
|
||||||
|
in: path
|
||||||
|
description: specified channel id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: topicId
|
||||||
|
in: path
|
||||||
|
description: specified topic id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: success
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Asset'
|
||||||
|
'401':
|
||||||
|
description: permission denied
|
||||||
|
'404':
|
||||||
|
description: channel not found
|
||||||
|
'406':
|
||||||
|
description: storage limit reached
|
||||||
|
'410':
|
||||||
|
description: account disabled
|
||||||
|
'500':
|
||||||
|
description: internal server error
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
/content/channels/{channelId}/topics/{topicId}/assets/{assetId}:
|
/content/channels/{channelId}/topics/{topicId}/assets/{assetId}:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -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 \
|
&& 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
|
&& 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 config set network-timeout 300000
|
||||||
RUN yarn --cwd /app/databag/net/web install
|
RUN yarn --cwd /app/databag/net/web install
|
||||||
|
@ -151,23 +151,6 @@ func AddChannelTopicAsset(w http.ResponseWriter, r *http.Request) {
|
|||||||
// invoke transcoder
|
// invoke transcoder
|
||||||
transcode()
|
transcode()
|
||||||
|
|
||||||
// determine affected contact list
|
|
||||||
cards := make(map[string]store.Card)
|
|
||||||
for _, member := range channelSlot.Channel.Members {
|
|
||||||
cards[member.Card.GUID] = member.Card
|
|
||||||
}
|
|
||||||
for _, group := range channelSlot.Channel.Groups {
|
|
||||||
for _, card := range group.Cards {
|
|
||||||
cards[card.GUID] = card
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// notify
|
|
||||||
SetStatus(act)
|
|
||||||
for _, card := range cards {
|
|
||||||
SetContactChannelNotification(act, &card)
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteResponse(w, &assets)
|
WriteResponse(w, &assets)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,6 +193,10 @@ func saveAsset(src io.Reader, path string) (crc uint32, size int64, err error) {
|
|||||||
data := make([]byte, 4096)
|
data := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
n, res := src.Read(data)
|
n, res := src.Read(data)
|
||||||
|
if n > 0 {
|
||||||
|
crc = crc32.Update(crc, table, data[:n])
|
||||||
|
output.Write(data[:n])
|
||||||
|
}
|
||||||
if res != nil {
|
if res != nil {
|
||||||
if res == io.EOF {
|
if res == io.EOF {
|
||||||
break
|
break
|
||||||
@ -217,9 +204,6 @@ func saveAsset(src io.Reader, path string) (crc uint32, size int64, err error) {
|
|||||||
err = res
|
err = res
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
crc = crc32.Update(crc, table, data[:n])
|
|
||||||
output.Write(data[:n])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// read size
|
// read size
|
||||||
|
107
net/server/internal/api_addChannelTopicBlock.go
Normal file
107
net/server/internal/api_addChannelTopicBlock.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package databag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"databag/internal/store"
|
||||||
|
"errors"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
//AddChannelTopicBlock adds a file block asset to a topic
|
||||||
|
func AddChannelTopicBlock(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// scan parameters
|
||||||
|
params := mux.Vars(r)
|
||||||
|
topicID := params["topicID"]
|
||||||
|
|
||||||
|
channelSlot, guid, code, err := getChannelSlot(r, true)
|
||||||
|
if err != nil {
|
||||||
|
ErrResponse(w, code, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
act := &channelSlot.Account
|
||||||
|
|
||||||
|
// check storage
|
||||||
|
if full, err := isStorageFull(act); err != nil {
|
||||||
|
ErrResponse(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
} else if full {
|
||||||
|
ErrResponse(w, http.StatusNotAcceptable, errors.New("storage limit reached"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// load topic
|
||||||
|
var topicSlot store.TopicSlot
|
||||||
|
if err = store.DB.Preload("Topic").Where("channel_id = ? AND topic_slot_id = ?", channelSlot.Channel.ID, topicID).First(&topicSlot).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
ErrResponse(w, http.StatusNotFound, err)
|
||||||
|
} else {
|
||||||
|
ErrResponse(w, http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if topicSlot.Topic == nil {
|
||||||
|
ErrResponse(w, http.StatusNotFound, errors.New("referenced empty topic"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// can only update topic if creator
|
||||||
|
if topicSlot.Topic.GUID != guid {
|
||||||
|
ErrResponse(w, http.StatusUnauthorized, errors.New("topic not created by you"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid async cleanup of file before record is created
|
||||||
|
garbageSync.Lock()
|
||||||
|
defer garbageSync.Unlock()
|
||||||
|
|
||||||
|
// save new file
|
||||||
|
id := uuid.New().String()
|
||||||
|
path := getStrConfigValue(CNFAssetPath, APPDefaultPath) + "/" + channelSlot.Account.GUID + "/" + id
|
||||||
|
crc, size, err := saveAsset(r.Body, path)
|
||||||
|
if err != nil {
|
||||||
|
ErrResponse(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
asset := &store.Asset{}
|
||||||
|
asset.AssetID = id
|
||||||
|
asset.AccountID = channelSlot.Account.ID
|
||||||
|
asset.ChannelID = channelSlot.Channel.ID
|
||||||
|
asset.TopicID = topicSlot.Topic.ID
|
||||||
|
asset.Status = APPAssetReady
|
||||||
|
asset.Transform = APPTransformCopy
|
||||||
|
asset.TransformID = id
|
||||||
|
asset.Size = size
|
||||||
|
asset.Crc = crc
|
||||||
|
err = store.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if res := tx.Save(asset).Error; res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if res := tx.Model(&topicSlot.Topic).Update("detail_revision", act.ChannelRevision+1).Error; res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if res := tx.Model(&topicSlot).Update("revision", act.ChannelRevision+1).Error; res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if res := tx.Model(&channelSlot.Channel).Update("topic_revision", act.ChannelRevision+1).Error; res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if res := tx.Model(&channelSlot).Update("revision", act.ChannelRevision+1).Error; res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if res := tx.Model(act).Update("channel_revision", act.ChannelRevision+1).Error; res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ErrResponse(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteResponse(w, &Asset{AssetID: asset.AssetID, Transform: "_", Status: APPAssetReady})
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,8 @@
|
|||||||
package databag
|
package databag
|
||||||
|
|
||||||
|
//APPCopyTransform reserved tranform code indicating copy
|
||||||
|
const APPTransformCopy = "_"
|
||||||
|
|
||||||
//APPTokenSize config for size of random access token
|
//APPTokenSize config for size of random access token
|
||||||
const APPTokenSize = 16
|
const APPTokenSize = 16
|
||||||
|
|
||||||
|
@ -545,6 +545,13 @@ var endpoints = routes{
|
|||||||
AddChannel,
|
AddChannel,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
route{
|
||||||
|
"AddChannelTopicBlock",
|
||||||
|
strings.ToUpper("Post"),
|
||||||
|
"/content/channels/{channelID}/topics/{topicID}/blocks",
|
||||||
|
AddChannelTopicBlock,
|
||||||
|
},
|
||||||
|
|
||||||
route{
|
route{
|
||||||
"AddChannelTopicAsset",
|
"AddChannelTopicAsset",
|
||||||
strings.ToUpper("Post"),
|
strings.ToUpper("Post"),
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-easy-crop": "^4.1.4",
|
"react-easy-crop": "^4.1.4",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-player": "^2.10.0",
|
"react-image-file-resizer": "^0.4.8",
|
||||||
"react-resize-detector": "^7.0.0",
|
"react-resize-detector": "^7.0.0",
|
||||||
"react-router-dom": "^6.2.2",
|
"react-router-dom": "^6.2.2",
|
||||||
"react-scripts": "5.0.0",
|
"react-scripts": "5.0.0",
|
||||||
|
@ -7,6 +7,7 @@ const Colors = {
|
|||||||
formHover: '#efefef',
|
formHover: '#efefef',
|
||||||
grey: '#888888',
|
grey: '#888888',
|
||||||
white: '#ffffff',
|
white: '#ffffff',
|
||||||
|
black: '#000000',
|
||||||
divider: '#dddddd',
|
divider: '#dddddd',
|
||||||
mask: '#dddddd',
|
mask: '#dddddd',
|
||||||
encircle: '#cccccc',
|
encircle: '#cccccc',
|
||||||
|
@ -56,6 +56,27 @@ export function updateChannelSubject(subject, contentKey) {
|
|||||||
return { subjectEncrypted, subjectIv };
|
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) {
|
export function decryptChannelSubject(subject, contentKey) {
|
||||||
const { subjectEncrypted, subjectIv } = JSON.parse(subject);
|
const { subjectEncrypted, subjectIv } = JSON.parse(subject);
|
||||||
const iv = CryptoJS.enc.Hex.parse(subjectIv);
|
const iv = CryptoJS.enc.Hex.parse(subjectIv);
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import Resizer from "react-image-file-resizer";
|
||||||
|
|
||||||
|
const ENCRYPTED_BLOCK_SIZE = (1024 * 1024);
|
||||||
|
|
||||||
export function useUploadContext() {
|
export function useUploadContext() {
|
||||||
|
|
||||||
@ -69,7 +72,8 @@ export function useUploadContext() {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const entry = {
|
const entry = {
|
||||||
index: index.current,
|
index: index.current,
|
||||||
url: `${host}/content/channels/${channelId}/topics/${topicId}/assets?contact=${token}`,
|
baseUrl: `${host}/content/channels/${channelId}/topics/${topicId}/`,
|
||||||
|
urlParams: `?contact=${token}`,
|
||||||
files,
|
files,
|
||||||
assets: [],
|
assets: [],
|
||||||
current: null,
|
current: null,
|
||||||
@ -91,7 +95,8 @@ export function useUploadContext() {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const entry = {
|
const entry = {
|
||||||
index: index.current,
|
index: index.current,
|
||||||
url: `/content/channels/${channelId}/topics/${topicId}/assets?agent=${token}`,
|
baseUrl: `/content/channels/${channelId}/topics/${topicId}/`,
|
||||||
|
urlParams: `?agent=${token}`,
|
||||||
files,
|
files,
|
||||||
assets: [],
|
assets: [],
|
||||||
current: null,
|
current: null,
|
||||||
@ -145,6 +150,63 @@ export function useUploadContext() {
|
|||||||
return { state, actions }
|
return { state, actions }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getImageThumb(data) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
Resizer.imageFileResizer(data, 192, 192, 'JPEG', 50, 0,
|
||||||
|
uri => {
|
||||||
|
resolve(uri);
|
||||||
|
}, 'base64', 128, 128 );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideoThumb(data, pos) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = URL.createObjectURL(data);
|
||||||
|
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();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 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(data, type, position) {
|
||||||
|
|
||||||
|
if (type === 'image') {
|
||||||
|
return await getImageThumb(data);
|
||||||
|
}
|
||||||
|
else if (type === 'video') {
|
||||||
|
return await getVideoThumb(data, position);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function upload(entry, update, complete) {
|
async function upload(entry, update, complete) {
|
||||||
if (!entry.files?.length) {
|
if (!entry.files?.length) {
|
||||||
entry.success(entry.assets);
|
entry.success(entry.assets);
|
||||||
@ -154,11 +216,35 @@ async function upload(entry, update, complete) {
|
|||||||
const file = entry.files.shift();
|
const file = entry.files.shift();
|
||||||
entry.active = {};
|
entry.active = {};
|
||||||
try {
|
try {
|
||||||
if (file.image) {
|
if (file.encrypted) {
|
||||||
|
const { size, getEncryptedBlock, position, label, image, video, audio } = file;
|
||||||
|
const { data, type } = image ? { data: image, type: 'image' } : video ? { data: video, type: 'video' } : audio ? { data: audio, type: 'audio' } : {}
|
||||||
|
const thumb = await getThumb(data, type, position);
|
||||||
|
const parts = [];
|
||||||
|
for (let pos = 0; pos < size; pos += ENCRYPTED_BLOCK_SIZE) {
|
||||||
|
const len = pos + ENCRYPTED_BLOCK_SIZE > size ? size - pos : ENCRYPTED_BLOCK_SIZE;
|
||||||
|
const { blockEncrypted, blockIv } = await getEncryptedBlock(pos, len);
|
||||||
|
const part = await axios.post(`${entry.baseUrl}blocks${entry.urlParams}`, blockEncrypted, {
|
||||||
|
headers: {'Content-Type': 'text/plain'},
|
||||||
|
signal: entry.cancel.signal,
|
||||||
|
onUploadProgress: (ev) => {
|
||||||
|
const { loaded, total } = ev;
|
||||||
|
const partLoaded = pos + Math.floor(len * loaded / total);
|
||||||
|
entry.active = { loaded: partLoaded, total: size }
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parts.push({ blockIv, partId: part.data.assetId });
|
||||||
|
}
|
||||||
|
entry.assets.push({
|
||||||
|
encrypted: { type, thumb, label, parts }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (file.image) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('asset', file.image);
|
formData.append('asset', file.image);
|
||||||
let transform = encodeURIComponent(JSON.stringify(["ithumb;photo", "ilg;photo"]));
|
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,
|
signal: entry.cancel.signal,
|
||||||
onUploadProgress: (ev) => {
|
onUploadProgress: (ev) => {
|
||||||
const { loaded, total } = ev;
|
const { loaded, total } = ev;
|
||||||
@ -178,7 +264,7 @@ async function upload(entry, update, complete) {
|
|||||||
formData.append('asset', file.video);
|
formData.append('asset', file.video);
|
||||||
let thumb = 'vthumb;video;' + file.position;
|
let thumb = 'vthumb;video;' + file.position;
|
||||||
let transform = encodeURIComponent(JSON.stringify(["vlq;video", "vhd;video", thumb]));
|
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,
|
signal: entry.cancel.signal,
|
||||||
onUploadProgress: (ev) => {
|
onUploadProgress: (ev) => {
|
||||||
const { loaded, total } = ev;
|
const { loaded, total } = ev;
|
||||||
@ -198,7 +284,7 @@ async function upload(entry, update, complete) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('asset', file.audio);
|
formData.append('asset', file.audio);
|
||||||
let transform = encodeURIComponent(JSON.stringify(["acopy;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,
|
signal: entry.cancel.signal,
|
||||||
onUploadProgress: (ev) => {
|
onUploadProgress: (ev) => {
|
||||||
const { loaded, total } = ev;
|
const { loaded, total } = ev;
|
||||||
|
@ -14,7 +14,7 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
|
|||||||
const thread = useRef(null);
|
const thread = useRef(null);
|
||||||
|
|
||||||
const topicRenderer = (topic) => {
|
const topicRenderer = (topic) => {
|
||||||
return (<TopicItem host={cardId == null} topic={topic}
|
return (<TopicItem host={cardId == null} contentKey={state.contentKey} topic={topic}
|
||||||
remove={() => actions.removeTopic(topic.id)}
|
remove={() => actions.removeTopic(topic.id)}
|
||||||
update={(text) => actions.updateTopic(topic, text)}
|
update={(text) => actions.updateTopic(topic, text)}
|
||||||
sealed={state.sealed && !state.contentKey}
|
sealed={state.sealed && !state.contentKey}
|
||||||
|
@ -10,7 +10,7 @@ import { Carousel } from 'carousel/Carousel';
|
|||||||
|
|
||||||
export function AddTopic({ contentKey }) {
|
export function AddTopic({ contentKey }) {
|
||||||
|
|
||||||
const { state, actions } = useAddTopic();
|
const { state, actions } = useAddTopic(contentKey);
|
||||||
|
|
||||||
const [modal, modalContext] = Modal.useModal();
|
const [modal, modalContext] = Modal.useModal();
|
||||||
const attachImage = useRef(null);
|
const attachImage = useRef(null);
|
||||||
@ -28,7 +28,7 @@ export function AddTopic({ contentKey }) {
|
|||||||
const addTopic = async () => {
|
const addTopic = async () => {
|
||||||
if (state.messageText || state.assets.length) {
|
if (state.messageText || state.assets.length) {
|
||||||
try {
|
try {
|
||||||
await actions.addTopic(contentKey);
|
await actions.addTopic();
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@ -108,22 +108,22 @@ export function AddTopic({ contentKey }) {
|
|||||||
value={state.messageText} autocapitalize="none" />
|
value={state.messageText} autocapitalize="none" />
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
{ !contentKey && state.enableImage && (
|
{ state.enableImage && (
|
||||||
<div class="button space" onClick={() => attachImage.current.click()}>
|
<div class="button space" onClick={() => attachImage.current.click()}>
|
||||||
<PictureOutlined />
|
<PictureOutlined />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ !contentKey && state.enableVideo && (
|
{ state.enableVideo && (
|
||||||
<div class="button space" onClick={() => attachVideo.current.click()}>
|
<div class="button space" onClick={() => attachVideo.current.click()}>
|
||||||
<VideoCameraOutlined />
|
<VideoCameraOutlined />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ !contentKey && state.enableAudio && (
|
{ state.enableAudio && (
|
||||||
<div class="button space" onClick={() => attachAudio.current.click()}>
|
<div class="button space" onClick={() => attachAudio.current.click()}>
|
||||||
<SoundOutlined />
|
<SoundOutlined />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ !contentKey && (
|
{ (state.enableImage || state.enableVideo || state.enableAudio) && (
|
||||||
<div class="bar space" />
|
<div class="bar space" />
|
||||||
)}
|
)}
|
||||||
<div class="button space">
|
<div class="button space">
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { useContext, useState, useEffect } from 'react';
|
import { useContext, useState, useRef, useEffect } from 'react';
|
||||||
import { ConversationContext } from 'context/ConversationContext';
|
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() {
|
export function useAddTopic(contentKey) {
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
enableImage: null,
|
enableImage: null,
|
||||||
@ -18,6 +19,7 @@ export function useAddTopic() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const conversation = useContext(ConversationContext);
|
const conversation = useContext(ConversationContext);
|
||||||
|
const objects = useRef([]);
|
||||||
|
|
||||||
const updateState = (value) => {
|
const updateState = (value) => {
|
||||||
setState((s) => ({ ...s, ...value }));
|
setState((s) => ({ ...s, ...value }));
|
||||||
@ -45,23 +47,79 @@ export function useAddTopic() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearObjects = () => {
|
||||||
|
objects.current.forEach(object => {
|
||||||
|
URL.revokeObjectURL(object);
|
||||||
|
});
|
||||||
|
objects.current = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateState({ assets: [] });
|
||||||
|
return () => { clearObjects() };
|
||||||
|
}, [contentKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { enableImage, enableAudio, enableVideo } = conversation.state.channel?.data?.channelDetail || {};
|
const { enableImage, enableAudio, enableVideo } = conversation.state.channel?.data?.channelDetail || {};
|
||||||
updateState({ enableImage, enableAudio, enableVideo });
|
updateState({ enableImage, enableAudio, enableVideo });
|
||||||
}, [conversation.state.channel?.data?.channelDetail]);
|
}, [conversation.state.channel?.data?.channelDetail]);
|
||||||
|
|
||||||
|
const loadFileData = (file) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = (res) => { resolve(reader.result) }
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrayBufferToBase64 = (buffer) => {
|
||||||
|
var binary = '';
|
||||||
|
var bytes = new Uint8Array( buffer );
|
||||||
|
var len = bytes.byteLength;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode( bytes[ i ] );
|
||||||
|
}
|
||||||
|
return window.btoa( binary );
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUrl = async (file) => {
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
objects.current.push(url);
|
||||||
|
if (contentKey) {
|
||||||
|
const buffer = await loadFileData(file)
|
||||||
|
const getEncryptedBlock = (pos, len) => {
|
||||||
|
if (pos + len > buffer.byteLength) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const slice = buffer.slice(pos, pos + len);
|
||||||
|
const block = arrayBufferToBase64(slice);
|
||||||
|
return encryptBlock(block, contentKey);
|
||||||
|
}
|
||||||
|
return { url, encrypted: true, size: buffer.byteLength, getEncryptedBlock };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { url, encrypted: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
addImage: (image) => {
|
addImage: async (image) => {
|
||||||
let url = URL.createObjectURL(image);
|
const scaled = await getResizedImage(image);
|
||||||
addAsset({ image, url })
|
const asset = await setUrl(scaled);
|
||||||
|
asset.image = image;
|
||||||
|
addAsset(asset);
|
||||||
},
|
},
|
||||||
addVideo: (video) => {
|
addVideo: async (video) => {
|
||||||
let url = URL.createObjectURL(video);
|
const asset = await setUrl(video);
|
||||||
addAsset({ video, url, position: 0 })
|
asset.video = video;
|
||||||
|
asset.position = 0;
|
||||||
|
addAsset(asset);
|
||||||
},
|
},
|
||||||
addAudio: (audio) => {
|
addAudio: async (audio) => {
|
||||||
let url = URL.createObjectURL(audio);
|
const asset = await setUrl(audio);
|
||||||
addAsset({ audio, url, label: '' })
|
asset.audio = audio;
|
||||||
|
asset.label = '';
|
||||||
|
addAsset(asset);
|
||||||
},
|
},
|
||||||
setLabel: (index, label) => {
|
setLabel: (index, label) => {
|
||||||
updateAsset(index, { label });
|
updateAsset(index, { label });
|
||||||
@ -81,17 +139,15 @@ export function useAddTopic() {
|
|||||||
setTextSize: (value) => {
|
setTextSize: (value) => {
|
||||||
updateState({ textSizeSet: true, textSize: value });
|
updateState({ textSizeSet: true, textSize: value });
|
||||||
},
|
},
|
||||||
addTopic: async (contentKey) => {
|
addTopic: async () => {
|
||||||
if (!state.busy) {
|
if (!state.busy) {
|
||||||
try {
|
try {
|
||||||
updateState({ busy: true });
|
updateState({ busy: true });
|
||||||
const type = contentKey ? 'sealedtopic' : 'superbasictopic';
|
const type = contentKey ? 'sealedtopic' : 'superbasictopic';
|
||||||
const message = (assets) => {
|
const message = (assets) => {
|
||||||
if (contentKey) {
|
if (contentKey) {
|
||||||
if (assets?.length) {
|
|
||||||
console.log('assets not yet supported on sealed channels');
|
|
||||||
}
|
|
||||||
const message = {
|
const message = {
|
||||||
|
assets: assets?.length ? assets : null,
|
||||||
text: state.messageText,
|
text: state.messageText,
|
||||||
textColor: state.textColorSet ? state.textColor : null,
|
textColor: state.textColorSet ? state.textColor : null,
|
||||||
textSize: state.textSizeSet ? state.textSize : null,
|
textSize: state.textSizeSet ? state.textSize : null,
|
||||||
@ -99,26 +155,18 @@ export function useAddTopic() {
|
|||||||
return encryptTopicSubject({ message }, contentKey);
|
return encryptTopicSubject({ message }, contentKey);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (assets?.length) {
|
return {
|
||||||
return {
|
assets: assets?.length ? assets : null,
|
||||||
assets,
|
text: state.messageText,
|
||||||
text: state.messageText,
|
textColor: state.textColorSet ? state.textColor : null,
|
||||||
textColor: state.textColorSet ? state.textColor : null,
|
textSize: state.textSizeSet ? state.textSize : null,
|
||||||
textSize: state.textSizeSet ? state.textSize : null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return {
|
|
||||||
text: state.messageText,
|
|
||||||
textColor: state.textColorSet ? state.textColor : null,
|
|
||||||
textSize: state.textSizeSet ? state.textSize : null,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
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,
|
updateState({ busy: false, messageText: null, textColor: '#444444', textColorSet: false,
|
||||||
textSize: 12, textSizeSet: false, assets: [] });
|
textSize: 12, textSizeSet: false, assets: [] });
|
||||||
|
clearObjects();
|
||||||
}
|
}
|
||||||
catch(err) {
|
catch(err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@ -135,3 +183,18 @@ export function useAddTopic() {
|
|||||||
return { state, actions };
|
return { state, actions };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getResizedImage(data) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
Resizer.imageFileResizer(data, 1024, 1024, 'JPEG', 90, 0,
|
||||||
|
uri => {
|
||||||
|
const base64 = uri.split(';base64,').pop();
|
||||||
|
var binaryString = atob(base64);
|
||||||
|
var bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (var i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
resolve(new Blob([bytes]));
|
||||||
|
}, 'base64', 256, 256 );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import ReactPlayer from 'react-player'
|
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
|
import { RightOutlined, LeftOutlined } from '@ant-design/icons';
|
||||||
import { VideoFileWrapper } from './VideoFile.styled';
|
import { VideoFileWrapper } from './VideoFile.styled';
|
||||||
@ -17,22 +16,22 @@ export function VideoFile({ url, onPosition }) {
|
|||||||
|
|
||||||
const onSeek = (offset) => {
|
const onSeek = (offset) => {
|
||||||
if (player.current) {
|
if (player.current) {
|
||||||
let len = player.current.getDuration();
|
const len = player.current.duration;
|
||||||
if (len > 128) {
|
if (len > 16) {
|
||||||
offset *= Math.floor(len / 128);
|
offset *= Math.floor(len / 16);
|
||||||
}
|
}
|
||||||
seek.current += offset;
|
seek.current += offset;
|
||||||
if (seek.current < 0 || seek.current >= len) {
|
if (seek.current < 0 || seek.current >= len) {
|
||||||
seek.current = 0;
|
seek.current = 0;
|
||||||
}
|
}
|
||||||
onPosition(seek.current);
|
onPosition(seek.current);
|
||||||
player.current.seekTo(seek.current, 'seconds');
|
player.current.currentTime = seek.current;
|
||||||
setPlaying(true);
|
player.current.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPause = () => {
|
const onPause = () => {
|
||||||
setPlaying(false);
|
player.current.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -42,8 +41,7 @@ export function VideoFile({ url, onPosition }) {
|
|||||||
if (width !== state.width || height !== state.height) {
|
if (width !== state.width || height !== state.height) {
|
||||||
updateState({ width, height });
|
updateState({ width, height });
|
||||||
}
|
}
|
||||||
return <ReactPlayer ref={player} playing={playing} playbackRate={0} controls={false} height="100%" width="auto" url={url}
|
return <video ref={player} muted onPlay={onPause} src={url} width={'auto'} height={'100%'} playsinline="true" />
|
||||||
onStart={() => onPause()} onPlay={() => onPause()} />
|
|
||||||
}}
|
}}
|
||||||
</ReactResizeDetector>
|
</ReactResizeDetector>
|
||||||
<div class="overlay" style={{ width: state.width, height: state.height }}>
|
<div class="overlay" style={{ width: state.width, height: state.height }}>
|
||||||
|
@ -8,10 +8,10 @@ import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined,
|
|||||||
import { Carousel } from 'carousel/Carousel';
|
import { Carousel } from 'carousel/Carousel';
|
||||||
import { useTopicItem } from './useTopicItem.hook';
|
import { useTopicItem } from './useTopicItem.hook';
|
||||||
|
|
||||||
export function TopicItem({ host, sealed, topic, update, remove }) {
|
export function TopicItem({ host, contentKey, sealed, topic, update, remove }) {
|
||||||
|
|
||||||
const [ modal, modalContext ] = Modal.useModal();
|
const [ modal, modalContext ] = Modal.useModal();
|
||||||
const { state, actions } = useTopicItem();
|
const { state, actions } = useTopicItem(topic, contentKey);
|
||||||
|
|
||||||
const removeTopic = () => {
|
const removeTopic = () => {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
@ -52,16 +52,14 @@ export function TopicItem({ host, sealed, topic, update, remove }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderAsset = (asset, idx) => {
|
const renderAsset = (asset, idx) => {
|
||||||
if (asset.image) {
|
if (asset.type === 'image') {
|
||||||
return <ImageAsset thumbUrl={topic.assetUrl(asset.image.thumb, topic.id)}
|
return <ImageAsset asset={asset} />
|
||||||
fullUrl={topic.assetUrl(asset.image.full, topic.id)} />
|
|
||||||
}
|
}
|
||||||
if (asset.video) {
|
if (asset.type === 'video') {
|
||||||
return <VideoAsset thumbUrl={topic.assetUrl(asset.video.thumb, topic.id)}
|
return <VideoAsset asset={asset} />
|
||||||
lqUrl={topic.assetUrl(asset.video.lq, topic.id)} hdUrl={topic.assetUrl(asset.video.hd, topic.id)} />
|
|
||||||
}
|
}
|
||||||
if (asset.audio) {
|
if (asset.type === 'audio') {
|
||||||
return <AudioAsset label={asset.audio.label} audioUrl={topic.assetUrl(asset.audio.full, topic.id)} />
|
return <AudioAsset asset={asset} />
|
||||||
}
|
}
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
@ -113,7 +111,7 @@ export function TopicItem({ host, sealed, topic, update, remove }) {
|
|||||||
)}
|
)}
|
||||||
{ topic.transform === 'complete' && (
|
{ topic.transform === 'complete' && (
|
||||||
<div class="topic-assets">
|
<div class="topic-assets">
|
||||||
<Carousel pad={40} items={topic.assets} itemRenderer={renderAsset} />
|
<Carousel pad={40} items={state.assets} itemRenderer={renderAsset} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,37 +1,21 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { Spin } from 'antd';
|
import { Modal, Spin } from 'antd';
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
import { PlayCircleOutlined, MinusCircleOutlined, SoundOutlined } from '@ant-design/icons';
|
import { PlayCircleOutlined, MinusCircleOutlined, SoundOutlined } from '@ant-design/icons';
|
||||||
import { AudioAssetWrapper } from './AudioAsset.styled';
|
import { AudioAssetWrapper, AudioModalWrapper } from './AudioAsset.styled';
|
||||||
|
import { useAudioAsset } from './useAudioAsset.hook';
|
||||||
|
|
||||||
import background from 'images/audio.png';
|
import background from 'images/audio.png';
|
||||||
|
|
||||||
export function AudioAsset({ label, audioUrl }) {
|
export function AudioAsset({ asset }) {
|
||||||
|
|
||||||
const [active, setActive] = useState(false);
|
|
||||||
const [width, setWidth] = useState(0);
|
const [width, setWidth] = useState(0);
|
||||||
const [ready, setReady] = useState(false);
|
|
||||||
const [playing, setPlaying] = useState(true);
|
const [playing, setPlaying] = useState(true);
|
||||||
const [url, setUrl] = useState(null);
|
|
||||||
|
const { actions, state } = useAudioAsset(asset);
|
||||||
|
|
||||||
const audio = useRef(null);
|
const audio = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setActive(false);
|
|
||||||
setReady(false);
|
|
||||||
setPlaying(true);
|
|
||||||
setUrl(null);
|
|
||||||
}, [label, audioUrl]);
|
|
||||||
|
|
||||||
const onActivate = () => {
|
|
||||||
setUrl(audioUrl);
|
|
||||||
setActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onReady = () => {
|
|
||||||
setReady(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const play = (on) => {
|
const play = (on) => {
|
||||||
setPlaying(on);
|
setPlaying(on);
|
||||||
if (on) {
|
if (on) {
|
||||||
@ -54,32 +38,44 @@ export function AudioAsset({ label, audioUrl }) {
|
|||||||
</ReactResizeDetector>
|
</ReactResizeDetector>
|
||||||
<div class="player" style={{ width: width, height: width }}>
|
<div class="player" style={{ width: width, height: width }}>
|
||||||
<img class="background" src={background} alt="audio background" />
|
<img class="background" src={background} alt="audio background" />
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<div class="control" onClick={actions.setActive}>
|
||||||
{ !active && (
|
<SoundOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
|
||||||
<div class="control" onClick={() => onActivate()}>
|
|
||||||
<SoundOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{ active && !ready && (
|
|
||||||
<div class="control">
|
|
||||||
<Spin />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{ active && ready && playing && (
|
|
||||||
<div class="control" onClick={() => play(false)}>
|
|
||||||
<MinusCircleOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{ active && ready && !playing && (
|
|
||||||
<div class="control" onClick={() => play(true)}>
|
|
||||||
<PlayCircleOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<audio style={{ position: 'absolute', top: 0, visibility: 'hidden' }} autoplay="true"
|
|
||||||
src={url} type="audio/mpeg" ref={audio} onPlay={onReady} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="label">{ asset.label }</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="label">{ label }</div>
|
<Modal centered={true} visible={state.active} width={256 + 12} bodyStyle={{ width: '100%', height: 'auto', paddingBottom: 6, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd' }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearActive}>
|
||||||
|
<audio style={{ position: 'absolute', top: 0, visibility: 'hidden' }} autoplay="true"
|
||||||
|
src={state.url} type="audio/mpeg" ref={audio} onPlay={actions.ready} />
|
||||||
|
<AudioModalWrapper>
|
||||||
|
<img class="background" src={background} alt="audio background" />
|
||||||
|
{ state.loading && state.error && (
|
||||||
|
<div class="failed">
|
||||||
|
<Spin size="large" delay={250} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ state.loading && !state.error && (
|
||||||
|
<div class="loading">
|
||||||
|
<Spin size="large" delay={250} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ !state.ready && !state.loading && (
|
||||||
|
<div class="loading">
|
||||||
|
<Spin size="large" delay={250} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ state.ready && !state.loading && playing && (
|
||||||
|
<div class="control" onClick={() => play(false)}>
|
||||||
|
<MinusCircleOutlined style={{ fontSize: 64, color: '#eeeeee', cursor: 'pointer' }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ state.ready && !state.loading && !playing && (
|
||||||
|
<div class="control" onClick={() => play(true)}>
|
||||||
|
<PlayCircleOutlined style={{ fontSize: 64, color: '#eeeeee', cursor: 'pointer' }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="label">{ asset.label }</div>
|
||||||
|
</AudioModalWrapper>
|
||||||
|
</Modal>
|
||||||
</AudioAssetWrapper>
|
</AudioAssetWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import Colors from 'constants/Colors';
|
||||||
|
|
||||||
export const AudioAssetWrapper = styled.div`
|
export const AudioAssetWrapper = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -41,3 +42,48 @@ export const AudioAssetWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
export const AudioModalWrapper = styled.div`
|
||||||
|
width: 256px;
|
||||||
|
height: 256px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #aaaaaa;
|
||||||
|
|
||||||
|
.background {
|
||||||
|
width: 256px;
|
||||||
|
height: 256px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding-top: 8px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.ant-spin-dot-item {
|
||||||
|
background-color: ${Colors.alert};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.ant-spin-dot-item {
|
||||||
|
background-color: ${Colors.white};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import { useState, useRef } from 'react';
|
||||||
|
|
||||||
|
export function useAudioAsset(asset) {
|
||||||
|
|
||||||
|
const revoke = useRef();
|
||||||
|
const index = useRef(0);
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
active: false,
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
ready: false,
|
||||||
|
url: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateState = (value) => {
|
||||||
|
setState((s) => ({ ...s, ...value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
setActive: async () => {
|
||||||
|
if (asset.encrypted) {
|
||||||
|
try {
|
||||||
|
const view = index.current;
|
||||||
|
updateState({ active: true, ready: false, error: false, loading: true, url: null });
|
||||||
|
const blob = await asset.getDecryptedBlob(() => view != index.current);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
revoke.current = url;
|
||||||
|
updateState({ loading: false, url });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
updateState({ error: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updateState({ active: true, loading: false, url: asset.full });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearActive: () => {
|
||||||
|
index.current += 1;
|
||||||
|
updateState({ active: false, url: null });
|
||||||
|
if (revoke.current) {
|
||||||
|
URL.revokeObjectURL(revoke.current);
|
||||||
|
revoke.current = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ready: () => {
|
||||||
|
updateState({ ready: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { state, actions };
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Modal } from 'antd';
|
import { Modal, Spin } from 'antd';
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
import { ImageAssetWrapper } from './ImageAsset.styled';
|
import { ImageAssetWrapper, ImageModalWrapper } from './ImageAsset.styled';
|
||||||
import { useImageAsset } from './useImageAsset.hook';
|
import { useImageAsset } from './useImageAsset.hook';
|
||||||
|
|
||||||
export function ImageAsset({ thumbUrl, fullUrl }) {
|
export function ImageAsset({ asset }) {
|
||||||
|
|
||||||
const { state, actions } = useImageAsset();
|
const { state, actions } = useImageAsset(asset);
|
||||||
const [dimension, setDimension] = useState({ width: 0, height: 0 });
|
const [dimension, setDimension] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
const popout = () => {
|
const popout = () => {
|
||||||
@ -29,16 +29,31 @@ export function ImageAsset({ thumbUrl, fullUrl }) {
|
|||||||
if (width !== dimension.width || height !== dimension.height) {
|
if (width !== dimension.width || height !== dimension.height) {
|
||||||
setDimension({ width, height });
|
setDimension({ width, height });
|
||||||
}
|
}
|
||||||
return <img style={{ height: '100%', objectFit: 'contain' }} src={thumbUrl} alt="" />
|
return <img style={{ height: '100%', objectFit: 'contain' }} src={asset.thumb} alt="" />
|
||||||
}}
|
}}
|
||||||
</ReactResizeDetector>
|
</ReactResizeDetector>
|
||||||
<div class="viewer">
|
<div class="viewer">
|
||||||
<div class="overlay" style={{ width: dimension.width, height: dimension.height }}
|
<div class="overlay" style={{ width: dimension.width, height: dimension.height }}
|
||||||
onClick={popout} />
|
onClick={popout} />
|
||||||
<Modal centered={true} visible={state.popout} width={state.width + 12} bodyStyle={{ width: '100%', height: 'auto', paddingBottom: 6, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd' }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearPopout}>
|
<Modal centered={true} visible={state.popout} width={state.width + 12} bodyStyle={{ width: '100%', height: 'auto', paddingBottom: 6, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd' }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearPopout}>
|
||||||
<div onClick={actions.clearPopout}>
|
<ImageModalWrapper onClick={actions.clearPopout}>
|
||||||
<img style={{ width: '100%', objectFit: 'contain' }} src={fullUrl} alt="topic asset" />
|
<div class="frame">
|
||||||
</div>
|
<img class="thumb" src={asset.thumb} alt="topic asset" />
|
||||||
|
{ !state.error && (
|
||||||
|
<div class="loading">
|
||||||
|
<Spin size="large" delay={250} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ state.error && (
|
||||||
|
<div class="failed">
|
||||||
|
<Spin size="large" delay={250} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ !state.loading && (
|
||||||
|
<img class="full" src={state.url} alt="topic asset" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ImageModalWrapper>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</ImageAssetWrapper>
|
</ImageAssetWrapper>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import Colors from 'constants/Colors';
|
||||||
|
|
||||||
export const ImageAssetWrapper = styled.div`
|
export const ImageAssetWrapper = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -40,3 +41,44 @@ export const ImageAssetWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const ImageModalWrapper = styled.div`
|
||||||
|
.frame {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: ${Colors.black};
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
opacity: 0.5;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.ant-spin-dot-item {
|
||||||
|
background-color: ${Colors.alert};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.ant-spin-dot-item {
|
||||||
|
background-color: ${Colors.white};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
|
|
||||||
export function useImageAsset() {
|
export function useImageAsset(asset) {
|
||||||
|
|
||||||
|
const revoke = useRef();
|
||||||
|
const index = useRef(0);
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
popout: false,
|
popout: false,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
url: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateState = (value) => {
|
const updateState = (value) => {
|
||||||
@ -13,11 +19,32 @@ export function useImageAsset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
setPopout: (width, height) => {
|
setPopout: async (width, height) => {
|
||||||
updateState({ popout: true, width, height });
|
if (asset.encrypted) {
|
||||||
|
try {
|
||||||
|
const view = index.current;
|
||||||
|
updateState({ popout: true, width, height, error: false, loading: true, url: null });
|
||||||
|
const blob = await asset.getDecryptedBlob(() => view != index.current);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
updateState({ loading: false, url });
|
||||||
|
revoke.current = url;
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
updateState({ error: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updateState({ popout: true, width, height, loading: false, url: asset.full });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clearPopout: () => {
|
clearPopout: () => {
|
||||||
|
index.current += 1;
|
||||||
updateState({ popout: false });
|
updateState({ popout: false });
|
||||||
|
if (revoke.current) {
|
||||||
|
URL.revokeObjectURL(revoke.current);
|
||||||
|
revoke.current = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,16 +1,91 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { checkResponse, fetchWithTimeout } from 'api/fetchUtil';
|
||||||
|
import { decryptBlock } from 'context/sealUtil';
|
||||||
|
|
||||||
export function useTopicItem() {
|
export function useTopicItem(topic, contentKey) {
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
editing: false,
|
editing: false,
|
||||||
message: null,
|
message: null,
|
||||||
|
assets: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateState = (value) => {
|
const updateState = (value) => {
|
||||||
setState((s) => ({ ...s, ...value }));
|
setState((s) => ({ ...s, ...value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const base64ToUint8Array = (base64) => {
|
||||||
|
var binaryString = atob(base64);
|
||||||
|
var bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (var i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const assets = [];
|
||||||
|
if (topic.assets?.length) {
|
||||||
|
topic.assets.forEach(asset => {
|
||||||
|
if (asset.encrypted) {
|
||||||
|
const encrypted = true;
|
||||||
|
const { type, thumb, label, parts } = asset.encrypted;
|
||||||
|
const getDecryptedBlob = async (abort) => {
|
||||||
|
let pos = 0;
|
||||||
|
let len = 0;
|
||||||
|
|
||||||
|
const slices = []
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (abort()) {
|
||||||
|
throw new Error("asset unseal aborted");
|
||||||
|
}
|
||||||
|
const part = parts[i];
|
||||||
|
const url = topic.assetUrl(part.partId, topic.id);
|
||||||
|
const response = await fetchWithTimeout(url, { method: 'GET' });
|
||||||
|
const block = await response.text();
|
||||||
|
const decrypted = decryptBlock(block, part.blockIv, contentKey);
|
||||||
|
const slice = base64ToUint8Array(decrypted);
|
||||||
|
slices.push(slice);
|
||||||
|
len += slice.byteLength;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = new Uint8Array(len)
|
||||||
|
for (let i = 0; i < slices.length; i++) {
|
||||||
|
const slice = slices[i];
|
||||||
|
data.set(slice, pos);
|
||||||
|
pos += slice.byteLength
|
||||||
|
}
|
||||||
|
return new Blob([data]);
|
||||||
|
}
|
||||||
|
assets.push({ type, thumb, label, encrypted, getDecryptedBlob });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const encrypted = false
|
||||||
|
if (asset.image) {
|
||||||
|
const type = 'image';
|
||||||
|
const thumb = topic.assetUrl(asset.image.thumb, topic.id);
|
||||||
|
const full = topic.assetUrl(asset.image.full, topic.id);
|
||||||
|
assets.push({ type, thumb, encrypted, full });
|
||||||
|
}
|
||||||
|
else if (asset.video) {
|
||||||
|
const type = 'video';
|
||||||
|
const thumb = topic.assetUrl(asset.video.thumb, topic.id);
|
||||||
|
const lq = topic.assetUrl(asset.video.lq, topic.id);
|
||||||
|
const hd = topic.assetUrl(asset.video.hd, topic.id);
|
||||||
|
assets.push({ type, thumb, encrypted, lq, hd });
|
||||||
|
}
|
||||||
|
else if (asset.audio) {
|
||||||
|
const type = 'audio';
|
||||||
|
const label = asset.audio.label;
|
||||||
|
const full = topic.assetUrl(asset.audio.full, topic.id);
|
||||||
|
assets.push({ type, label, encrypted, full });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateState({ assets });
|
||||||
|
}
|
||||||
|
}, [topic.assets]);
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
setEditing: (message) => {
|
setEditing: (message) => {
|
||||||
updateState({ editing: true, message });
|
updateState({ editing: true, message });
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Modal } from 'antd';
|
import { Modal, Spin } from 'antd';
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
import { VideoCameraOutlined } from '@ant-design/icons';
|
import { VideoCameraOutlined } from '@ant-design/icons';
|
||||||
import { VideoAssetWrapper } from './VideoAsset.styled';
|
import { VideoAssetWrapper, VideoModalWrapper } from './VideoAsset.styled';
|
||||||
import { useVideoAsset } from './useVideoAsset.hook';
|
import { useVideoAsset } from './useVideoAsset.hook';
|
||||||
|
|
||||||
export function VideoAsset({ thumbUrl, lqUrl, hdUrl }) {
|
export function VideoAsset({ asset }) {
|
||||||
|
|
||||||
const { state, actions } = useVideoAsset();
|
const { state, actions } = useVideoAsset(asset);
|
||||||
|
|
||||||
const activate = () => {
|
const activate = () => {
|
||||||
if (state.dimension.width / state.dimension.height > window.innerWidth / window.innerHeight) {
|
if (state.dimension.width / state.dimension.height > window.innerWidth / window.innerHeight) {
|
||||||
@ -28,7 +28,7 @@ export function VideoAsset({ thumbUrl, lqUrl, hdUrl }) {
|
|||||||
if (width !== state.dimension.width || height !== state.dimension.height) {
|
if (width !== state.dimension.width || height !== state.dimension.height) {
|
||||||
actions.setDimension({ width, height });
|
actions.setDimension({ width, height });
|
||||||
}
|
}
|
||||||
return <img style={{ height: '100%', objectFit: 'contain' }} src={thumbUrl} alt="" />
|
return <img style={{ height: '100%', objectFit: 'contain' }} src={asset.thumb} alt="" />
|
||||||
}}
|
}}
|
||||||
</ReactResizeDetector>
|
</ReactResizeDetector>
|
||||||
<div class="overlay" style={{ width: state.dimension.width, height: state.dimension.height }}>
|
<div class="overlay" style={{ width: state.dimension.width, height: state.dimension.height }}>
|
||||||
@ -38,8 +38,29 @@ export function VideoAsset({ thumbUrl, lqUrl, hdUrl }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Modal centered={true} style={{ backgroundColor: '#aacc00', padding: 0 }} visible={state.active} width={state.width + 12} bodyStyle={{ paddingBottom: 0, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd', margin: 0 }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearActive}>
|
<Modal centered={true} style={{ backgroundColor: '#aacc00', padding: 0 }} visible={state.active} width={state.width + 12} bodyStyle={{ paddingBottom: 0, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd', margin: 0 }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearActive}>
|
||||||
<video autoplay="true" controls src={hdUrl} width={state.width} height={state.height}
|
<VideoModalWrapper>
|
||||||
playsinline="true" />
|
<div class="wrapper">
|
||||||
|
{ !state.loaded && (
|
||||||
|
<div class="frame">
|
||||||
|
<img class="thumb" src={asset.thumb} alt="topic asset" />
|
||||||
|
{ state.error && (
|
||||||
|
<div class="failed">
|
||||||
|
<Spin size="large" delay={250} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ !state.error && (
|
||||||
|
<div class="loading">
|
||||||
|
<Spin size="large" delay={250} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ !state.loading && (
|
||||||
|
<video style={{display: state.loaded ? 'block' : 'none'}} autoplay="true" controls src={state.url} width={state.width} height={state.height}
|
||||||
|
playsinline="true" onLoadedData={actions.setLoaded} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</VideoModalWrapper>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</VideoAssetWrapper>
|
</VideoAssetWrapper>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import Colors from 'constants/Colors';
|
||||||
|
|
||||||
export const VideoAssetWrapper = styled.div`
|
export const VideoAssetWrapper = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -14,3 +15,42 @@ export const VideoAssetWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const VideoModalWrapper = styled.div`
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
opacity: 0.3;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.ant-spin-dot-item {
|
||||||
|
background-color: ${Colors.alert};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.ant-spin-dot-item {
|
||||||
|
background-color: ${Colors.white};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
|
|
||||||
export function useVideoAsset() {
|
export function useVideoAsset(asset) {
|
||||||
|
|
||||||
|
const revoke = useRef();
|
||||||
|
const index = useRef(0);
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
active: false,
|
active: false,
|
||||||
dimension: { width: 0, height: 0 },
|
dimension: { width: 0, height: 0 },
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
url: null,
|
||||||
|
loaded: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateState = (value) => {
|
const updateState = (value) => {
|
||||||
@ -14,15 +21,39 @@ export function useVideoAsset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
setActive: (width, height, url) => {
|
setActive: async (width, height) => {
|
||||||
updateState({ active: true, width, height });
|
if (asset.encrypted) {
|
||||||
|
try {
|
||||||
|
const view = index.current;
|
||||||
|
updateState({ active: true, width, height, error: false, loaded: false, loading: true, url: null });
|
||||||
|
const blob = await asset.getDecryptedBlob(() => view != index.current);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
revoke.current = url;
|
||||||
|
updateState({ url, loading: false });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
updateState({ error: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updateState({ active: true, width, height, loading: false, url: asset.hd });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clearActive: () => {
|
clearActive: () => {
|
||||||
|
index.current += 1;
|
||||||
updateState({ active: false });
|
updateState({ active: false });
|
||||||
|
if (revoke.current) {
|
||||||
|
URL.revokeObjectURL(revoke.current);
|
||||||
|
revoke.current = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setDimension: (dimension) => {
|
setDimension: (dimension) => {
|
||||||
updateState({ dimension });
|
updateState({ dimension });
|
||||||
},
|
},
|
||||||
|
setLoaded: () => {
|
||||||
|
updateState({ loaded: true });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { state, actions };
|
return { state, actions };
|
||||||
|
@ -145,7 +145,7 @@ export function useConversation(cardId, channelId) {
|
|||||||
|
|
||||||
let group = '';
|
let group = '';
|
||||||
let clickable = [];
|
let clickable = [];
|
||||||
const words = text == null ? '' : DOMPurify.sanitize(text).split(' ');
|
const words = text == [] ? '' : DOMPurify.sanitize(text).split(' ');
|
||||||
words.forEach((word, index) => {
|
words.forEach((word, index) => {
|
||||||
if (!!urlPattern.test(word)) {
|
if (!!urlPattern.test(word)) {
|
||||||
clickable.push(<span key={index}>{ group }</span>);
|
clickable.push(<span key={index}>{ group }</span>);
|
||||||
|
@ -167,7 +167,6 @@ export function useSession() {
|
|||||||
await ring.actions.decline(cardId, contactNode, contactToken, callId);
|
await ring.actions.decline(cardId, contactNode, contactToken, callId);
|
||||||
},
|
},
|
||||||
accept: async (call) => {
|
accept: async (call) => {
|
||||||
console.log("ACCEPTING:", call);
|
|
||||||
const { cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword } = call;
|
const { cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword } = call;
|
||||||
await ring.actions.accept(cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword);
|
await ring.actions.accept(cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword);
|
||||||
},
|
},
|
||||||
|
@ -4171,7 +4171,7 @@ deep-is@^0.1.3, deep-is@~0.1.3:
|
|||||||
resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz"
|
resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz"
|
||||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||||
|
|
||||||
deepmerge@^4.0.0, deepmerge@^4.2.2:
|
deepmerge@^4.2.2:
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz"
|
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz"
|
||||||
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
|
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
|
||||||
@ -7162,11 +7162,6 @@ lines-and-columns@^1.1.6:
|
|||||||
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
|
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
|
||||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||||
|
|
||||||
load-script@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz"
|
|
||||||
integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==
|
|
||||||
|
|
||||||
loader-runner@^4.2.0:
|
loader-runner@^4.2.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz"
|
resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz"
|
||||||
@ -7421,11 +7416,6 @@ memfs@^3.1.2, memfs@^3.4.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fs-monkey "^1.0.3"
|
fs-monkey "^1.0.3"
|
||||||
|
|
||||||
memoize-one@^5.1.1:
|
|
||||||
version "5.2.1"
|
|
||||||
resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz"
|
|
||||||
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
|
||||||
|
|
||||||
merge-descriptors@1.0.1:
|
merge-descriptors@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
|
||||||
@ -8600,7 +8590,7 @@ prompts@^2.0.1, prompts@^2.4.2:
|
|||||||
kleur "^3.0.3"
|
kleur "^3.0.3"
|
||||||
sisteransi "^1.0.5"
|
sisteransi "^1.0.5"
|
||||||
|
|
||||||
prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1:
|
prop-types@^15.5.10, prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@ -9129,16 +9119,16 @@ react-error-overlay@^6.0.11:
|
|||||||
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz"
|
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz"
|
||||||
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
|
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
|
||||||
|
|
||||||
react-fast-compare@^3.0.1:
|
|
||||||
version "3.2.0"
|
|
||||||
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz"
|
|
||||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
|
||||||
|
|
||||||
react-icons@^4.8.0:
|
react-icons@^4.8.0:
|
||||||
version "4.8.0"
|
version "4.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445"
|
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445"
|
||||||
integrity sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==
|
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:
|
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"
|
version "16.13.1"
|
||||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||||
@ -9154,17 +9144,6 @@ react-is@^18.0.0, react-is@^18.2.0:
|
|||||||
resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz"
|
resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz"
|
||||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||||
|
|
||||||
react-player@^2.10.0:
|
|
||||||
version "2.11.0"
|
|
||||||
resolved "https://registry.npmjs.org/react-player/-/react-player-2.11.0.tgz"
|
|
||||||
integrity sha512-fIrwpuXOBXdEg1FiyV9isKevZOaaIsAAtZy5fcjkQK9Nhmk1I2NXzY/hkPos8V0zb/ZX416LFy8gv7l/1k3a5w==
|
|
||||||
dependencies:
|
|
||||||
deepmerge "^4.0.0"
|
|
||||||
load-script "^1.0.0"
|
|
||||||
memoize-one "^5.1.1"
|
|
||||||
prop-types "^15.7.2"
|
|
||||||
react-fast-compare "^3.0.1"
|
|
||||||
|
|
||||||
react-refresh@^0.11.0:
|
react-refresh@^0.11.0:
|
||||||
version "0.11.0"
|
version "0.11.0"
|
||||||
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz"
|
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user