From ba279dfcdae11bdc82914bb52c61b25ed8709269 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Wed, 25 Jan 2023 10:56:43 -0800 Subject: [PATCH] refactor of conversation in webapp --- net/web/src/context/cardUtil.js | 4 +- net/web/src/context/sealUtil.js | 2 +- net/web/src/context/useCardContext.hook.js | 3 +- net/web/src/context/useChannelContext.hook.js | 2 +- .../context/useConversationContext.hook.js | 5 +- .../src/session/conversation/Conversation.jsx | 6 +- .../conversation/addTopic/AddTopic.jsx | 15 +- .../conversation/addTopic/useAddTopic.hook.js | 67 +++--- .../conversation/topicItem/TopicItem.jsx | 145 ++----------- .../conversation/useConversation.hook.js | 193 ++++++++++++++---- 10 files changed, 223 insertions(+), 219 deletions(-) diff --git a/net/web/src/context/cardUtil.js b/net/web/src/context/cardUtil.js index bf226c90..a6182258 100644 --- a/net/web/src/context/cardUtil.js +++ b/net/web/src/context/cardUtil.js @@ -11,9 +11,9 @@ export function getCardByGuid(cards, guid) { export function getProfileByGuid(cards, guid) { const card = getCardByGuid(cards, guid); if (card?.data?.cardProfile) { - const { name, handle, imageSet } = card.data.cardProfile; + const { name, handle, imageSet, node } = card.data.cardProfile; const cardId = card.id; - return { cardId, name, handle, imageSet } + return { cardId, name, handle, imageSet, node } } return {}; } diff --git a/net/web/src/context/sealUtil.js b/net/web/src/context/sealUtil.js index a9624cfb..5e8c7af4 100644 --- a/net/web/src/context/sealUtil.js +++ b/net/web/src/context/sealUtil.js @@ -63,7 +63,7 @@ export function decryptChannelSubject(subject, contentKey) { export function encryptTopicSubject(subject, contentKey) { const iv = CryptoJS.lib.WordArray.random(128 / 8); const key = CryptoJS.enc.Hex.parse(contentKey); - const encrypted = CryptoJS.AES.encrypt(JSON.stringify({ subject }), key, { iv: iv }); + const encrypted = CryptoJS.AES.encrypt(JSON.stringify(subject), key, { iv: iv }); const messageEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64) const messageIv = iv.toString(); return { messageEncrypted, messageIv }; diff --git a/net/web/src/context/useCardContext.hook.js b/net/web/src/context/useCardContext.hook.js index f1c3305e..b7d995a6 100644 --- a/net/web/src/context/useCardContext.hook.js +++ b/net/web/src/context/useCardContext.hook.js @@ -185,6 +185,7 @@ export function useCardContext() { else { delta = await getContactChannels(node, token, setNotifiedView, setNotifiedChannel); } + for (let channel of delta) { if (channel.data) { let cur = card.channels.get(channel.id); @@ -299,7 +300,7 @@ export function useCardContext() { const subject = message([]); await addContactChannelTopic(node, token, channelId, type, subject, files); } - resyncCard(cardId); + //resyncCard(cardId); }, removeTopic: async (cardId, channelId, topicId) => { const card = cards.current.get(cardId); diff --git a/net/web/src/context/useChannelContext.hook.js b/net/web/src/context/useChannelContext.hook.js index 27e20d6f..ccb21d02 100644 --- a/net/web/src/context/useChannelContext.hook.js +++ b/net/web/src/context/useChannelContext.hook.js @@ -153,7 +153,7 @@ export function useChannelContext() { const subject = message([]); await addChannelTopic(access.current, channelId, type, subject); } - await resync(); + //await resync(); }, removeTopic: async (channelId, topicId) => { await removeChannelTopic(access.current, channelId, topicId); diff --git a/net/web/src/context/useConversationContext.hook.js b/net/web/src/context/useConversationContext.hook.js index d0c29c4d..16745346 100644 --- a/net/web/src/context/useConversationContext.hook.js +++ b/net/web/src/context/useConversationContext.hook.js @@ -212,7 +212,7 @@ export function useConversationContext() { delta = await getTopicDelta(cardId, channelId, null, COUNT, null, marker.current); } else { - delta = await getTopicDelta(cardId, channelId, topicRevision, null, marker.current, null); + delta = await getTopicDelta(cardId, channelId, setTopicRevision.current, null, marker.current, null); } for (let topic of delta?.topics) { @@ -240,8 +240,9 @@ export function useConversationContext() { } } - marker.current = delta.marker; + marker.current = delta.marker ? delta.marker : marker.current; setTopicRevision.current = topicRevision; + updateState({ offsync: false, topicRevision: topicRevision, topics: topics.current }); } } diff --git a/net/web/src/session/conversation/Conversation.jsx b/net/web/src/session/conversation/Conversation.jsx index 653ec1cd..71a1fcd6 100644 --- a/net/web/src/session/conversation/Conversation.jsx +++ b/net/web/src/session/conversation/Conversation.jsx @@ -14,7 +14,7 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId const thread = useRef(null); const topicRenderer = (topic) => { - return () + return () } // an unfortunate cludge for the mobile browser @@ -107,8 +107,8 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId )}
- { (!state.sealed || state.sealKey) && ( - + { (!state.sealed || state.contentKey) && ( + )} { state.uploadError && (
diff --git a/net/web/src/session/conversation/addTopic/AddTopic.jsx b/net/web/src/session/conversation/addTopic/AddTopic.jsx index 351fab38..1e87508e 100644 --- a/net/web/src/session/conversation/addTopic/AddTopic.jsx +++ b/net/web/src/session/conversation/addTopic/AddTopic.jsx @@ -8,10 +8,11 @@ import { AudioFile } from './audioFile/AudioFile'; import { VideoFile } from './videoFile/VideoFile'; import { Carousel } from 'carousel/Carousel'; -export function AddTopic({ cardId, channelId, sealed, sealKey }) { +export function AddTopic({ contentKey }) { + + const { state, actions } = useAddTopic(); const [modal, modalContext] = Modal.useModal(); - const { state, actions } = useAddTopic(cardId, channelId); const attachImage = useRef(null); const attachAudio = useRef(null); const attachVideo = useRef(null); @@ -27,7 +28,7 @@ export function AddTopic({ cardId, channelId, sealed, sealKey }) { const addTopic = async () => { if (state.messageText || state.assets.length) { try { - await actions.addTopic(sealed, sealKey); + await actions.addTopic(contentKey); } catch (err) { console.log(err); @@ -106,22 +107,22 @@ export function AddTopic({ cardId, channelId, sealed, sealKey }) { value={state.messageText} autocapitalize="none" />
- { !state.sealed && state.enableImage && ( + { !contentKey && state.enableImage && (
attachImage.current.click()}>
)} - { !state.sealed && state.enableVideo && ( + { !contentKey && state.enableVideo && (
attachVideo.current.click()}>
)} - { !state.sealed && state.enableAudio && ( + { !contentKey && state.enableAudio && (
attachAudio.current.click()}>
)} - { !state.sealed && ( + { !contentKey && (
)}
diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 35203e72..6bd9e15d 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -2,14 +2,14 @@ import { useContext, useState, useEffect } from 'react'; import { CardContext } from 'context/CardContext'; import { ChannelContext } from 'context/ChannelContext'; import { ConversationContext } from 'context/ConversationContext'; +import { encryptTopicSubject } from 'context/sealUtil'; -export function useAddTopic(cardId, channelId) { +export function useAddTopic() { const [state, setState] = useState({ enableImage: null, enableAudio: null, enableVideo: null, - sealed: false, assets: [], messageText: null, textColor: '#444444', @@ -50,9 +50,9 @@ export function useAddTopic(cardId, channelId) { } useEffect(() => { - const { enableImage, enableAudio, enableVideo, sealed } = conversation.state; - updateState({ enableImage, enableAudio, enableVideo, sealed }); - }, [conversation]); + const { enableImage, enableAudio, enableVideo } = conversation.state.channel?.data?.channelDetail || {}; + updateState({ enableImage, enableAudio, enableVideo }); + }, [conversation.state.channel?.data?.channelDetail]); const actions = { addImage: (image) => { @@ -73,7 +73,9 @@ export function useAddTopic(cardId, channelId) { setPosition: (index, position) => { updateAsset(index, { position }); }, - removeAsset: (idx) => { removeAsset(idx) }, + removeAsset: (idx) => { + removeAsset(idx) + }, setTextColor: (value) => { updateState({ textColorSet: true, textColor: value }); }, @@ -83,31 +85,42 @@ export function useAddTopic(cardId, channelId) { setTextSize: (value) => { updateState({ textSizeSet: true, textSize: value }); }, - addTopic: async (sealed, sealKey) => { + addTopic: async (contentKey) => { if (!state.busy) { try { updateState({ busy: true }); - let message = { - text: state.messageText, - textColor: state.textColorSet ? state.textColor : null, - textSize: state.textSizeSet ? state.textSize : null, + const type = contentKey ? 'sealedtopic' : 'superbasictopic'; + const message = (assets) => { + if (contentKey) { + if (assets?.length) { + console.log('assets not yet supported on sealed channels'); + } + const message = { + text: state.messageText, + textColor: state.textColorSet ? state.textColor : null, + textSize: state.textSizeSet ? state.textSize : null, + } + return encryptTopicSubject({ message }, contentKey); + } + else { + if (assets?.length) { + return { + assets, + text: state.messageText, + textColor: state.textColorSet ? state.textColor : null, + textSize: state.textSizeSet ? state.textSize : null, + } + } + else { + return { + text: state.messageText, + textColor: state.textColorSet ? state.textColor : null, + textSize: state.textSizeSet ? state.textSize : null, + } + } + } }; - if (cardId) { - if (sealed) { - await card.actions.addSealedChannelTopic(cardId, channelId, sealKey, message, state.assets); - } - else { - await card.actions.addChannelTopic(cardId, channelId, message, state.assets); - } - } - else { - if (sealed) { - await channel.actions.addSealedChannelTopic(channelId, sealKey, message, state.assets); - } - else { - await channel.actions.addChannelTopic(channelId, message, state.assets); - } - } + await conversation.actions.addTopic(type, message, state.assets); updateState({ busy: false, messageText: null, textColor: '#444444', textColorSet: false, textSize: 12, textSizeSet: false, assets: [] }); } diff --git a/net/web/src/session/conversation/topicItem/TopicItem.jsx b/net/web/src/session/conversation/topicItem/TopicItem.jsx index 64998d04..e1e9727b 100644 --- a/net/web/src/session/conversation/topicItem/TopicItem.jsx +++ b/net/web/src/session/conversation/topicItem/TopicItem.jsx @@ -8,141 +8,22 @@ import { Space, Skeleton, Button, Modal, Input } from 'antd'; import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined, PictureOutlined } from '@ant-design/icons'; import { Carousel } from 'carousel/Carousel'; -export function TopicItem({ host, topic, sealed, sealKey }) { - - const { state, actions } = useTopicItem(topic, sealed, sealKey); - - let name = state.name ? state.name : state.handle; - let nameClass = state.name ? 'set' : 'unset'; - if (name == null) { - name = "unknown contact" - nameClass = "unknown" - } - - const renderAsset = (asset, idx, topicId) => { - if (asset.image) { - return - } - if (asset.video) { - return - } - if (asset.audio) { - return - } - return <> - } - - const removeTopic = () => { - Modal.confirm({ - title: 'Do you want to delete this message?', - icon: , - okText: 'Yes, Delete', - cancelText: 'No, Cancel', - onOk() { actions.removeTopic() }, - }); - } - - const Options = () => { - if (state.editing) { - return <>; - } - if (state.owner) { - return ( -
-
actions.setEditing(true)}> - -
-
removeTopic()}> - -
-
- ); - } - if (host) { - return ( -
-
removeTopic()}> - -
-
- ); - } - return <>; - } - - const Message = () => { - if (state.editing) { - return ( -
- actions.setEdit(e.target.value)} rows={3} bordered={false}/> -
- - - - -
-
- ); - } - return
{ state.text }
- } +export function TopicItem({ host, topic }) { return ( - { state.init && ( - <> -
-
- -
-
-
{ name }
-
{ state.created }
-
- { !state.sealed && ( -
- -
- )} -
- { !state.confirmed && ( -
- -
- )} - { state.confirmed && ( -
- { state.error && ( -
- -
- )} - { !state.error && !state.ready && ( -
- -
- )} - { !state.error && state.ready && state.assets.length > 0 && ( -
- -
- )} -
- { !state.sealed && ( - - )} - { state.sealed && ( -
sealed message
- )} -
-
- )} - - )} +
+
+ +
+
+
{ topic.name }
+
{ topic.createdStr }
+
+
+
+
{ topic.text }
+
) } diff --git a/net/web/src/session/conversation/useConversation.hook.js b/net/web/src/session/conversation/useConversation.hook.js index 73482c14..f83a1367 100644 --- a/net/web/src/session/conversation/useConversation.hook.js +++ b/net/web/src/session/conversation/useConversation.hook.js @@ -1,35 +1,42 @@ -import { useContext, useState, useEffect } from 'react'; +import { useContext, useRef, useState, useEffect } from 'react'; import { ViewportContext } from 'context/ViewportContext'; import { AccountContext } from 'context/AccountContext'; import { ConversationContext } from 'context/ConversationContext'; import { UploadContext } from 'context/UploadContext'; import { StoreContext } from 'context/StoreContext'; +import { CardContext } from 'context/CardContext'; +import { ProfileContext } from 'context/ProfileContext'; +import { isUnsealed, getChannelSeals, getContentKey } from 'context/sealUtil'; import { JSEncrypt } from 'jsencrypt' +import { decryptTopicSubject } from 'context/sealUtil'; +import { getProfileByGuid } from 'context/cardUtil'; + export function useConversation(cardId, channelId) { const [state, setState] = useState({ display: null, - logo: null, - subject: null, - topics: [], - loadingInit: false, - loadingMore: false, upload: false, uploadError: false, uploadPercent: 0, - error: false, + topics: [], + loading: false, sealed: false, - sealKey: null, - delayed: false, + contentKey: null, }); + const profile = useContext(ProfileContext); + const card = useContext(CardContext); const account = useContext(AccountContext); const viewport = useContext(ViewportContext); const conversation = useContext(ConversationContext); const upload = useContext(UploadContext); const store = useContext(StoreContext); + const loading = useRef(false); + const conversationId = useRef(null); + const topics = useRef(new Map()); + const updateState = (value) => { setState((s) => ({ ...s, ...value })); } @@ -39,19 +46,28 @@ export function useConversation(cardId, channelId) { }, [viewport]); useEffect(() => { - let sealKey; - const seals = conversation.state.seals; - if (seals?.length > 0) { - seals.forEach(seal => { - if (seal.publicKey === account.state.sealKey?.public) { - let crypto = new JSEncrypt(); - crypto.setPrivateKey(account.state.sealKey.private); - sealKey = crypto.decrypt(seal.sealedKey); + const { dataType, data } = conversation.state.channel?.data?.channelDetail || {}; + if (dataType === 'sealed') { + try { + const { sealKey } = account.state; + const seals = getChannelSeals(data); + if (isUnsealed(seals, sealKey)) { + const contentKey = getContentKey(seals, sealKey); + updateState({ sealed: true, contentKey }); } - }); + else { + updateState({ sealed: true, contentKey: null }); + } + } + catch (err) { + console.log(err); + updateState({ sealed: true, contentKey: null }); + } } - updateState({ sealed: conversation.state.sealed, sealKey }); - }, [account.state.sealKey, conversation.state.seals, conversation.state.sealed]); + else { + updateState({ sealed: false, contentKey: null }); + } + }, [account.state.sealKey, conversation.state.channel?.data?.channelDetail]); useEffect(() => { let active = false; @@ -83,42 +99,133 @@ export function useConversation(cardId, channelId) { } updateState({ upload: active, uploadError, uploadPercent }); - }, [cardId, channelId, upload]); + }, [cardId, channelId, upload.state]); + + const setChannel = async () => { + if (!loading.current && conversationId.current) { + const { card, channel } = conversationId.current; + loading.current = true; + conversationId.current = null; + updateState({ loading: true }); + await conversation.setChannel(card, channel); + updateState({ loading: false }); + loading.current = false; + await setChannel(); + } + } useEffect(() => { - updateState({ delayed: false, topics: [] }); - setTimeout(() => { - updateState({ delayed: true }); - }, 250); conversation.actions.setChannel(cardId, channelId); // eslint-disable-next-line }, [cardId, channelId]); + const syncTopic = async (item, value) => { + const revision = value.data?.detailRevision; + const detail = value.data?.topicDetail || {}; + const identity = profile.state.identity || {}; + + item.create = detail.created; + const date = new Date(detail.created * 1000); + const now = new Date(); + const offset = now.getTime() - date.getTime(); + if(offset < 86400000) { + item.createdStr = date.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'}); + } + else if (offset < 31449600000) { + item.createdStr = date.toLocaleDateString("en-US", {day: 'numeric', month:'numeric'}); + } + else { + item.createdStr = date.toLocaleDateString("en-US"); + } + + if (detail.guid === identity.guid) { + item.creator = true; + item.imageUrl = profile.state.imageUrl; + if (identity.name) { + item.name = identity.name; + item.nameSet = true; + } + else { + item.name = `${identity.handle}@${identity.node}`; + item.nameSet = false; + } + } + else { + item.creator = false; + const contact = getProfileByGuid(card.state.cards, detail.guid); + if (contact) { + item.imageUrl = contact.imageSet ? card.actions.getCardImageUrl(contact.cardId) : null; + if (contact.name) { + item.name = contact.name; + item.nameSet = true; + } + else { + item.name = `${contact.handle}@${contact.node}`; + item.nameSet = false; + } + } + else { + item.imageUrl = null; + item.name = 'unknown'; + item.nameSet = false; + } + } + + if (detail.dataType === 'superbasictopic') { + if (item.revision !== revision) { + try { + const message = JSON.parse(detail.data); + item.text = message.text; + item.textColor = message.textColor ? message.textColor : '#444444'; + item.textSize = message.textSize ? message.textSize : 14; + } + catch (err) { + console.log(err); + } + } + } + if (detail.dataType === 'sealedtopic') { + if (item.revision !== revision || item.contentKey !== state.contentKey) { + item.contentKey = state.contentKey; + try { + const subject = decryptTopicSubject(detail.data, state.contentKey); + item.text = subject.message.text; + item.textColor = subject.message.textColor ? subject.message.textColor : '#444444'; + item.textSize = subject.message.textSize ? subject.message.textSize : 14; + } + catch (err) { + console.log(err); + } + } + } + item.revision = revision; + }; + useEffect(() => { - let topics = Array.from(conversation.state.topics.values()).sort((a, b) => { - const aTimestamp = a?.data?.topicDetail?.created; - const bTimestamp = b?.data?.topicDetail?.created; - if(aTimestamp === bTimestamp) { + const messages = new Map(); + conversation.state.topics.forEach((value, topicId) => { + let item = topics.current.get(topicId); + if (!item) { + item = { topicId }; + } + syncTopic(item, value); + messages.set(topicId, item); + }); + topics.current = messages; + + const sorted = Array.from(messages.values()).sort((a, b) => { + if(a.created === b.created) { return 0; } - if(aTimestamp == null || aTimestamp < bTimestamp) { + if(a.created == null || a.created < b.created) { return -1; } return 1; }); - if (topics.length) { - updateState({ delayed: false }); - } - else { - setTimeout(() => { - updateState({ delayed: true }); - }, 250); - } - const { error, loadingInit, loadingMore, subject, logoUrl, logoImg } = conversation.state; - updateState({ topics, error, loadingInit, loadingMore, subject, logoUrl, logoImg }); - store.actions.setValue(`${channelId}::${cardId}`, Number(conversation.state.topicRevision)); - // eslint-disable-next-line - }, [conversation]); + + updateState({ topics: sorted }); + // eslint-disable-next-line + }, [conversation.state, profile.state, card.state, state.contentKey]); const actions = { more: () => {