From 5f20b22250f50e1e8039bc0401592189fc4bb23a Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Tue, 2 May 2023 16:09:35 -0700 Subject: [PATCH] rendering unsealed assets --- .../src/session/conversation/Conversation.jsx | 2 +- .../conversation/addTopic/useAddTopic.hook.js | 8 +- .../conversation/topicItem/TopicItem.jsx | 20 +++-- .../topicItem/imageAsset/ImageAsset.jsx | 26 ++++-- .../topicItem/imageAsset/ImageAsset.styled.js | 18 +++++ .../imageAsset/useImageAsset.hook.js | 17 +++- .../topicItem/useTopicItem.hook.js | 79 ++++++++++++++++++- .../topicItem/videoAsset/VideoAsset.jsx | 26 ++++-- .../topicItem/videoAsset/VideoAsset.styled.js | 18 +++++ .../videoAsset/useVideoAsset.hook.js | 17 +++- 10 files changed, 193 insertions(+), 38 deletions(-) diff --git a/net/web/src/session/conversation/Conversation.jsx b/net/web/src/session/conversation/Conversation.jsx index e14074b1..598b9fa3 100644 --- a/net/web/src/session/conversation/Conversation.jsx +++ b/net/web/src/session/conversation/Conversation.jsx @@ -14,7 +14,7 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId const thread = useRef(null); const topicRenderer = (topic) => { - return ( actions.removeTopic(topic.id)} update={(text) => actions.updateTopic(topic, text)} sealed={state.sealed && !state.contentKey} diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 3db1cf11..b3de6636 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -144,16 +144,18 @@ export function useAddTopic(contentKey) { updateState({ busy: true }); const type = contentKey ? 'sealedtopic' : 'superbasictopic'; const message = (assets) => { - if (assets?.length) { - return { - assets, + if (contentKey) { + const message = { + assets: assets?.length ? assets : null, text: state.messageText, textColor: state.textColorSet ? state.textColor : null, textSize: state.textSizeSet ? state.textSize : null, } + return encryptTopicSubject({ message }, contentKey); } else { return { + assets: assets?.length ? assets : null, text: state.messageText, textColor: state.textColorSet ? state.textColor : null, textSize: state.textSizeSet ? state.textSize : null, diff --git a/net/web/src/session/conversation/topicItem/TopicItem.jsx b/net/web/src/session/conversation/topicItem/TopicItem.jsx index 0136eb9b..fd10666d 100644 --- a/net/web/src/session/conversation/topicItem/TopicItem.jsx +++ b/net/web/src/session/conversation/topicItem/TopicItem.jsx @@ -8,10 +8,10 @@ import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined, import { Carousel } from 'carousel/Carousel'; 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 { state, actions } = useTopicItem(); + const { state, actions } = useTopicItem(topic, contentKey); const removeTopic = () => { modal.confirm({ @@ -52,16 +52,14 @@ export function TopicItem({ host, sealed, topic, update, remove }) { }; const renderAsset = (asset, idx) => { - if (asset.image) { - return + if (asset.type === 'image') { + return } - if (asset.video) { - return + if (asset.type === 'video') { + return } - if (asset.audio) { - return + if (asset.type === 'audio') { + return } return <> } @@ -113,7 +111,7 @@ export function TopicItem({ host, sealed, topic, update, remove }) { )} { topic.transform === 'complete' && (
- +
)} diff --git a/net/web/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx b/net/web/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx index 4ebf15af..6510bdd8 100644 --- a/net/web/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx +++ b/net/web/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; -import { Modal } from 'antd'; +import { Modal, Spin } from 'antd'; import ReactResizeDetector from 'react-resize-detector'; -import { ImageAssetWrapper } from './ImageAsset.styled'; +import { ImageAssetWrapper, ImageModalWrapper } from './ImageAsset.styled'; 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 popout = () => { @@ -29,16 +29,26 @@ export function ImageAsset({ thumbUrl, fullUrl }) { if (width !== dimension.width || height !== dimension.height) { setDimension({ width, height }); } - return + return }}
-
- topic asset -
+ + { state.loading && ( +
+ topic asset +
+ +
+
+ )} + { !state.loading && ( + topic asset + )} +
diff --git a/net/web/src/session/conversation/topicItem/imageAsset/ImageAsset.styled.js b/net/web/src/session/conversation/topicItem/imageAsset/ImageAsset.styled.js index bbaeb449..e9098399 100644 --- a/net/web/src/session/conversation/topicItem/imageAsset/ImageAsset.styled.js +++ b/net/web/src/session/conversation/topicItem/imageAsset/ImageAsset.styled.js @@ -40,3 +40,21 @@ export const ImageAssetWrapper = styled.div` } `; +export const ImageModalWrapper = styled.div` + .frame { + display: flex; + align-items: center; + justify-content: center; + opacity: 0.5; + } + + .ant-spin-dot-item { + background-color: white; + } + + .spinner { + position: absolute; + color: white; + border-radius: 8px; + } +`; diff --git a/net/web/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js b/net/web/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js index 45030865..0d56bb4f 100644 --- a/net/web/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js +++ b/net/web/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js @@ -1,11 +1,14 @@ import { useState } from 'react'; -export function useImageAsset() { +export function useImageAsset(asset) { const [state, setState] = useState({ popout: false, width: 0, height: 0, + loading: false, + error: false, + url: null, }); const updateState = (value) => { @@ -13,8 +16,16 @@ export function useImageAsset() { } const actions = { - setPopout: (width, height) => { - updateState({ popout: true, width, height }); + setPopout: async (width, height) => { + if (asset.encrypted) { + updateState({ popout: true, width, height, loading: true, url: null }); + const blob = await asset.getDecryptedBlob(); + const url = URL.createObjectURL(blob); + updateState({ loading: false, url }); + } + else { + updateState({ popout: true, width, height, loading: false, url: asset.full }); + } }, clearPopout: () => { updateState({ popout: false }); diff --git a/net/web/src/session/conversation/topicItem/useTopicItem.hook.js b/net/web/src/session/conversation/topicItem/useTopicItem.hook.js index e56d20cd..a5cf71d4 100644 --- a/net/web/src/session/conversation/topicItem/useTopicItem.hook.js +++ b/net/web/src/session/conversation/topicItem/useTopicItem.hook.js @@ -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({ editing: false, message: null, + assets: [], }); + console.log(topic); + + const updateState = (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, parts } = asset.encrypted; + const getDecryptedBlob = async () => { + let pos = 0; + let len = 0; + + const slices = [] + for (let i = 0; i < parts.length; i++) { + 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, 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 = { setEditing: (message) => { updateState({ editing: true, message }); diff --git a/net/web/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx b/net/web/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx index 43e04793..cb230252 100644 --- a/net/web/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx +++ b/net/web/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx @@ -1,12 +1,12 @@ -import { Modal } from 'antd'; +import { Modal, Spin } from 'antd'; import ReactResizeDetector from 'react-resize-detector'; import { VideoCameraOutlined } from '@ant-design/icons'; -import { VideoAssetWrapper } from './VideoAsset.styled'; +import { VideoAssetWrapper, VideoModalWrapper } from './VideoAsset.styled'; 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 = () => { 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) { actions.setDimension({ width, height }); } - return + return }}
@@ -38,8 +38,20 @@ export function VideoAsset({ thumbUrl, lqUrl, hdUrl }) {
)} -
diff --git a/net/web/src/session/conversation/topicItem/videoAsset/VideoAsset.styled.js b/net/web/src/session/conversation/topicItem/videoAsset/VideoAsset.styled.js index fc6c3e34..a21d8f75 100644 --- a/net/web/src/session/conversation/topicItem/videoAsset/VideoAsset.styled.js +++ b/net/web/src/session/conversation/topicItem/videoAsset/VideoAsset.styled.js @@ -14,3 +14,21 @@ export const VideoAssetWrapper = styled.div` } `; +export const VideoModalWrapper = styled.div` + .frame { + display: flex; + align-items: center; + justify-content: center; + opacity: 0.5; + } + + .ant-spin-dot-item { + background-color: white; + } + + .spinner { + position: absolute; + color: white; + border-radius: 8px; + } +`; diff --git a/net/web/src/session/conversation/topicItem/videoAsset/useVideoAsset.hook.js b/net/web/src/session/conversation/topicItem/videoAsset/useVideoAsset.hook.js index 54eded4e..f26bcf05 100644 --- a/net/web/src/session/conversation/topicItem/videoAsset/useVideoAsset.hook.js +++ b/net/web/src/session/conversation/topicItem/videoAsset/useVideoAsset.hook.js @@ -1,12 +1,15 @@ import { useState } from 'react'; -export function useVideoAsset() { +export function useVideoAsset(asset) { const [state, setState] = useState({ width: 0, height: 0, active: false, dimension: { width: 0, height: 0 }, + loading: false, + error: false, + url: null, }); const updateState = (value) => { @@ -14,8 +17,16 @@ export function useVideoAsset() { } const actions = { - setActive: (width, height, url) => { - updateState({ active: true, width, height }); + setActive: async (width, height) => { + if (asset.encrypted) { + updateState({ active: true, width, height, loading: true, url: null }); + const blob = await asset.getDecryptedBlob(); + const url = URL.createObjectURL(blob); + updateState({ loading: false, url }); + } + else { + updateState({ popout: true, width, height, loading: false, url: asset.hd }); + } }, clearActive: () => { updateState({ active: false });