From 809d603cdf0caee16f5d679b461808105791003d Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Wed, 3 May 2023 15:42:22 -0700 Subject: [PATCH] playback of e2e assets in mobile app --- .../ios/Databag.xcodeproj/project.pbxproj | 4 +- .../context/useConversationContext.hook.js | 2 +- .../src/context/useUploadContext.hook.js | 12 +-- .../conversation/addTopic/useAddTopic.hook.js | 20 ++-- .../conversation/topicItem/TopicItem.jsx | 42 +++------ .../topicItem/audioAsset/AudioAsset.jsx | 6 +- .../audioAsset/useAudioAsset.hook.js | 12 ++- .../topicItem/audioThumb/AudioThumb.jsx | 6 +- .../topicItem/imageAsset/ImageAsset.jsx | 5 +- .../imageAsset/useImageAsset.hook.js | 12 ++- .../topicItem/imageThumb/ImageThumb.jsx | 6 +- .../imageThumb/useImageThumb.hook.js | 7 +- .../topicItem/useTopicItem.hook.js | 91 ++++++++++++++++++- .../topicItem/videoAsset/VideoAsset.jsx | 8 +- .../videoAsset/useVideoAsset.hook.js | 12 ++- .../topicItem/videoThumb/VideoThumb.jsx | 7 +- .../videoThumb/useVideoThumb.hook.js | 16 +--- 17 files changed, 163 insertions(+), 105 deletions(-) diff --git a/app/mobile/ios/Databag.xcodeproj/project.pbxproj b/app/mobile/ios/Databag.xcodeproj/project.pbxproj index 84b94ec7..9b8a0ef7 100644 --- a/app/mobile/ios/Databag.xcodeproj/project.pbxproj +++ b/app/mobile/ios/Databag.xcodeproj/project.pbxproj @@ -756,7 +756,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -828,7 +828,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; diff --git a/app/mobile/src/context/useConversationContext.hook.js b/app/mobile/src/context/useConversationContext.hook.js index ce77b23a..08b277f7 100644 --- a/app/mobile/src/context/useConversationContext.hook.js +++ b/app/mobile/src/context/useConversationContext.hook.js @@ -146,7 +146,7 @@ export function useConversationContext() { } else { topics.current.delete(entry.id); - clearTopicItem(entry.id); + clearTopicItem(cardId, channelId, entry.id); } } } diff --git a/app/mobile/src/context/useUploadContext.hook.js b/app/mobile/src/context/useUploadContext.hook.js index 0308da0e..efd0677b 100644 --- a/app/mobile/src/context/useUploadContext.hook.js +++ b/app/mobile/src/context/useUploadContext.hook.js @@ -63,14 +63,12 @@ export function useUploadContext() { const actions = { 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 controller = new AbortController(); const entry = { 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, assets: [], current: null, @@ -187,7 +185,7 @@ async function upload(entry, update, complete) { formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'}); } 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' }, signal: entry.cancel.signal, onUploadProgress: (ev) => { @@ -213,7 +211,7 @@ async function upload(entry, update, complete) { } let thumb = 'vthumb;video;' + file.position; let transform = encodeURIComponent(JSON.stringify(["vlq;video", "vhd;video", thumb])); - let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, signal: entry.cancel.signal, onUploadProgress: (ev) => { @@ -239,7 +237,7 @@ async function upload(entry, update, complete) { formData.append("asset", {uri: 'file://' + file.data, name: 'asset', type: 'application/octent-stream'}); } 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' }, signal: entry.cancel.signal, onUploadProgress: (ev) => { diff --git a/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js index 989afd83..be249491 100644 --- a/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js @@ -205,24 +205,16 @@ export function useAddTopic(contentKey) { const assemble = (assets) => { if (!state.locked) { - if (assets?.length) { - return { - assets, - text: state.message, - textColor: state.colorSet ? state.color : null, - textSize: state.sizeSet ? state.size : null, - } - } - else { - return { - text: state.message, - textColor: state.colorSet ? state.color : null, - textSize: state.sizeSet ? state.size : null, - } + return { + assets: assets?.length ? assets : null, + text: state.message, + textColor: state.colorSet ? state.color : null, + textSize: state.sizeSet ? state.size : null, } } else { const message = { + assets: assets?.length ? assets : null, text: state.message, textColor: state.textColorSet ? state.textColor : null, textSize: state.textSizeSet ? state.textSize : null, diff --git a/app/mobile/src/session/conversation/topicItem/TopicItem.jsx b/app/mobile/src/session/conversation/topicItem/TopicItem.jsx index 1edf18a5..4fd274b0 100644 --- a/app/mobile/src/session/conversation/topicItem/TopicItem.jsx +++ b/app/mobile/src/session/conversation/topicItem/TopicItem.jsx @@ -109,34 +109,17 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block ); } - - const renderAsset = (asset) => { - return ( - - { asset.item.image && ( - - )} - { asset.item.video && ( - - )} - { asset.item.audio && ( - - )} - - ) - } - const renderThumb = (thumb) => { return ( - { thumb.item.image && ( - actions.showCarousel(thumb.index)} /> + { thumb.item.type === 'image' && ( + actions.showCarousel(thumb.index)} /> )} - { thumb.item.video && ( - actions.showCarousel(thumb.index)} /> + { thumb.item.type === 'video' && ( + actions.showCarousel(thumb.index)} /> )} - { thumb.item.audio && ( - actions.showCarousel(thumb.index)} /> + { thumb.item.type === 'audio' && ( + actions.showCarousel(thumb.index)} /> )} ); @@ -236,16 +219,17 @@ export function TopicItem({ item, focused, focus, hosting, remove, update, block data={state.assets} defaultIndex={state.carouselIndex} scrollAnimationDuration={1000} + onSnapToItem={(index) => console.log('current index:', index)} renderItem={({ index }) => ( - { state.assets[index].image && ( - + { state.assets[index].type === 'image' && ( + )} - { state.assets[index].video && ( - + { state.assets[index].type === 'video' && ( + )} - { state.assets[index].audio && ( - + { state.assets[index].type === 'audio' && ( + )} )} /> diff --git a/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.jsx b/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.jsx index b889a331..8511232a 100644 --- a/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.jsx +++ b/app/mobile/src/session/conversation/topicItem/audioAsset/AudioAsset.jsx @@ -1,5 +1,5 @@ import { Image, View, Text, TouchableOpacity } from 'react-native'; -import { useRef } from 'react'; +import { useEffect, useRef } from 'react'; import Colors from 'constants/Colors'; import Video from 'react-native-video'; import { useAudioAsset } from './useAudioAsset.hook'; @@ -8,9 +8,9 @@ import Icons from 'react-native-vector-icons/MaterialCommunityIcons'; import audio from 'images/audio.png'; 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); diff --git a/app/mobile/src/session/conversation/topicItem/audioAsset/useAudioAsset.hook.js b/app/mobile/src/session/conversation/topicItem/audioAsset/useAudioAsset.hook.js index 079e8766..66408083 100644 --- a/app/mobile/src/session/conversation/topicItem/audioAsset/useAudioAsset.hook.js +++ b/app/mobile/src/session/conversation/topicItem/audioAsset/useAudioAsset.hook.js @@ -3,7 +3,7 @@ import { ConversationContext } from 'context/ConversationContext'; import { Image } from 'react-native'; import { useWindowDimensions } from 'react-native'; -export function useAudioAsset(topicId, asset) { +export function useAudioAsset(asset) { const [state, setState] = useState({ width: 1, @@ -38,9 +38,13 @@ export function useAudioAsset(topicId, asset) { }, [dimensions]); useEffect(() => { - const url = conversation.actions.getTopicAssetUrl(topicId, asset.full); - updateState({ url }); - }, [topicId, conversation, asset]); + if (asset.encrypted) { + updateState({ url: asset.decrypted, failed: asset.error }); + } + else { + updateState({ url: asset.full }); + } + }, [asset]); const actions = { play: () => { diff --git a/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.jsx b/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.jsx index 03210f5d..1ab776cd 100644 --- a/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.jsx +++ b/app/mobile/src/session/conversation/topicItem/audioThumb/AudioThumb.jsx @@ -4,14 +4,14 @@ import { styles } from './AudioThumb.styled'; import Colors from 'constants/Colors'; import audio from 'images/audio.png'; -export function AudioThumb({ topicId, asset, onAssetView }) { +export function AudioThumb({ label, onAssetView }) { return ( - { asset.label && ( + { label && ( - { asset.label } + { label } )} diff --git a/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx b/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx index 82468daa..10ff837f 100644 --- a/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx +++ b/app/mobile/src/session/conversation/topicItem/imageAsset/ImageAsset.jsx @@ -4,8 +4,8 @@ import { styles } from './ImageAsset.styled'; import Colors from 'constants/Colors'; import Ionicons from 'react-native-vector-icons/AntDesign'; -export function ImageAsset({ topicId, asset, dismiss }) { - const { state, actions } = useImageAsset(topicId, asset); +export function ImageAsset({ asset, dismiss }) { + const { state, actions } = useImageAsset(asset); return ( @@ -33,4 +33,3 @@ export function ImageAsset({ topicId, asset, dismiss }) { ); } - diff --git a/app/mobile/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js b/app/mobile/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js index f3967270..bd2f9590 100644 --- a/app/mobile/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js +++ b/app/mobile/src/session/conversation/topicItem/imageAsset/useImageAsset.hook.js @@ -3,7 +3,7 @@ import { ConversationContext } from 'context/ConversationContext'; import { Image } from 'react-native'; import { useWindowDimensions } from 'react-native'; -export function useImageAsset(topicId, asset) { +export function useImageAsset(asset) { const [state, setState] = useState({ frameWidth: 1, @@ -49,9 +49,13 @@ export function useImageAsset(topicId, asset) { }, [dimensions]); useEffect(() => { - const url = conversation.actions.getTopicAssetUrl(topicId, asset.full); - updateState({ url }); - }, [topicId, conversation, asset]); + if (asset.encrypted) { + updateState({ url: asset.decrypted, failed: asset.error }); + } + else { + updateState({ url: asset.full }); + } + }, [asset]); const actions = { loaded: (e) => { diff --git a/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.jsx b/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.jsx index 3786db6d..ee540ca6 100644 --- a/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.jsx +++ b/app/mobile/src/session/conversation/topicItem/imageThumb/ImageThumb.jsx @@ -4,12 +4,12 @@ import { useImageThumb } from './useImageThumb.hook'; import { styles } from './ImageThumb.styled'; import Colors from 'constants/Colors'; -export function ImageThumb({ topicId, asset, onAssetView }) { - const { state, actions } = useImageThumb(topicId, asset); +export function ImageThumb({ url, onAssetView }) { + const { state, actions } = useImageThumb(); return ( - ); diff --git a/app/mobile/src/session/conversation/topicItem/imageThumb/useImageThumb.hook.js b/app/mobile/src/session/conversation/topicItem/imageThumb/useImageThumb.hook.js index 44469f4d..147c84bd 100644 --- a/app/mobile/src/session/conversation/topicItem/imageThumb/useImageThumb.hook.js +++ b/app/mobile/src/session/conversation/topicItem/imageThumb/useImageThumb.hook.js @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useContext } from 'react'; import { ConversationContext } from 'context/ConversationContext'; import { Image } from 'react-native'; -export function useImageThumb(topicId, asset) { +export function useImageThumb() { const [state, setState] = useState({ loaded: false, @@ -16,11 +16,6 @@ export function useImageThumb(topicId, asset) { setState((s) => ({ ...s, ...value })); } - useEffect(() => { - const url = conversation.actions.getTopicAssetUrl(topicId, asset.thumb); - updateState({ url }); - }, [topicId, conversation, asset]); - const actions = { loaded: (e) => { const { width, height } = e.nativeEvent.source; diff --git a/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js b/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js index 23c2ae49..4268444a 100644 --- a/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js +++ b/app/mobile/src/session/conversation/topicItem/useTopicItem.hook.js @@ -1,4 +1,4 @@ -import { useState, useEffect, useContext } from 'react'; +import { useRef, useState, useEffect, useContext } from 'react'; import { Linking } from 'react-native'; import { ConversationContext } from 'context/ConversationContext'; import { CardContext } from 'context/CardContext'; @@ -8,10 +8,12 @@ import moment from 'moment'; import { useWindowDimensions, Text } from 'react-native'; import Colors from 'constants/Colors'; import { getCardByGuid } from 'context/cardUtil'; -import { decryptTopicSubject } from 'context/sealUtil'; +import { decryptBlock, decryptTopicSubject } from 'context/sealUtil'; import { sanitizeUrl } from '@braintree/sanitize-url'; import Share from 'react-native-share'; 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) { @@ -42,6 +44,8 @@ export function useTopicItem(item, hosting, remove, contentKey) { const account = useContext(AccountContext); const dimensions = useWindowDimensions(); + const cancel = useRef(false); + const updateState = (value) => { setState((s) => ({ ...s, ...value })); } @@ -50,6 +54,43 @@ export function useTopicItem(item, hosting, remove, contentKey) { updateState({ width: dimensions.width, height: dimensions.height }); }, [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(() => { const { topicId, revision, detail, unsealedDetail } = item; @@ -103,7 +144,7 @@ export function useTopicItem(item, hosting, remove, contentKey) { parsed = JSON.parse(data); message = parsed?.text; clickable = clickableText(parsed.text); - assets = parsed.assets; + assets = setAssets(parsed.assets); if (parsed.textSize === 'small') { fontSize = 10; } @@ -145,6 +186,7 @@ export function useTopicItem(item, hosting, remove, contentKey) { if (unsealed) { sealed = false; parsed = unsealed.message; + assets = setAssets(parsed.assets); message = parsed?.text; clickable = clickableText(parsed?.text); if (parsed?.textSize === 'small') { @@ -231,11 +273,50 @@ export function useTopicItem(item, hosting, remove, contentKey) { }; const actions = { - showCarousel: (index) => { - updateState({ carousel: true, carouselIndex: index }); + showCarousel: async (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.TemporaryDirectoryPath + `/${i}.asset${ext}`; + const exists = await RNFS.exists(path); + if (exists) { + RNFS.unlink(path); + } + 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'); + }; + + 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: () => { updateState({ carousel: false }); + cancel.current = true; }, setActive: (activeId) => { updateState({ activeId }); diff --git a/app/mobile/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx b/app/mobile/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx index eece3cc8..239ff60e 100644 --- a/app/mobile/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx +++ b/app/mobile/src/session/conversation/topicItem/videoAsset/VideoAsset.jsx @@ -6,9 +6,11 @@ import { styles } from './VideoAsset.styled'; import Icons from 'react-native-vector-icons/MaterialCommunityIcons'; import { useKeepAwake } from '@sayem314/react-native-keep-awake'; -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(); @@ -16,7 +18,7 @@ export function VideoAsset({ topicId, asset, dismiss }) { { state.url && ( -