diff --git a/app/mobile/App.js b/app/mobile/App.js index b81fad99..f662cc97 100644 --- a/app/mobile/App.js +++ b/app/mobile/App.js @@ -9,27 +9,33 @@ import { StoreContextProvider } from 'context/StoreContext'; import { AppContextProvider } from 'context/AppContext'; import { AccountContextProvider } from 'context/AccountContext'; import { ProfileContextProvider } from 'context/ProfileContext'; +import { CardContextProvider } from 'context/CardContext'; +import { ChannelContextProvider } from 'context/ChannelContext'; export default function App() { return ( - - - - - - } /> - } /> - } /> - } /> - } /> - } /> - - - - - + + + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + + ); } diff --git a/app/mobile/src/api/getChannelDetail.js b/app/mobile/src/api/getChannelDetail.js index cd5b6f04..3b873a86 100644 --- a/app/mobile/src/api/getChannelDetail.js +++ b/app/mobile/src/api/getChannelDetail.js @@ -1,7 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function getChannelDetail(token, channelId) { - let detail = await fetchWithTimeout(`/content/channels/${channelId}/detail?agent=${token}`, { method: 'GET' }); +export async function getChannelDetail(server, token, channelId) { + let detail = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/detail?agent=${token}`, { method: 'GET' }); checkResponse(detail) return await detail.json() } diff --git a/app/mobile/src/api/getChannelSummary.js b/app/mobile/src/api/getChannelSummary.js index ca9ea8cf..062528bc 100644 --- a/app/mobile/src/api/getChannelSummary.js +++ b/app/mobile/src/api/getChannelSummary.js @@ -1,7 +1,7 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function getChannelSummary(token, channelId) { - let summary = await fetchWithTimeout(`/content/channels/${channelId}/summary?agent=${token}`, { method: 'GET' }); +export async function getChannelSummary(server, token, channelId) { + let summary = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/summary?agent=${token}`, { method: 'GET' }); checkResponse(summary) return await summary.json() } diff --git a/app/mobile/src/api/getChannels.js b/app/mobile/src/api/getChannels.js index 85af261c..f7cb3a53 100644 --- a/app/mobile/src/api/getChannels.js +++ b/app/mobile/src/api/getChannels.js @@ -1,11 +1,11 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function getChannels(token, revision) { +export async function getChannels(server, token, revision) { let param = "?agent=" + token if (revision != null) { param += '&channelRevision=' + revision } - let channels = await fetchWithTimeout('/content/channels' + param, { method: 'GET' }); + let channels = await fetchWithTimeout(`https://${server}/content/channels${param}`, { method: 'GET' }); checkResponse(channels) let ret = await channels.json() return ret; diff --git a/app/mobile/src/context/CardContext.js b/app/mobile/src/context/CardContext.js new file mode 100644 index 00000000..1bc8fb18 --- /dev/null +++ b/app/mobile/src/context/CardContext.js @@ -0,0 +1,14 @@ +import { createContext } from 'react'; +import { useCardContext } from './useCardContext.hook'; + +export const CardContext = createContext({}); + +export function CardContextProvider({ children }) { + const { state, actions } = useCardContext(); + return ( + + {children} + + ); +} + diff --git a/app/mobile/src/context/ChannelContext.js b/app/mobile/src/context/ChannelContext.js new file mode 100644 index 00000000..103e537c --- /dev/null +++ b/app/mobile/src/context/ChannelContext.js @@ -0,0 +1,14 @@ +import { createContext } from 'react'; +import { useChannelContext } from './useChannelContext.hook'; + +export const ChannelContext = createContext({}); + +export function ChannelContextProvider({ children }) { + const { state, actions } = useChannelContext(); + return ( + + {children} + + ); +} + diff --git a/app/mobile/src/context/useAccountContext.hook.js b/app/mobile/src/context/useAccountContext.hook.js index 5e2c7031..26f1568a 100644 --- a/app/mobile/src/context/useAccountContext.hook.js +++ b/app/mobile/src/context/useAccountContext.hook.js @@ -34,6 +34,8 @@ export function useAccountContext() { } catch(err) { console.log(err); + syncing.current = false; + return; } syncing.current = false; diff --git a/app/mobile/src/context/useAppContext.hook.js b/app/mobile/src/context/useAppContext.hook.js index 60b9c4d9..f4aba29e 100644 --- a/app/mobile/src/context/useAppContext.hook.js +++ b/app/mobile/src/context/useAppContext.hook.js @@ -7,6 +7,7 @@ import { getUsername } from 'api/getUsername'; import { StoreContext } from 'context/StoreContext'; import { AccountContext } from 'context/AccountContext'; import { ProfileContext } from 'context/ProfileContext'; +import { ChannelContext } from 'context/ChannelContext'; export function useAppContext() { const [state, setState] = useState({ @@ -16,6 +17,7 @@ export function useAppContext() { const store = useContext(StoreContext); const account = useContext(AccountContext); const profile = useContext(ProfileContext); + const channel = useContext(ChannelContext); const delay = useRef(2); const ws = useRef(null); @@ -41,6 +43,7 @@ export function useAppContext() { const setSession = async (access) => { await account.actions.setSession(access); await profile.actions.setSession(access); + await channel.actions.setSession(access); updateState({ session: true }); setWebsocket(access.server, access.appToken); } @@ -48,6 +51,7 @@ export function useAppContext() { const clearSession = async () => { account.actions.clearSession(); profile.actions.clearSession(); + channel.actions.clearSession(); updateState({ session: false }); clearWebsocket(); } @@ -84,8 +88,14 @@ export function useAppContext() { ws.current.onmessage = (ev) => { try { const rev = JSON.parse(ev.data); - profile.actions.setRevision(rev.profile); - account.actions.setRevision(rev.account); + try { + profile.actions.setRevision(rev.profile); + account.actions.setRevision(rev.account); + channel.actions.setRevision(rev.channel); + } + catch(err) { + console.log(err); + } updateState({ disconnected: false }); } catch (err) { diff --git a/app/mobile/src/context/useCardContext.hook.js b/app/mobile/src/context/useCardContext.hook.js new file mode 100644 index 00000000..f039a77a --- /dev/null +++ b/app/mobile/src/context/useCardContext.hook.js @@ -0,0 +1,68 @@ +import { useState, useRef, useContext } from 'react'; +import { StoreContext } from 'context/StoreContext'; + +export function useCardContext() { + const [state, setState] = useState({ + }); + const store = useContext(StoreContext); + + const session = useRef(null); + const curRevision = useRef(null); + const setRevision = useRef(null); + const syncing = useRef(false); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })) + } + + const sync = async () => { + if (!syncing.current && setRevision.current !== curRevision.current) { + syncing.current = true; + + try { + const revision = curRevision.current; + const { server, appToken, guid } = session.current; + + // get and store + + updateState({ status }); + setRevision.current = revision; + } + catch(err) { + console.log(err); + syncing.current = false; + return; + } + + syncing.current = false; + sync(); + } + }; + + const actions = { + setSession: async (access) => { + const { guid, server, appToken } = access; + + // load + + const revision = await store.actions.getCardRevision(guid); + + // update + + setRevision.current = revision; + curRevision.current = revision; + session.current = access; + }, + clearSession: () => { + session.current = {}; + updateState({ account: null }); + }, + setRevision: (rev) => { + curRevision.current = rev; + sync(); + }, + } + + return { state, actions } +} + diff --git a/app/mobile/src/context/useChannelContext.hook.js b/app/mobile/src/context/useChannelContext.hook.js new file mode 100644 index 00000000..cd3a784f --- /dev/null +++ b/app/mobile/src/context/useChannelContext.hook.js @@ -0,0 +1,95 @@ +import { useState, useRef, useContext } from 'react'; +import { StoreContext } from 'context/StoreContext'; +import { getChannels } from 'api/getChannels'; +import { getChannelDetail } from 'api/getChannelDetail'; +import { getChannelSummary } from 'api/getChannelSummary'; + +export function useChannelContext() { + const [state, setState] = useState({ + }); + const store = useContext(StoreContext); + + const session = useRef(null); + const curRevision = useRef(null); + const setRevision = useRef(null); + const syncing = useRef(false); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })) + } + + const sync = async () => { + + if (!syncing.current && setRevision.current !== curRevision.current) { + syncing.current = true; + + try { + const revision = curRevision.current; + const { server, appToken, guid } = session.current; + + const delta = await getChannels(server, appToken, setRevision.current); + for (let channel of delta) { + if (channel.data) { + if (channel.data.channelDetail && channel.data.channelSummary) { + await store.actions.setChannelItem(guid, channel); + } + else { + const { detailRevision, topicRevision, channelDetail, channelSummary } = channel.data; + const view = await store.actions.getChannelItemView(guid, channel.id); + if (view.detailRevision != detailRevision) { + const detail = await getChannelDetail(server, appToken, channel.id); + await store.actions.setChannelItemDetail(guid, channel.id, detailRevision, detail); + } + if (view.topicRevision != topicRevision) { + const summary = await getChannelSummary(server, appToken, channel.id); + await store.actions.setChannelItemSummary(guid, channel.id, topicRevision, summary); + } + await store.actions.setChannelItemRevision(guid, channel.revision); + } + } + else { + await store.actions.clearChannelItem(channel.id); + } + } + + setRevision.current = revision; + await store.actions.setChannelRevision(guid, revision); + } + catch(err) { + console.log(err); + syncing.current = false; + return; + } + + syncing.current = false; + sync(); + } + }; + + const actions = { + setSession: async (access) => { + const { guid, server, appToken } = access; + + // load + + const revision = await store.actions.getChannelRevision(guid); + + // update + + setRevision.current = revision; + curRevision.current = revision; + session.current = access; + }, + clearSession: () => { + session.current = {}; + updateState({ account: null }); + }, + setRevision: (rev) => { + curRevision.current = rev; + sync(); + }, + } + + return { state, actions } +} + diff --git a/app/mobile/src/context/useProfileContext.hook.js b/app/mobile/src/context/useProfileContext.hook.js index b3f92c42..3fcd4764 100644 --- a/app/mobile/src/context/useProfileContext.hook.js +++ b/app/mobile/src/context/useProfileContext.hook.js @@ -36,6 +36,8 @@ export function useProfileContext() { } catch(err) { console.log(err); + syncing.current = false; + return; } syncing.current = false; diff --git a/app/mobile/src/context/useStoreContext.hook.js b/app/mobile/src/context/useStoreContext.hook.js index 89b68342..e5ec4ee6 100644 --- a/app/mobile/src/context/useStoreContext.hook.js +++ b/app/mobile/src/context/useStoreContext.hook.js @@ -1,7 +1,7 @@ import { useEffect, useState, useRef, useContext } from 'react'; import SQLite from "react-native-sqlite-storage"; -const DATABAG_DB = 'databag_v005.db'; +const DATABAG_DB = 'databag_v011.db'; export function useStoreContext() { const [state, setState] = useState({}); @@ -11,6 +11,11 @@ export function useStoreContext() { setState((s) => ({ ...s, ...value })) } + const initSession = async (guid) => { + await db.current.executeSql(`CREATE TABLE IF NOT EXISTS channel_${guid} (channel_id text, revision integer, detail_revision integer, topic_revision integer, detail text, summary text, unique(channel_id))`); + await db.current.executeSql(`CREATE TABLE IF NOT EXISTS topic_${guid} (channel_id text, topic_id text, revision integer, detail_revision integer, detail text, unique(channel_id, topic_id))`); + } + const actions = { init: async () => { SQLite.DEBUG(false); @@ -21,6 +26,7 @@ export function useStoreContext() { return await getAppValue(db.current, 'session'); }, setSession: async (access) => { + await initSession(access.guid); await db.current.executeSql("UPDATE app SET value=? WHERE key='session';", [encodeObject(access)]); }, clearSession: async () => { @@ -33,17 +39,15 @@ export function useStoreContext() { }, setProfile: async (guid, profile) => { const dataId = `${guid}_profile`; - await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values (?, null);", [dataId]); - await db.current.executeSql("UPDATE app SET value=? WHERE key=?;", [encodeObject(profile), dataId]); + await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(profile)]); }, getProfileRevision: async (guid) => { const dataId = `${guid}_profileRevision`; - return await getAppValue(db.current, dataId, 0); + return await getAppValue(db.current, dataId, null); }, setProfileRevision: async (guid, revision) => { const dataId = `${guid}_profileRevision`; - await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values (?, 0);", [dataId]); - await db.current.executeSql("UPDATE app SET value=? WHERE key=?;", [encodeObject(revision), dataId]); + await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(revision)]); }, getAccountStatus: async (guid) => { @@ -52,18 +56,73 @@ export function useStoreContext() { }, setAccountStatus: async (guid, status) => { const dataId = `${guid}_status`; - await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values (?, null);", [dataId]); - await db.current.executeSql("UPDATE app SET value=? WHERE key=?;", [encodeObject(status), dataId]); + await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(status)]); }, getAccountRevision: async (guid) => { const dataId = `${guid}_accountRevision`; - return await getAppValue(db.current, dataId, 0); + return await getAppValue(db.current, dataId, null); }, setAccountRevision: async (guid, revision) => { const dataId = `${guid}_accountRevision`; - await db.current.executeSql("INSERT OR IGNORE INTO app (key, value) values (?, 0);", [dataId]); - await db.current.executeSql("UPDATE app SET value=? WHERE key=?;", [encodeObject(revision), dataId]); + await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(revision)]); }, + + getCardRevision: async (guid) => { + const dataId = `${guid}_cardRevision`; + return await getAppValue(db.current, dataId, null); + }, + setCardRevision: async (guid, revision) => { + const dataId = `${guid}_cardRevision`; + await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(revision)]); + }, + + getChannelRevision: async (guid) => { + const dataId = `${guid}_channelRevision`; + return await getAppValue(db.current, dataId, null); + }, + setChannelRevision: async (guid, revision) => { + const dataId = `${guid}_channelRevision`; + await db.current.executeSql("INSERT OR REPLACE INTO app (key, value) values (?, ?);", [dataId, encodeObject(revision)]); + }, + setChannelItem: async (guid, channel) => { + const { id, revision, data } = channel; + await db.current.executeSql(`INSERT OR REPLACE INTO channel_${guid} (channel_id, revision, detail_revision, topic_revision, detail, summary) values (?, ?, ?, ?, ?, ?);`, [id, revision, data.detailRevision, data.topicRevision, encodeObject(data.channelDetail), encodeObject(data.channelSummary)]); + }, + clearChannelItem: async (guid, channelId) => { + await db.current.executeSql(`DELETE FROM channel_${guid} WHERE channel_id=?`, [channelId]); + }, + setChannelItemRevision: async (guid, channelId, revision) => { + await db.current.executeSql(`UPDATE channel_${guid} set revision=? where channel_id=?`, [revision, channelId]); + }, + setChannelItemDetail: async (guid, channelId, revision, detail) => { + await db.current.executeSql(`UPDATE channel_${guid} set detail_revision=?, detail=? where channel_id=?`, [revision, encodeObject(detail), channelId]); + }, + setChannelItemSummary: async (guid, channelId, revision, summary) => { + await db.current.executeSql(`UPDATE channel_${guid} set topic_revision=?, summary=? where channel_id=?`, [revision, encodeObject(summary), channelId]); + }, + getChannelItemView: async (guid, channelId) => { +console.log("HERE", channelId); + const values = await getAppValues(db.current, `SELECT revision, detail_revision, topic_revision FROM channel_${guid} WHERE channel_id=?`, [channelId]); + if (!values.length) { + return {}; + } + return { + revision: values[0].revision, + detailRevision: values[0].detail_revision, + topicRevision: values[0].topic_revision, + }; + }, + getChannelItems: async (guid) => { + const values = await getAppValues(db.current, `SELECT channel_id, revision, detail_revision, topic_revision, detail, summary FROM channel_${guid}`, []); + return values.map(channel => ({ + channelId: channel.channel_id, + revision: channel.revision, + detailRevision: channel.detail_revision, + topicRevision: channel.topic_revision, + detail: decodeObject(channel.detail), + summary: decodeObject(channel.summary), + })); + }, } return { state, actions } } @@ -105,4 +164,17 @@ async function getAppValue(sql: SQLite.SQLiteDatabase, id: string, unset) { return unset; } +async function getAppValues(sql: SQLite.SQLiteDatabase, query: string, params) { + const res = await sql.executeSql(query, params); + if (!hasResult(res)) { + return []; + } + const values = []; + for (let i = 0; i < res[0].rows.length; i++) { + values.push(res[0].rows.item(i)); + } + return values; +} + +