mirror of
https://github.com/balzack/databag.git
synced 2025-04-23 01:55:17 +00:00
building mobile message thread
This commit is contained in:
parent
d7991ab0f0
commit
ebc3855df3
@ -1633,7 +1633,7 @@ SPEC CHECKSUMS:
|
||||
RNVectorIcons: 845eda5c7819bd29699cafd0fc98c9d4afe28c96
|
||||
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
||||
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
|
||||
Yoga: 88480008ccacea6301ff7bf58726e27a72931c8d
|
||||
Yoga: 04f1db30bb810187397fa4c37dd1868a27af229c
|
||||
|
||||
PODFILE CHECKSUM: 8461018d8deceb200962c829584af7c2eb345c80
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
}
|
||||
});
|
||||
|
@ -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 <TouchableOpacity onPress={close}><Text>CONVERSATION</Text></TouchableOpacity>;
|
||||
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 (
|
||||
<View style={styles.conversation}>
|
||||
<SafeAreaView style={styles.header}>
|
||||
{close && (
|
||||
<View style={styles.iconSpace}>
|
||||
<IconButton style={styles.back} compact="true" mode="contained" icon="arrow-left" size={28} onPress={close} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.title}>
|
||||
{ state.detailSet && state.subject && (
|
||||
<Text adjustsFontSizeToFit={true} numberOfLines={1} style={styles.label}>{ state.subject }</Text>
|
||||
)}
|
||||
{ state.detailSet && state.host && !state.subject && state.subjectNames.length == 0 && (
|
||||
<Text adjustsFontSizeToFit={true} numberOfLines={1} styles={styles.label}>{ state.strings.notes }</Text>
|
||||
)}
|
||||
{ state.detailSet && !state.subject && state.subjectNames.length > 0 && (
|
||||
<Text adjustsFontSizeToFit={true} numberOfLines={1} style={styles.label}>{ state.subjectNames.join(', ') }</Text>
|
||||
)}
|
||||
{ state.detailSet && !state.subject && state.unknownContacts > 0 && (
|
||||
<Text adjustsFontSizeToFit={true} numberOfLines={1} style={styles.unknown}>{ `, ${state.strings.unknownContact} (${state.unknownContacts})` }</Text>
|
||||
)}
|
||||
</View>
|
||||
{close && <View style={styles.iconSpace} />}
|
||||
</SafeAreaView>
|
||||
<Divider style={styles.border} bold={true} />
|
||||
<FlatList
|
||||
inverted
|
||||
ref={thread}
|
||||
onScroll={onScroll}
|
||||
style={styles.messages}
|
||||
data={state.topics}
|
||||
initialNumToRender={32}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onContentSizeChange={onContent}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0}
|
||||
contentContainerStyle={styles.messages}
|
||||
renderItem={({item}) => {
|
||||
const { host } = state;
|
||||
const card = state.cards.get(item.guid) || null;
|
||||
const profile = state.profile?.guid === item.guid ? state.profile : null;
|
||||
return (
|
||||
<Message
|
||||
topic={item}
|
||||
card={card}
|
||||
profile={profile}
|
||||
host={host}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
keyExtractor={topic => (topic.topicId)}
|
||||
/>
|
||||
<TouchableOpacity style={styles.add} onPress={() => thread.current.scrollToEnd()}>
|
||||
<Text>ADD</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -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[]) => {
|
||||
|
192
app/client/mobile/src/message/Message.module.css
Normal file
192
app/client/mobile/src/message/Message.module.css
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
app/client/mobile/src/message/Message.styled.ts
Normal file
5
app/client/mobile/src/message/Message.styled.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {Colors} from '../constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
});
|
16
app/client/mobile/src/message/Message.tsx
Normal file
16
app/client/mobile/src/message/Message.tsx
Normal file
@ -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 (<Text style={{ height: 32 }}>{ JSON.stringify(topic.data) }</Text>)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {Colors} from '../constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
});
|
11
app/client/mobile/src/message/audioAsset/AudioAsset.tsx
Normal file
11
app/client/mobile/src/message/audioAsset/AudioAsset.tsx
Normal file
@ -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 (<Text>AUDIO</Text>);
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {Colors} from '../constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
});
|
11
app/client/mobile/src/message/binaryAsset/BinaryAsset.tsx
Normal file
11
app/client/mobile/src/message/binaryAsset/BinaryAsset.tsx
Normal file
@ -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 (<Text>BINARY</Text>);
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {Colors} from '../constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
});
|
11
app/client/mobile/src/message/imageAsset/ImageAsset.tsx
Normal file
11
app/client/mobile/src/message/imageAsset/ImageAsset.tsx
Normal file
@ -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 (<Text>IMAGE</Text>);
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
82
app/client/mobile/src/message/useMessage.hook.ts
Normal file
82
app/client/mobile/src/message/useMessage.hook.ts
Normal file
@ -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 }
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {Colors} from '../constants/Colors';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
});
|
11
app/client/mobile/src/message/videoAsset/VideoAsset.tsx
Normal file
11
app/client/mobile/src/message/videoAsset/VideoAsset.tsx
Normal file
@ -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 (<Text>VIDEO</Text>);
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
@ -157,7 +157,11 @@ function ContentTab({scheme}: {scheme: string}) {
|
||||
<Content openConversation={()=>props.navigation.navigate('conversation')} />
|
||||
)}
|
||||
</ContentStack.Screen>
|
||||
<ContentStack.Screen name="conversation" options={{headerBackTitleVisible: false}}>
|
||||
<ContentStack.Screen name="conversation"
|
||||
options={{
|
||||
headerBackTitleVisible: false,
|
||||
...TransitionPresets.ScaleFromCenterAndroid,
|
||||
}}>
|
||||
{props => (
|
||||
<Conversation close={()=>props.navigation.goBack()} />
|
||||
)}
|
||||
|
@ -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}`);
|
||||
|
@ -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<any> {
|
||||
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<void> {
|
||||
private async setTableValue(guid: string, table: string, record: {field: string, value: any}[], where: {field: string, value: string}[]): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user