building mobile message thread

This commit is contained in:
balzack 2024-12-28 20:41:20 -08:00
parent d7991ab0f0
commit ebc3855df3
26 changed files with 748 additions and 29 deletions

View File

@ -1633,7 +1633,7 @@ SPEC CHECKSUMS:
RNVectorIcons: 845eda5c7819bd29699cafd0fc98c9d4afe28c96
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
Yoga: 88480008ccacea6301ff7bf58726e27a72931c8d
Yoga: 04f1db30bb810187397fa4c37dd1868a27af229c
PODFILE CHECKSUM: 8461018d8deceb200962c829584af7c2eb345c80

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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[]) => {

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

View File

@ -0,0 +1,5 @@
import {StyleSheet} from 'react-native';
import {Colors} from '../constants/Colors';
export const styles = StyleSheet.create({
});

View 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>)
}

View File

@ -0,0 +1,5 @@
import {StyleSheet} from 'react-native';
import {Colors} from '../constants/Colors';
export const styles = StyleSheet.create({
});

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

View File

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

View File

@ -0,0 +1,5 @@
import {StyleSheet} from 'react-native';
import {Colors} from '../constants/Colors';
export const styles = StyleSheet.create({
});

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

View File

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

View File

@ -0,0 +1,5 @@
import {StyleSheet} from 'react-native';
import {Colors} from '../constants/Colors';
export const styles = StyleSheet.create({
});

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

View File

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

View 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 }
}

View File

@ -0,0 +1,5 @@
import {StyleSheet} from 'react-native';
import {Colors} from '../constants/Colors';
export const styles = StyleSheet.create({
});

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

View File

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

View File

@ -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()} />
)}

View File

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

View File

@ -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 [];
}
}