From dcf0699a3b5abd214b22703e1b6d0c5768806e62 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Sun, 10 Jul 2022 00:33:50 -0700 Subject: [PATCH] adding staggard topic support --- net/server/internal/api_getChannelTopics.go | 4 +- .../User/Conversation/useConversation.hook.js | 11 +- net/web/src/VirtualList/VirtualList.jsx | 14 + net/web/src/api/getChannelTopics.js | 22 +- net/web/src/api/getContactChannelTopics.js | 22 +- net/web/src/context/useCardContext.hook.js | 4 +- net/web/src/context/useChannelContext.hook.js | 4 +- .../context/useConversationContext.hook.js | 273 +++++++++--------- 8 files changed, 210 insertions(+), 144 deletions(-) diff --git a/net/server/internal/api_getChannelTopics.go b/net/server/internal/api_getChannelTopics.go index 297ae070..d0a2cc95 100644 --- a/net/server/internal/api_getChannelTopics.go +++ b/net/server/internal/api_getChannelTopics.go @@ -132,7 +132,7 @@ func GetChannelTopics(w http.ResponseWriter, r *http.Request) { for _, slot := range slots { if slot.Topic != nil { if countSet { - w.Header().Set("Topic-Marker", strconv.FormatUint(uint64(slot.ID), 10)) + w.Header().Set("topic-marker", strconv.FormatUint(uint64(slot.ID), 10)) countSet = false } response = append(response, getTopicModel(&slot)) @@ -140,7 +140,7 @@ func GetChannelTopics(w http.ResponseWriter, r *http.Request) { } } - w.Header().Set("Topic-Revision", strconv.FormatInt(channelSlot.Revision, 10)) + w.Header().Set("topic-revision", strconv.FormatInt(channelSlot.Revision, 10)) WriteResponse(w, response) } diff --git a/net/web/src/User/Conversation/useConversation.hook.js b/net/web/src/User/Conversation/useConversation.hook.js index bb11351b..8b4a2689 100644 --- a/net/web/src/User/Conversation/useConversation.hook.js +++ b/net/web/src/User/Conversation/useConversation.hook.js @@ -41,14 +41,23 @@ export function useConversation() { }, [cardId, channelId]); useEffect(() => { + let topics = Array.from(conversation.state.topics.values()).sort((a, b) => { + if (a?.data?.topicDetail?.created > b?.data?.topicDetail?.created) { + return 1; + } + if (a?.data?.topicDetail?.created < b?.data?.topicDetail?.created) { + return -1; + } + return 0; + }); updateState({ init: conversation.state.init, subject: conversation.state.subject, contacts: conversation.state.contacts, cardId: conversation.state.cardId, channelId: conversation.state.channelId, - topics: Array.from(conversation.state.topics.values()), members: conversation.state.members, + topics, }); }, [conversation]); diff --git a/net/web/src/VirtualList/VirtualList.jsx b/net/web/src/VirtualList/VirtualList.jsx index 0284659a..be5b5ece 100644 --- a/net/web/src/VirtualList/VirtualList.jsx +++ b/net/web/src/VirtualList/VirtualList.jsx @@ -239,6 +239,20 @@ export function VirtualList({ id, items, itemRenderer }) { const setItems = () => { + // align containers in case history was loaded + if (containers.current.length > 0) { + let container = containers.current[0]; + for (let j = 0; j < itemView.current.length; j++) { + if (itemView.current[j].id == container.id) { + for(let i = 0; i < containers.current.length; i++) { + containers.current[i].index = i + j; + } + break; + } + } + } + + // remove containers following any removed item for (let i = 0; i < containers.current.length; i++) { let container = containers.current[i]; if (itemView.current.length <= container.index || itemView.current[container.index].id != container.id) { diff --git a/net/web/src/api/getChannelTopics.js b/net/web/src/api/getChannelTopics.js index f553b7c4..69cdd9e4 100644 --- a/net/web/src/api/getChannelTopics.js +++ b/net/web/src/api/getChannelTopics.js @@ -1,13 +1,29 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function getChannelTopics(token, channelId, revision) { +export async function getChannelTopics(token, channelId, revision, count, begin, end) { let rev = '' if (revision != null) { rev = `&revision=${revision}` } - let topics = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}${rev}`, + let cnt = '' + if (count != null) { + cnt = `&count=${count}` + } + let bgn = '' + if (begin != null) { + bgn = `&begin=${begin}` + } + let edn = '' + if (end != null) { + edn = `&end=${end}` + } + let topics = await fetchWithTimeout(`/content/channels/${channelId}/topics?agent=${token}${rev}${cnt}${bgn}${edn}`, { method: 'GET' }); checkResponse(topics) - return await topics.json() + return { + marker: topics.headers.get('topic-marker'), + revision: topics.headers.get('topic-revision'), + topics: await topics.json(), + } } diff --git a/net/web/src/api/getContactChannelTopics.js b/net/web/src/api/getContactChannelTopics.js index 597abf48..cf55dd36 100644 --- a/net/web/src/api/getContactChannelTopics.js +++ b/net/web/src/api/getContactChannelTopics.js @@ -1,13 +1,29 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; -export async function getContactChannelTopics(server, token, channelId, revision) { +export async function getContactChannelTopics(server, token, channelId, revision, count, begin, end) { let rev = '' if (revision != null) { rev = `&revision=${revision}` } - let topics = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?contact=${token}${rev}`, + let cnt = '' + if (count != null) { + cnt = `&count=${count}` + } + let bgn = '' + if (begin != null) { + bgn = `&begin=${begin}` + } + let edn = '' + if (end != null) { + edn = `&end=${end}` + } + let topics = await fetchWithTimeout(`https://${server}/content/channels/${channelId}/topics?contact=${token}${rev}${cnt}${bgn}${edn}`, { method: 'GET' }); checkResponse(topics) - return await topics.json() + return { + marker: topics.headers.get('topic-marker'), + revision: topics.headers.get('topic-revision'), + topics: await topics.json(), + } } diff --git a/net/web/src/context/useCardContext.hook.js b/net/web/src/context/useCardContext.hook.js index f740e410..de469994 100644 --- a/net/web/src/context/useCardContext.hook.js +++ b/net/web/src/context/useCardContext.hook.js @@ -261,12 +261,12 @@ export function useCardContext() { let channel = card.channels.get(channelId); return channel?.revision; }, - getChannelTopics: async (cardId, channelId, revision) => { + getChannelTopics: async (cardId, channelId, revision, count, begin, end) => { let card = cards.current.get(cardId); let node = card.data.cardProfile.node; let channel = card.channels.get(channelId); let token = card.data.cardProfile.guid + '.' + card.data.cardDetail.token; - return await getContactChannelTopics(node, token, channelId, revision); + return await getContactChannelTopics(node, token, channelId, revision, count, begin, end); }, getChannelTopic: async (cardId, channelId, topicId) => { let card = cards.current.get(cardId); diff --git a/net/web/src/context/useChannelContext.hook.js b/net/web/src/context/useChannelContext.hook.js index d20cbdd5..6fc05cfc 100644 --- a/net/web/src/context/useChannelContext.hook.js +++ b/net/web/src/context/useChannelContext.hook.js @@ -127,8 +127,8 @@ export function useChannelContext() { let channel = channels.current.get(channelId); return channel?.revision; }, - getChannelTopics: async (channelId, revision) => { - return await getChannelTopics(access.current, channelId, revision); + getChannelTopics: async (channelId, revision, count, begin, end) => { + return await getChannelTopics(access.current, channelId, revision, count, begin, end); }, getChannelTopic: async (channelId, topicId) => { return await getChannelTopic(access.current, channelId, topicId); diff --git a/net/web/src/context/useConversationContext.hook.js b/net/web/src/context/useConversationContext.hook.js index 4b8cb53a..7ede1e4e 100644 --- a/net/web/src/context/useConversationContext.hook.js +++ b/net/web/src/context/useConversationContext.hook.js @@ -4,6 +4,7 @@ import { CardContext } from 'context/CardContext'; import { ChannelContext } from 'context/ChannelContext'; export function useConversationContext() { + const TOPIC_BATCH = 32; const [state, setState] = useState({ init: false, @@ -15,15 +16,27 @@ export function useConversationContext() { topics: new Map(), }); + const EVENT_OPEN = 1; + const EVENT_MORE = 2; + const EVENT_UPDATE = 3; + const events = useRef([]); + + const channelView = useRef({ + cardId: null, + channelId: null, + batch: 1, + revision: null, + begin: null, + init: false, + error: false, + }); + const card = useContext(CardContext); const channel = useContext(ChannelContext); const profile = useContext(ProfileContext); const topics = useRef(new Map()); - const revision = useRef(null); - const count = useRef(0); - const conversationId = useRef(null); const view = useRef(0); - const gone = useRef(false); + const serialize = useRef(0); const updateState = (value) => { setState((s) => ({ ...s, ...value })); @@ -68,109 +81,119 @@ export function useConversationContext() { return members; } - const setTopics = async () => { - const { cardId, channelId } = conversationId.current; - const curRevision = revision.current; - const curView = view.current; - + const getChannel = () => { + const { cardId, channelId } = channelView.current; if (cardId) { - let deltaRevision = card.actions.getChannelRevision(cardId, channelId); - if (!deltaRevision && !gone.current) { - gone.current = true; - window.alert("This converstaion has been removed"); - return; + return card.actions.getChannel(cardId, channelId); + } + return channel.actions.getChannel(channelId); + } + + const getTopicDelta = async (revision, count, begin, end) => { + + const { cardId, channelId } = channelView.current; + if (cardId) { + return await card.actions.getChannelTopics(cardId, channelId, revision, count, begin, end); + } + return await channel.actions.getChannelTopics(channelId, revision, count, begin, end); + } + + const getTopic = async (topicId) => { + const { cardId, channelId } = channelView.current; + if (cardId) { + return await card.actions.getChannelTopic(cardId, channelId, topicId); + } + return await channel.actions.getChannelTopic(channelId, topicId); + } + + const getChannelRevision = async () => { + const { cardId, channelId } = channelView.current; + if (cardId) { + return await card.actions.getChannelRevision(cardId, channelId); + } + return await channel.actions.getChannelRevision(channelId); + } + + const setTopicDelta = async (delta, curView) => { + for (let topic of delta) { + if (topic.data == null) { + if (curView == view.current) { + topics.current.delete(topic.id); + } } - if (curRevision != deltaRevision) { - let conversation = card.actions.getChannel(cardId, channelId); - let subject = getSubject(conversation); - let contacts = getContacts(conversation); - let members = getMembers(conversation); - let delta = await card.actions.getChannelTopics(cardId, channelId, curRevision); - for (let topic of delta) { - if (topic.data == null) { - topics.current.delete(topic.id); + else { + let cur = topics.current.get(topic.id); + if (cur == null) { + cur = { id: topic.id, data: {} }; + } + if (topic.data.detailRevision != cur.data.detailRevision) { + if(topic.data.topicDetail) { + cur.data.topicDetail = topic.data.topicDetail; + cur.data.detailRevision = topic.data.detailRevision; } else { - let cur = topics.current.get(topic.id); - if (cur == null) { - cur = { id: topic.id, data: {} }; - } - if (topic.data.detailRevision != cur.data.detailRevision) { - if(topic.data.topicDetail) { - cur.data.topicDetail = topic.data.topicDetail; - cur.data.detailRevision = topic.data.detailRevision; - } - else { - let slot = await card.actions.getChannelTopic(cardId, channelId, topic.id); - cur.data.topicDetail = slot.data.topicDetail; - cur.data.detailRevision = slot.data.detailRevision; - } - } - cur.revision = topic.revision; - topics.current.set(topic.id, cur); + let slot = await getTopic(topic.id); + cur.data.topicDetail = slot.data.topicDetail; + cur.data.detailRevision = slot.data.detailRevision; } } - if (curView == view.current) { - updateState({ - init: true, - subject, - contacts, - members, - topics: topics.current, - }); - revision.current = deltaRevision; - } - else { - topics.current = new Map(); + cur.revision = topic.revision; + if (curView == view.current) { + topics.current.set(topic.id, cur); } } } - else { - let deltaRevision = channel.actions.getChannelRevision(channelId); - if (curRevision != deltaRevision) { - let conversation = channel.actions.getChannel(channelId); - let subject = getSubject(conversation); - let contacts = getContacts(conversation); - let members = getMembers(conversation); - let delta = await channel.actions.getChannelTopics(channelId, curRevision); - for (let topic of delta) { - if (topic.data == null) { - topics.current.delete(topic.id); - } - else { - let cur = topics.current.get(topic.id); - if (cur == null) { - cur = { id: topic.id, data: {} }; - } - if (topic.data.detailRevision != cur.data.detailRevision) { - if(topic.data.topicDetail != null) { - cur.data.topicDetail = topic.data.topicDetail; - cur.data.detailRevision = topic.data.detailRevision; - } - else { - let slot = await channel.actions.getChannelTopic(channelId, topic.id); - cur.data.topicDetail = slot.data.topicDetail; - cur.data.detailRevision = slot.data.detailRevision; - } - } - cur.revision = topic.revision; - topics.current.set(topic.id, cur); - } + } + + const setTopics = async (ev) => { + const curView = view.current; + try { + if (ev.type == EVENT_OPEN) { + const { cardId, channelId } = ev.data; + channelView.current.cardId = cardId; + channelView.current.channelId = channelId; + channelView.current.batch = 1; + channelView.current.error = false; + channelView.current.init = true; + let delta = await getTopicDelta(null, TOPIC_BATCH, null, null); + await setTopicDelta(delta.topics, curView); + channelView.current.revision = delta.revision; + channelView.current.begin = delta.marker; + } + else if (ev.type == EVENT_MORE) { + if (channelView.current.init) { + channelView.current.batch += 1; + let delta = await getTopicDelta(null, channelView.current.batch * TOPIC_BATCH, null, channelView.current.begin); + await setTopicDelta(delta.topics, curView); + channelView.current.begin = delta.marker; } - if (curView == view.current) { - updateState({ - init: true, - subject, - contacts, - members, - topics: topics.current, - }); - revision.current = deltaRevision; - } - else { - topics.current = new Map(); + } + else if (ev.type == EVENT_UPDATE) { + let deltaRevision = getChannelRevision(); + if (channelView.current.init && deltaRevision != channelView.current.revision) { + let delta = await getTopicDelta(channelView.current.revision, null, channelView.current.begin, null); + await setTopicDelta(delta.topics, curView); + channelView.current.revision = delta.revision } } + let chan = getChannel(); + let subject = getSubject(chan); + let contacts = getContacts(chan); + let members = getMembers(chan); + updateState({ + init: true, + subject, + contacts, + members, + topics: topics.current, + }); + } + catch (err) { + console.log(err); + if (!channelView.current.error) { + window.alert("This converstaion failed to update"); + channelView.current.error = true; + } } } @@ -180,58 +203,47 @@ export function useConversationContext() { return; } - const { cardId } = conversationId.current; - if (cardId && card.state.cards.get(cardId)?.data.cardDetail.status != 'connected') { - window.alert("You are disconnected from the host"); - conversationId.current = null; - return; - } + if (serialize.current == 0) { + serialize.current++; - if (count.current == 0) { - count.current += 1; - while(count.current > 0) { - try { - await setTopics(); - } - catch (err) { - console.log(err); - } - count.current -= 1; + while (events.current.length > 0) { + await setTopics(events.current[0]); + events.current.shift(); } - } - else { - count.current += 1; + + serialize.current--; } }; useEffect(() => { - if (conversationId.current != null) { - updateConversation(); - } + events.current.push({ type: EVENT_UPDATE }); + updateConversation(); }, [card, channel]); const actions = { setConversationId: (cardId, channelId) => { view.current += 1; - conversationId.current = { cardId, channelId }; - revision.current = null; - gone.current = false; - topics.current = new Map(); - updateState({ init: false, subject: null, cardId, channelId, topics: topics.current }); + events.current = [{ type: EVENT_OPEN, data: { cardId, channelId }}]; + updateState({ init: false, subject: null, cardId, channelId, topics: new Map() }); + updateConversation(); + + }, + addHistory: () => { + events.current.push({ type: EVENT_MORE }); updateConversation(); }, setChannelSubject: async (subject) => { - return await channel.actions.setChannelSubject(conversationId.current.channelId, subject); + return await channel.actions.setChannelSubject(channelView.current.channelId, subject); }, setChannelCard: async (cardId) => { - return await channel.actions.setChannelCard(conversationId.current.channelId, cardId); + return await channel.actions.setChannelCard(channelView.current.channelId, cardId); }, clearChannelCard: async (cardId) => { - return await channel.actions.clearChannelCard(conversationId.current.channelId, cardId); + return await channel.actions.clearChannelCard(channelView.current.channelId, cardId); }, getAssetUrl: (topicId, assetId) => { - const { cardId, channelId } = conversationId.current; - if (conversationId.current.cardId) { + const { cardId, channelId } = channelView.current; + if (channelView.current.cardId) { return card.actions.getContactChannelTopicAssetUrl(cardId, channelId, topicId, assetId); } else { @@ -239,7 +251,7 @@ export function useConversationContext() { } }, removeConversation: async () => { - const { cardId, channelId } = conversationId.current; + const { cardId, channelId } = channelView.current; if (cardId) { return await card.actions.removeChannel(cardId, channelId); } @@ -248,7 +260,7 @@ export function useConversationContext() { } }, removeTopic: async (topicId) => { - const { cardId, channelId } = conversationId.current; + const { cardId, channelId } = channelView.current; if (cardId) { return await card.actions.removeChannelTopic(cardId, channelId, topicId); } @@ -257,8 +269,7 @@ export function useConversationContext() { } }, setTopicSubject: async (topicId, data) => { -console.log("DATA:", data); - const { cardId, channelId } = conversationId.current; + const { cardId, channelId } = channelView.current; if (cardId) { return await card.actions.setChannelTopicSubject(cardId, channelId, topicId, data); }