rendering unsealed assets

This commit is contained in:
Roland Osborne 2023-05-02 16:09:35 -07:00
parent 452679324a
commit 5f20b22250
10 changed files with 193 additions and 38 deletions

View File

@ -14,7 +14,7 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
const thread = useRef(null);
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)}
update={(text) => actions.updateTopic(topic, text)}
sealed={state.sealed && !state.contentKey}

View File

@ -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,

View File

@ -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 <ImageAsset thumbUrl={topic.assetUrl(asset.image.thumb, topic.id)}
fullUrl={topic.assetUrl(asset.image.full, topic.id)} />
if (asset.type === 'image') {
return <ImageAsset asset={asset} />
}
if (asset.video) {
return <VideoAsset thumbUrl={topic.assetUrl(asset.video.thumb, topic.id)}
lqUrl={topic.assetUrl(asset.video.lq, topic.id)} hdUrl={topic.assetUrl(asset.video.hd, topic.id)} />
if (asset.type === 'video') {
return <VideoAsset asset={asset} />
}
if (asset.audio) {
return <AudioAsset label={asset.audio.label} audioUrl={topic.assetUrl(asset.audio.full, topic.id)} />
if (asset.type === 'audio') {
return <AudioAsset asset={asset} />
}
return <></>
}
@ -113,7 +111,7 @@ export function TopicItem({ host, sealed, topic, update, remove }) {
)}
{ topic.transform === 'complete' && (
<div class="topic-assets">
<Carousel pad={40} items={topic.assets} itemRenderer={renderAsset} />
<Carousel pad={40} items={state.assets} itemRenderer={renderAsset} />
</div>
)}
</>

View File

@ -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 <img style={{ height: '100%', objectFit: 'contain' }} src={thumbUrl} alt="" />
return <img style={{ height: '100%', objectFit: 'contain' }} src={asset.thumb} alt="" />
}}
</ReactResizeDetector>
<div class="viewer">
<div class="overlay" style={{ width: dimension.width, height: dimension.height }}
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}>
<div onClick={actions.clearPopout}>
<img style={{ width: '100%', objectFit: 'contain' }} src={fullUrl} alt="topic asset" />
</div>
<ImageModalWrapper onClick={actions.clearPopout}>
{ state.loading && (
<div class="frame">
<img style={{ width: '100%', objectFit: 'contain' }} src={asset.thumb} alt="topic asset" />
<div class="spinner">
<Spin color={'white'} size="large" delay={250} />
</div>
</div>
)}
{ !state.loading && (
<img style={{ width: '100%', objectFit: 'contain' }} src={state.url} alt="topic asset" />
)}
</ImageModalWrapper>
</Modal>
</div>
</ImageAssetWrapper>

View File

@ -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;
}
`;

View File

@ -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 });

View File

@ -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 });

View File

@ -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 <img style={{ height: '100%', objectFit: 'contain' }} src={thumbUrl} alt="" />
return <img style={{ height: '100%', objectFit: 'contain' }} src={asset.thumb} alt="" />
}}
</ReactResizeDetector>
<div class="overlay" style={{ width: state.dimension.width, height: state.dimension.height }}>
@ -38,8 +38,20 @@ export function VideoAsset({ thumbUrl, lqUrl, hdUrl }) {
</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}>
<video autoplay="true" controls src={hdUrl} width={state.width} height={state.height}
playsinline="true" />
<VideoModalWrapper>
{ state.loading && (
<div class="frame">
<img style={{ width: '100%', objectFit: 'contain' }} src={asset.thumb} alt="topic asset" />
<div class="spinner">
<Spin color={'white'} size="large" delay={250} />
</div>
</div>
)}
{ !state.loading && (
<video autoplay="true" controls src={state.url} width={state.width} height={state.height}
playsinline="true" />
)}
</VideoModalWrapper>
</Modal>
</div>
</VideoAssetWrapper>

View File

@ -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;
}
`;

View File

@ -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 });