From ebc3855df3e0c87576aa2cddde8bc08b7810bc4a Mon Sep 17 00:00:00 2001 From: balzack Date: Sat, 28 Dec 2024 20:41:20 -0800 Subject: [PATCH] building mobile message thread --- app/client/mobile/ios/Podfile.lock | 2 +- app/client/mobile/src/content/Content.tsx | 4 +- .../mobile/src/content/useContent.hook.ts | 8 +- .../mobile/src/context/useAppContext.hook.ts | 2 +- .../src/conversation/Conversation.styled.ts | 42 ++++ .../mobile/src/conversation/Conversation.tsx | 118 ++++++++++- .../src/conversation/useConversation.hook.ts | 9 +- .../mobile/src/message/Message.module.css | 192 ++++++++++++++++++ .../mobile/src/message/Message.styled.ts | 5 + app/client/mobile/src/message/Message.tsx | 16 ++ .../message/audioAsset/AudioAsset.styled.ts | 5 + .../src/message/audioAsset/AudioAsset.tsx | 11 + .../message/audioAsset/useAudioAsset.hook.ts | 41 ++++ .../message/binaryAsset/BinaryAsset.styled.ts | 5 + .../src/message/binaryAsset/BinaryAsset.tsx | 11 + .../binaryAsset/useBinaryAsset.hook.ts | 41 ++++ .../message/imageAsset/ImageAsset.styled.ts | 5 + .../src/message/imageAsset/ImageAsset.tsx | 11 + .../message/imageAsset/useImageAsset.hook.ts | 59 ++++++ .../mobile/src/message/useMessage.hook.ts | 82 ++++++++ .../message/videoAsset/VideoAsset.styled.ts | 5 + .../src/message/videoAsset/VideoAsset.tsx | 11 + .../message/videoAsset/useVideoAsset.hook.ts | 59 ++++++ app/client/mobile/src/session/Session.tsx | 6 +- app/sdk/src/focus.ts | 3 +- app/sdk/src/store.ts | 24 +-- 26 files changed, 748 insertions(+), 29 deletions(-) create mode 100644 app/client/mobile/src/message/Message.module.css create mode 100644 app/client/mobile/src/message/Message.styled.ts create mode 100644 app/client/mobile/src/message/Message.tsx create mode 100644 app/client/mobile/src/message/audioAsset/AudioAsset.styled.ts create mode 100644 app/client/mobile/src/message/audioAsset/AudioAsset.tsx create mode 100644 app/client/mobile/src/message/audioAsset/useAudioAsset.hook.ts create mode 100644 app/client/mobile/src/message/binaryAsset/BinaryAsset.styled.ts create mode 100644 app/client/mobile/src/message/binaryAsset/BinaryAsset.tsx create mode 100644 app/client/mobile/src/message/binaryAsset/useBinaryAsset.hook.ts create mode 100644 app/client/mobile/src/message/imageAsset/ImageAsset.styled.ts create mode 100644 app/client/mobile/src/message/imageAsset/ImageAsset.tsx create mode 100644 app/client/mobile/src/message/imageAsset/useImageAsset.hook.ts create mode 100644 app/client/mobile/src/message/useMessage.hook.ts create mode 100644 app/client/mobile/src/message/videoAsset/VideoAsset.styled.ts create mode 100644 app/client/mobile/src/message/videoAsset/VideoAsset.tsx create mode 100644 app/client/mobile/src/message/videoAsset/useVideoAsset.hook.ts diff --git a/app/client/mobile/ios/Podfile.lock b/app/client/mobile/ios/Podfile.lock index be508cd4..86244e7a 100644 --- a/app/client/mobile/ios/Podfile.lock +++ b/app/client/mobile/ios/Podfile.lock @@ -1633,7 +1633,7 @@ SPEC CHECKSUMS: RNVectorIcons: 845eda5c7819bd29699cafd0fc98c9d4afe28c96 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 - Yoga: 88480008ccacea6301ff7bf58726e27a72931c8d + Yoga: 04f1db30bb810187397fa4c37dd1868a27af229c PODFILE CHECKSUM: 8461018d8deceb200962c829584af7c2eb345c80 diff --git a/app/client/mobile/src/content/Content.tsx b/app/client/mobile/src/content/Content.tsx index 0abc4d8e..9c5e3b8a 100644 --- a/app/client/mobile/src/content/Content.tsx +++ b/app/client/mobile/src/content/Content.tsx @@ -31,7 +31,7 @@ export function Content({openConversation}: {openConversation: ()=>void}) { const addTopic = async () => { setAdding(true); try { - await actions.addTopic( + const id = await actions.addTopic( sealedTopic, subjectTopic, members.filter(id => Boolean(cards.find(card => card.cardId === id))), @@ -40,6 +40,8 @@ export function Content({openConversation}: {openConversation: ()=>void}) { setSubjectTopic(''); setMembers([]); setSealedTopic(false); + actions.setFocus(null, id); + openConversation(); } catch (err) { console.log(err); setAdd(false); diff --git a/app/client/mobile/src/content/useContent.hook.ts b/app/client/mobile/src/content/useContent.hook.ts index 2fbea703..9b7f70ac 100644 --- a/app/client/mobile/src/content/useContent.hook.ts +++ b/app/client/mobile/src/content/useContent.hook.ts @@ -230,11 +230,13 @@ export function useContent() { app.actions.setFocus(cardId, channelId); }, addTopic: async (sealed: boolean, subject: string, contacts: string[]) => { - const content = app.state.session.getContent(); + const content = app.state.session.getContent() if (sealed) { - await content.addChannel(true, 'sealed', {subject}, contacts); + const topic = await content.addChannel(true, 'sealed', { subject }, contacts) + return topic.id; } else { - await content.addChannel(false, 'superbasic', {subject}, contacts); + const topic = await content.addChannel(false, 'superbasic', { subject }, contacts) + return topic.id; } }, }; diff --git a/app/client/mobile/src/context/useAppContext.hook.ts b/app/client/mobile/src/context/useAppContext.hook.ts index b263122b..63bf087e 100644 --- a/app/client/mobile/src/context/useAppContext.hook.ts +++ b/app/client/mobile/src/context/useAppContext.hook.ts @@ -3,7 +3,7 @@ import {DatabagSDK, Session, Focus} from 'databag-client-sdk'; import {SessionStore} from '../SessionStore'; import {NativeCrypto} from '../NativeCrypto'; import {LocalStore} from '../LocalStore'; -const DATABAG_DB = 'db_v231.db'; +const DATABAG_DB = 'db_v234.db'; const SETTINGS_DB = 'ls_v001.db'; const databag = new DatabagSDK( diff --git a/app/client/mobile/src/conversation/Conversation.styled.ts b/app/client/mobile/src/conversation/Conversation.styled.ts index bb1b8272..ee29943d 100644 --- a/app/client/mobile/src/conversation/Conversation.styled.ts +++ b/app/client/mobile/src/conversation/Conversation.styled.ts @@ -2,4 +2,46 @@ import {StyleSheet} from 'react-native'; import {Colors} from '../constants/Colors'; export const styles = StyleSheet.create({ + conversation: { + display: 'flex', + flexDirection: 'column', + height: '100%', + }, + messages: { + paddingBottom: 64, + }, + thread: { + width: '100%', + flexGrow: 1, + }, + add: { + height: 72, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + back: { + flexShrink: 0, + marginRight: 0, + marginLeft: 0, + marginTop: 0, + marginBottom: 0, + backgroundColor: 'transparent', + }, + header: { + display: 'flex', + flexDirection: 'row', + }, + iconSpace: { + width: '10%', + }, + title: { + width: '80%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + label: { + fontSize: 24, + } }); diff --git a/app/client/mobile/src/conversation/Conversation.tsx b/app/client/mobile/src/conversation/Conversation.tsx index cd24de01..f5d39ee5 100644 --- a/app/client/mobile/src/conversation/Conversation.tsx +++ b/app/client/mobile/src/conversation/Conversation.tsx @@ -1,11 +1,121 @@ -import React from 'react'; -import {TouchableOpacity} from 'react-native'; -import {Text} from 'react-native-paper'; +import React, { useEffect, useState, useRef } from 'react'; +import {SafeAreaView, View, FlatList, TouchableOpacity} from 'react-native'; import {styles} from './Conversation.styled'; import {useConversation} from './useConversation.hook'; +import {Message} from '../message/Message'; +import {Icon, Text, IconButton, Divider} from 'react-native-paper'; + +const SCROLL_THRESHOLD = 16; + +export type MediaAsset = { + encrypted?: { type: string, thumb: string, label: string, extension: string, parts: { blockIv: string, partId: string }[] }, + image?: { thumb: string, full: string }, + audio?: { label: string, full: string }, + video?: { thumb: string, lq: string, hd: string }, + binary?: { label: string, extension: string, data: string } +} export function Conversation({close}: {close: ()=>void}) { const { state, actions } = useConversation(); + const [ more, setMore ] = useState(false); + const thread = useRef(); - return CONVERSATION; + const scrolled = useRef(false); + const contentHeight = useRef(0); + const contentLead = useRef(null); + const scrollOffset = useRef(0); + + const loadMore = async () => { + if (!more) { + setMore(true); + await actions.more(); + setMore(false); + } + } + + const onContent = (width, height) => { + const currentLead = state.topics.length > 0 ? state.topics[0].topicId : null; + if (scrolled.current) { + if (currentLead !== contentLead.current) { + const offset = scrollOffset.current + (height - contentHeight.current); + const animated = false; + thread.current.scrollToOffset({offset, animated}); + } + } + contentLead.current = currentLead; + contentHeight.current = height; + } + + const onScroll = (ev) => { + const { contentOffset } = ev.nativeEvent; + const offset = contentOffset.y; + if (offset > scrollOffset.current) { + if (offset > SCROLL_THRESHOLD) { + scrolled.current = true; + } + } else { + if (offset < SCROLL_THRESHOLD) { + scrolled.current = false; + } + } + scrollOffset.current = offset; + } + + return ( + + + {close && ( + + + + )} + + { state.detailSet && state.subject && ( + { state.subject } + )} + { state.detailSet && state.host && !state.subject && state.subjectNames.length == 0 && ( + { state.strings.notes } + )} + { state.detailSet && !state.subject && state.subjectNames.length > 0 && ( + { state.subjectNames.join(', ') } + )} + { state.detailSet && !state.subject && state.unknownContacts > 0 && ( + { `, ${state.strings.unknownContact} (${state.unknownContacts})` } + )} + + {close && } + + + { + const { host } = state; + const card = state.cards.get(item.guid) || null; + const profile = state.profile?.guid === item.guid ? state.profile : null; + return ( + + ) + }} + keyExtractor={topic => (topic.topicId)} + /> + thread.current.scrollToEnd()}> + ADD + + + ); } diff --git a/app/client/mobile/src/conversation/useConversation.hook.ts b/app/client/mobile/src/conversation/useConversation.hook.ts index fe70be9f..f5a208b2 100644 --- a/app/client/mobile/src/conversation/useConversation.hook.ts +++ b/app/client/mobile/src/conversation/useConversation.hook.ts @@ -33,6 +33,7 @@ export function useConversation() { focus: null as Focus | null, layout: null, topics: [] as Topic[], + topicCount: 0, loaded: false, loadingMore: false, profile: null as Profile | null, @@ -90,20 +91,18 @@ export function useConversation() { const { contact, identity } = app.state.session || { }; if (focus && contact && identity) { const setTopics = (topics: Topic[]) => { -console.log(">>>", topics); - if (topics) { const filtered = topics.filter(topic => !topic.blocked); const sorted = filtered.sort((a, b) => { if (a.created < b.created) { - return -1; - } else if (a.created > b.created) { return 1; + } else if (a.created > b.created) { + return -1; } else { return 0; } }); - updateState({ topics: sorted, loaded: true }); + updateState({ topics: sorted, topicCount: topics.length, loaded: true }); } } const setCards = (cards: Card[]) => { diff --git a/app/client/mobile/src/message/Message.module.css b/app/client/mobile/src/message/Message.module.css new file mode 100644 index 00000000..03dfa7ec --- /dev/null +++ b/app/client/mobile/src/message/Message.module.css @@ -0,0 +1,192 @@ +.topic { + display: flex; + flex-direction: column; + width: 100%; + margin-bottom: 8px; + + .media { + display: flex; + align-items: center; + justify-content: center; + position: relative; + + .goleft { + position: absolute; + left: 32px; + } + + .goright { + position: absolute; + right: 32px; + } + } + + .assets { + width: 100%; + height: 128px; + padding-left: 72px; + padding-right: 32px; + margin-top: 8px; + flex-grow: 1; + flex-shrink: 1; + min-width: 0; + margin-bottom: 8px; + overflow: auto; + -ms-overflow-style: none; + scrollbar-width: none; + display: flex; + flex-direction: row; + } + + .assets::-webkit-scrollbar { + display: none; + } + + .editing { + margin-top: 12px; + } + + .controls { + padding: 4px; + justify-content: flex-end; + display: flex; + gap: 8px; + } + + .thumbs { + display: flex; + flex-direction: row; + align-items: center; + flex-grow: 1; + gap: 16px; + } + + .failed { + margin-left: 72px; + margin-right: 32px; + margin-top: 8px; + border-radius: 8px; + color: var(--mantine-color-red-9); + display: flex; + gap: 8px; + } + + .incomplete { + margin-left: 72px; + margin-right: 32px; + margin-top: 8px; + } + + .content { + display: flex; + flex-direction: row; + align-items: flex-start; + width: calc(100% - 32px); + margin-left: 16px; + margin-right: 16px; + padding-left: 8px; + padding-right: 8px; + padding-top: 12px; + border-top: 1px solid var(--mantine-color-text-8); + + .logo { + width: 40px; + height: 40px; + } + + .body { + display: flex; + flex-grow: 1; + flex-direction: column; + padding-left: 8px; + padding-right: 8px; + min-width: 0; + + .padding { + padding-top: 8px; + padding-bottom: 4px; + } + + .text { + word-wrap:break-word; + white-space: pre-wrap; + } + + .locked { + font-style: italic; + color: var(--mantine-color-text-7); + } + + .unconfirmed { + width: 100%; + } + + .header { + display: flex; + flex-direction: row; + align-items: flex-start; + width: 100%; + line-height: 16px; + padding-bottom: 4px; + gap: 16px; + position: relative; + + &:hover { + .options { + display: flex; + flex-grow: 1; + justify-content: flex-end; + } + } + + .options { + display: none; + position: absolute; + top: 0; + right: 0; + } + + .surface { + display: flex; + background-color: var(--mantine-color-surface-4); + gap: 10px; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 8px; + padding-right: 8px; + border-radius: 4px; + } + + .option { + cursor: pointer; + width: 20px; + height: 20px; + color: var(--mantine-color-dbgreen-1); + } + + .careful { + cursor: pointer; + width: 20px; + height: 20px; + color: var(--mantine-color-red-2); + } + + .name { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .timestamp { + font-size: 0.8rem; + } + + .unknown { + font-style: italic; + color: var(--mantine-color-text-7); + } + } + } + } + } +} diff --git a/app/client/mobile/src/message/Message.styled.ts b/app/client/mobile/src/message/Message.styled.ts new file mode 100644 index 00000000..bb1b8272 --- /dev/null +++ b/app/client/mobile/src/message/Message.styled.ts @@ -0,0 +1,5 @@ +import {StyleSheet} from 'react-native'; +import {Colors} from '../constants/Colors'; + +export const styles = StyleSheet.create({ +}); diff --git a/app/client/mobile/src/message/Message.tsx b/app/client/mobile/src/message/Message.tsx new file mode 100644 index 00000000..f16b8765 --- /dev/null +++ b/app/client/mobile/src/message/Message.tsx @@ -0,0 +1,16 @@ +import { useRef, useEffect, useState, useCallback } from 'react'; +import { avatar } from '../constants/Icons' +import {Icon, Text, IconButton, Divider} from 'react-native-paper'; +import { Topic, Card, Profile } from 'databag-client-sdk'; +import classes from './Message.styles.ts' +import { ImageAsset } from './imageAsset/ImageAsset'; +import { AudioAsset } from './audioAsset/AudioAsset'; +import { VideoAsset } from './videoAsset/VideoAsset'; +import { BinaryAsset } from './binaryAsset/BinaryAsset'; +import { useMessage } from './useMessage.hook'; + +export function Message({ topic, card, profile, host }: { topic: Topic, card: Card | null, profile: Profile | null, host: boolean }) { + const { state, actions } = useMessage(); + + return ({ JSON.stringify(topic.data) }) +} diff --git a/app/client/mobile/src/message/audioAsset/AudioAsset.styled.ts b/app/client/mobile/src/message/audioAsset/AudioAsset.styled.ts new file mode 100644 index 00000000..bb1b8272 --- /dev/null +++ b/app/client/mobile/src/message/audioAsset/AudioAsset.styled.ts @@ -0,0 +1,5 @@ +import {StyleSheet} from 'react-native'; +import {Colors} from '../constants/Colors'; + +export const styles = StyleSheet.create({ +}); diff --git a/app/client/mobile/src/message/audioAsset/AudioAsset.tsx b/app/client/mobile/src/message/audioAsset/AudioAsset.tsx new file mode 100644 index 00000000..824393cd --- /dev/null +++ b/app/client/mobile/src/message/audioAsset/AudioAsset.tsx @@ -0,0 +1,11 @@ +import React, { useState, useEffect } from 'react'; +import { Text } from 'react-native' +import { useAudioAsset } from './useAudioAsset.hook'; +import { MediaAsset } from '../../conversation/Conversation'; + +export function AudioAsset({ topicId, asset }: { topicId: string, asset: MediaAsset }) { + const { state, actions } = useAudioAsset(topicId, asset); + + return (AUDIO); +} + diff --git a/app/client/mobile/src/message/audioAsset/useAudioAsset.hook.ts b/app/client/mobile/src/message/audioAsset/useAudioAsset.hook.ts new file mode 100644 index 00000000..e227657c --- /dev/null +++ b/app/client/mobile/src/message/audioAsset/useAudioAsset.hook.ts @@ -0,0 +1,41 @@ +import { useState, useContext, useEffect } from 'react' +import { AppContext } from '../../context/AppContext' +import { Focus } from 'databag-client-sdk' +import { ContextType } from '../../context/ContextType' +import { MediaAsset } from '../../conversation/Conversation'; + +export function useAudioAsset(topicId: string, asset: MediaAsset) { + const app = useContext(AppContext) as ContextType + const [state, setState] = useState({ + dataUrl: null, + loading: false, + loadPercent: 0, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateState = (value: any) => { + setState((s) => ({ ...s, ...value })) + } + + const actions = { + unloadAudio: () => { + updateState({ dataUrl: null }); + }, + loadAudio: async () => { + const { focus } = app.state; + const assetId = asset.audio ? asset.audio.full : asset.encrypted ? asset.encrypted.parts : null; + if (focus && assetId != null && !state.loading) { + updateState({ loading: true, loadPercent: 0 }); + try { + const dataUrl = await focus.getTopicAssetUrl(topicId, assetId, (loadPercent: number)=>{ updateState({ loadPercent }) }); + updateState({ dataUrl, loading: false }); + } catch (err) { + updateState({ loading: false }); + console.log(err); + } + } + } + } + + return { state, actions } +} diff --git a/app/client/mobile/src/message/binaryAsset/BinaryAsset.styled.ts b/app/client/mobile/src/message/binaryAsset/BinaryAsset.styled.ts new file mode 100644 index 00000000..bb1b8272 --- /dev/null +++ b/app/client/mobile/src/message/binaryAsset/BinaryAsset.styled.ts @@ -0,0 +1,5 @@ +import {StyleSheet} from 'react-native'; +import {Colors} from '../constants/Colors'; + +export const styles = StyleSheet.create({ +}); diff --git a/app/client/mobile/src/message/binaryAsset/BinaryAsset.tsx b/app/client/mobile/src/message/binaryAsset/BinaryAsset.tsx new file mode 100644 index 00000000..577169c4 --- /dev/null +++ b/app/client/mobile/src/message/binaryAsset/BinaryAsset.tsx @@ -0,0 +1,11 @@ +import React, { useState, useEffect } from 'react'; +import { Text } from 'react-native' +import { useBinaryAsset } from './useBinaryAsset.hook'; +import { MediaAsset } from '../../conversation/Conversation'; + +export function BinaryAsset({ topicId, asset }: { topicId: string, asset: MediaAsset }) { + const { state, actions } = useBinaryAsset(topicId, asset); + + return (BINARY); +} + diff --git a/app/client/mobile/src/message/binaryAsset/useBinaryAsset.hook.ts b/app/client/mobile/src/message/binaryAsset/useBinaryAsset.hook.ts new file mode 100644 index 00000000..c055ed9d --- /dev/null +++ b/app/client/mobile/src/message/binaryAsset/useBinaryAsset.hook.ts @@ -0,0 +1,41 @@ +import { useState, useContext, useEffect } from 'react' +import { AppContext } from '../../context/AppContext' +import { Focus } from 'databag-client-sdk' +import { ContextType } from '../../context/ContextType' +import { MediaAsset } from '../../conversation/Conversation'; + +export function useBinaryAsset(topicId: string, asset: MediaAsset) { + const app = useContext(AppContext) as ContextType + const [state, setState] = useState({ + dataUrl: '', + loading: false, + loadPercent: 0, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateState = (value: any) => { + setState((s) => ({ ...s, ...value })) + } + + const actions = { + unloadBinary: () => { + updateState({ dataUrl: null }); + }, + loadBinary: async () => { + const { focus } = app.state; + const assetId = asset.binary ? asset.binary.data : asset.encrypted ? asset.encrypted.parts : null; + if (focus && assetId != null && !state.loading) { + updateState({ loading: true, loadPercent: 0 }); + try { + const dataUrl = await focus.getTopicAssetUrl(topicId, assetId, (loadPercent: number)=>{ updateState({ loadPercent }) }); + updateState({ dataUrl, loading: false }); + } catch (err) { + updateState({ loading: false }); + console.log(err); + } + } + } + } + + return { state, actions } +} diff --git a/app/client/mobile/src/message/imageAsset/ImageAsset.styled.ts b/app/client/mobile/src/message/imageAsset/ImageAsset.styled.ts new file mode 100644 index 00000000..bb1b8272 --- /dev/null +++ b/app/client/mobile/src/message/imageAsset/ImageAsset.styled.ts @@ -0,0 +1,5 @@ +import {StyleSheet} from 'react-native'; +import {Colors} from '../constants/Colors'; + +export const styles = StyleSheet.create({ +}); diff --git a/app/client/mobile/src/message/imageAsset/ImageAsset.tsx b/app/client/mobile/src/message/imageAsset/ImageAsset.tsx new file mode 100644 index 00000000..ea51e9d4 --- /dev/null +++ b/app/client/mobile/src/message/imageAsset/ImageAsset.tsx @@ -0,0 +1,11 @@ +import React, { useState, useEffect } from 'react'; +import { Text } from 'react-native' +import { useImageAsset } from './useImageAsset.hook'; +import { MediaAsset } from '../../conversation/Conversation'; + +export function ImageAsset({ topicId, asset }: { topicId: string, asset: MediaAsset }) { + const { state, actions } = useImageAsset(topicId, asset); + + return (IMAGE); +} + diff --git a/app/client/mobile/src/message/imageAsset/useImageAsset.hook.ts b/app/client/mobile/src/message/imageAsset/useImageAsset.hook.ts new file mode 100644 index 00000000..1e757920 --- /dev/null +++ b/app/client/mobile/src/message/imageAsset/useImageAsset.hook.ts @@ -0,0 +1,59 @@ +import { useState, useContext, useEffect } from 'react' +import { AppContext } from '../../context/AppContext' +import { Focus } from 'databag-client-sdk' +import { ContextType } from '../../context/ContextType' +import { MediaAsset } from '../../conversation/Conversation'; + +export function useImageAsset(topicId: string, asset: MediaAsset) { + const app = useContext(AppContext) as ContextType + const [state, setState] = useState({ + thumbUrl: null, + dataUrl: null, + loading: false, + loadPercent: 0, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateState = (value: any) => { + setState((s) => ({ ...s, ...value })) + } + + const setThumb = async () => { + const { focus } = app.state; + const assetId = asset.image ? asset.image.thumb : asset.encrypted ? asset.encrypted.thumb : null; + if (focus && assetId != null) { + try { + const thumbUrl = await focus.getTopicAssetUrl(topicId, assetId); + updateState({ thumbUrl }); + } catch (err) { + console.log(err); + } + } + }; + + useEffect(() => { + setThumb(); + }, [asset]); + + const actions = { + unloadImage: () => { + updateState({ dataUrl: null }); + }, + loadImage: async () => { + const { focus } = app.state; + const assetId = asset.image ? asset.image.full : asset.encrypted ? asset.encrypted.parts : null; + if (focus && assetId != null && !state.loading) { + updateState({ loading: true, loadPercent: 0 }); + try { + const dataUrl = await focus.getTopicAssetUrl(topicId, assetId, (loadPercent: number)=>{ updateState({ loadPercent }) }); + updateState({ dataUrl }); + } catch (err) { + console.log(err); + } + updateState({ loading: false }); + } + } + } + + return { state, actions } +} diff --git a/app/client/mobile/src/message/useMessage.hook.ts b/app/client/mobile/src/message/useMessage.hook.ts new file mode 100644 index 00000000..8239119e --- /dev/null +++ b/app/client/mobile/src/message/useMessage.hook.ts @@ -0,0 +1,82 @@ +import { useState, useContext, useEffect } from 'react' +import { DisplayContext } from '../context/DisplayContext' +import { AppContext } from '../context/AppContext'; +import { ContextType } from '../context/ContextType' + +export function useMessage() { + const app = useContext(AppContext) as ContextType + const display = useContext(DisplayContext) as ContextType + const [state, setState] = useState({ + strings: display.state.strings, + timeFormat: display.state.timeFormat, + dateFormat: display.state.dateFormat, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateState = (value: any) => { + setState((s) => ({ ...s, ...value })) + } + + useEffect(() => { + const { strings, timeFormat, dateFormat } = display.state; + updateState({ strings, timeFormat, dateFormat }); + }, [display.state]); + + const actions = { + block: async (topicId: string) => { + const focus = app.state.focus; + if (focus) { + await focus.setBlockTopic(topicId); + } + }, + flag: async (topicId: string) => { + const focus = app.state.focus; + if (focus) { + await focus.flagTopic(topicId); + } + }, + remove: async (topicId: string) => { + const focus = app.state.focus; + if (focus) { + await focus.removeTopic(topicId); + } + }, + saveSubject: async (topicId: string, sealed: boolean, subject: any) => { + const focus = app.state.focus; + if (focus) { + await focus.setTopicSubject(topicId, sealed ? 'sealedtopic' : 'superbasictopic', ()=>subject, [], ()=>true); + } + }, + getTimestamp: (created: number) => { + const now = Math.floor((new Date()).getTime() / 1000) + const date = new Date(created * 1000); + const offset = now - created; + if(offset < 43200) { + if (state.timeFormat === '12h') { + return date.toLocaleTimeString("en-US", {hour: 'numeric', minute:'2-digit'}); + } + else { + return date.toLocaleTimeString("en-GB", {hour: 'numeric', minute:'2-digit'}); + } + } + else if (offset < 31449600) { + if (state.dateFormat === 'mm/dd') { + return date.toLocaleDateString("en-US", {day: 'numeric', month:'numeric'}); + } + else { + return date.toLocaleDateString("en-GB", {day: 'numeric', month:'numeric'}); + } + } + else { + if (state.dateFormat === 'mm/dd') { + return date.toLocaleDateString("en-US"); + } + else { + return date.toLocaleDateString("en-GB"); + } + } + } + } + + return { state, actions } +} diff --git a/app/client/mobile/src/message/videoAsset/VideoAsset.styled.ts b/app/client/mobile/src/message/videoAsset/VideoAsset.styled.ts new file mode 100644 index 00000000..bb1b8272 --- /dev/null +++ b/app/client/mobile/src/message/videoAsset/VideoAsset.styled.ts @@ -0,0 +1,5 @@ +import {StyleSheet} from 'react-native'; +import {Colors} from '../constants/Colors'; + +export const styles = StyleSheet.create({ +}); diff --git a/app/client/mobile/src/message/videoAsset/VideoAsset.tsx b/app/client/mobile/src/message/videoAsset/VideoAsset.tsx new file mode 100644 index 00000000..4a38b672 --- /dev/null +++ b/app/client/mobile/src/message/videoAsset/VideoAsset.tsx @@ -0,0 +1,11 @@ +import React, { useState, useEffect } from 'react'; +import { Text } from 'react-native' +import { useVideoAsset } from './useVideoAsset.hook'; +import { MediaAsset } from '../../conversation/Conversation'; + +export function VideoAsset({ topicId, asset }: { topicId: string, asset: MediaAsset }) { + const { state, actions } = useVideoAsset(topicId, asset); + + return (VIDEO); +} + diff --git a/app/client/mobile/src/message/videoAsset/useVideoAsset.hook.ts b/app/client/mobile/src/message/videoAsset/useVideoAsset.hook.ts new file mode 100644 index 00000000..8d025920 --- /dev/null +++ b/app/client/mobile/src/message/videoAsset/useVideoAsset.hook.ts @@ -0,0 +1,59 @@ +import { useState, useContext, useEffect } from 'react' +import { AppContext } from '../../context/AppContext' +import { Focus } from 'databag-client-sdk' +import { ContextType } from '../../context/ContextType' +import { MediaAsset } from '../../conversation/Conversation'; + +export function useVideoAsset(topicId: string, asset: MediaAsset) { + const app = useContext(AppContext) as ContextType + const [state, setState] = useState({ + thumbUrl: null, + dataUrl: null, + loading: false, + loadPercent: 0, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateState = (value: any) => { + setState((s) => ({ ...s, ...value })) + } + + const setThumb = async () => { + const { focus } = app.state; + const assetId = asset.video ? asset.video.thumb : asset.encrypted ? asset.encrypted.thumb : null; + if (focus && assetId != null) { + try { + const thumbUrl = await focus.getTopicAssetUrl(topicId, assetId); + updateState({ thumbUrl }); + } catch (err) { + console.log(err); + } + } + }; + + useEffect(() => { + setThumb(); + }, [asset]); + + const actions = { + unloadVideo: () => { + updateState({ dataUrl: null }); + }, + loadVideo: async () => { + const { focus } = app.state; + const assetId = asset.video ? asset.video.hd : asset.encrypted ? asset.encrypted.parts : null; + if (focus && assetId != null && !state.loading) { + updateState({ loading: true, loadPercent: 0 }); + try { + const dataUrl = await focus.getTopicAssetUrl(topicId, assetId, (loadPercent: number)=>{ updateState({ loadPercent }) }); + updateState({ dataUrl, loading: false }); + } catch (err) { + updateState({ loading: false }); + console.log(err); + } + } + } + } + + return { state, actions } +} diff --git a/app/client/mobile/src/session/Session.tsx b/app/client/mobile/src/session/Session.tsx index 063c052c..526ce45e 100644 --- a/app/client/mobile/src/session/Session.tsx +++ b/app/client/mobile/src/session/Session.tsx @@ -157,7 +157,11 @@ function ContentTab({scheme}: {scheme: string}) { props.navigation.navigate('conversation')} /> )} - + {props => ( props.navigation.goBack()} /> )} diff --git a/app/sdk/src/focus.ts b/app/sdk/src/focus.ts index f2692bdf..340e4acd 100644 --- a/app/sdk/src/focus.ts +++ b/app/sdk/src/focus.ts @@ -182,7 +182,7 @@ export class FocusModule implements Focus { if (data) { const { detailRevision, topicDetail } = data; const detail = topicDetail ? topicDetail : await this.getRemoteChannelTopicDetail(id); - if (!this.cacheView || this.cacheView.position > detail.created || (this.cacheView.position === detail.created && this.cacheView.topicId >= id)) { + if (!this.cacheView || this.cacheView.position < detail.created || (this.cacheView.position === detail.created && this.cacheView.topicId >= id)) { const entry = await this.getTopicEntry(id); if (detailRevision > entry.item.detail.revision) { entry.item.detail = this.getTopicDetail(detail, detailRevision); @@ -208,6 +208,7 @@ export class FocusModule implements Focus { if (this.nextRevision === nextRev) { this.nextRevision = null; } + await this.markRead(); this.emitTopics(); this.log.info(`topic revision: ${nextRev}`); diff --git a/app/sdk/src/store.ts b/app/sdk/src/store.ts index bce20c0a..f74cc04a 100644 --- a/app/sdk/src/store.ts +++ b/app/sdk/src/store.ts @@ -153,7 +153,7 @@ export class OfflineStore implements Store { try { return JSON.parse(value); } catch (err) { - console.log(err); + this.log.error(err); } return {}; } @@ -166,7 +166,7 @@ export class OfflineStore implements Store { return JSON.parse(rows[0].value); } } catch (err) { - console.log(err); + this.log.error(err); } return unset; } @@ -182,17 +182,17 @@ export class OfflineStore implements Store { private async getTableValue(guid: string, table: string, field: string, where: {field: string, value: string}[], unset: any): Promise { try { const params = where.map(({value}) => value); - const rows = await this.sql.get(`SELECT ${field} FROM ${table} WHERE ${where.map(column => (column.field + '=?')).join(' AND ')}`, params) - if (rows.length == 1 && rows[0].value != null) { - return this.parse(rows[0].value); + const rows = await this.sql.get(`SELECT ${field} FROM ${table}_${guid} WHERE ${where.map(column => (column.field + '=?')).join(' AND ')}`, params) + if (rows.length == 1 && rows[0][field]) { + return this.parse(rows[0][field]); } } catch (err) { - console.log(err); + this.log.error(err); } return unset; } - private async setTableValue(guid: string, table: string, record: {field: string, value: string}[], where: {field: string, value: string}[]): Promise { + private async setTableValue(guid: string, table: string, record: {field: string, value: any}[], where: {field: string, value: string}[]): Promise { const params = [...record.map(({value}) => JSON.stringify(value)), ...where.map(({value}) => value)] await this.sql.set(`UPDATE ${table}_${guid} SET ${record.map(({field}) => (field + '=?')).join(', ')} WHERE ${where.map(({field}) => (field + '=?')).join(' AND ')}`, params); } @@ -506,7 +506,7 @@ export class OfflineStore implements Store { return await this.getTableValue(guid, 'channel', 'sync', [{field: 'channel_id', value: channelId}], { revision: null, marker: null }); } public async setContentChannelTopicRevision(guid: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise { - await this.setTableValue(guid, 'channel', [{field: 'sync', value: JSON.stringify(sync)}], [{field: 'channel_id', value: channelId}]); + await this.setTableValue(guid, 'channel', [{field: 'sync', value: sync}], [{field: 'channel_id', value: channelId}]); } public async getContentChannelTopics(guid: string, channelId: string, count: number, offset: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]> { const fields = ['topic_id', 'detail', 'unsealed_detail', 'position']; @@ -543,7 +543,7 @@ export class OfflineStore implements Store { return await this.getTableValue(guid, 'card_channel', 'sync', [{field: 'card_id', value: cardId},{field: 'channel_id', value: channelId}], { revision: null, marker: null }); } public async setContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise { - return await this.setTableValue(guid, 'card_channel', [{field: 'sync', value: JSON.stringify(sync)}], [{field: 'card_id', value: cardId}, {field: 'channel_id', value: channelId}]); + return await this.setTableValue(guid, 'card_channel', [{field: 'sync', value: sync}], [{field: 'card_id', value: cardId}, {field: 'channel_id', value: channelId}]); } public async getContactCardChannelTopics(guid: string, cardId: string, channelId: string, count: number, offset: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]> { const fields = ['topic_id', 'detail', 'unsealed_detail', 'position']; @@ -614,7 +614,7 @@ export class OnlineStore implements Store { updated.push({ id, value }); this.setAppValue(guid, `marker_${type}`, updated); } catch (err) { - console.log(err); + this.log.error(err); } } @@ -624,7 +624,7 @@ export class OnlineStore implements Store { const updated = markers.filter((marker: {id: string, value: string}) => (marker.id !== id)); this.setAppValue(guid, `marker_${type}`, updated); } catch (err) { - console.log(err); + this.log.error(err); } } @@ -633,7 +633,7 @@ export class OnlineStore implements Store { try { return markers.map((marker: {id: string, value: string}) => ({ id: marker.id, value: marker.value })); } catch (err) { - console.log(err); + this.log.error(err); return []; } }