From 9fbfe85d60b9cfe5a3dda1d63a0a359e1c660331 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Fri, 27 Jan 2023 14:51:24 -0800 Subject: [PATCH] refactor of conversation detail in webapp --- net/web/src/context/sealUtil.js | 10 + net/web/src/session/cardSelect/CardSelect.jsx | 4 +- .../cardSelect/selectItem/SelectItem.jsx | 10 +- .../src/session/channels/useChannels.hook.js | 2 +- net/web/src/session/details/Details.jsx | 76 +++-- .../details/editMembers/EditMembers.jsx | 7 +- .../details/editSubject/EditSubject.jsx | 4 +- .../src/session/details/useDetails.hook.js | 314 ++++++++++-------- 8 files changed, 264 insertions(+), 163 deletions(-) diff --git a/net/web/src/context/sealUtil.js b/net/web/src/context/sealUtil.js index 5e8c7af4..32f83157 100644 --- a/net/web/src/context/sealUtil.js +++ b/net/web/src/context/sealUtil.js @@ -46,6 +46,16 @@ export function encryptChannelSubject(subject, publicKeys) { return { subjectEncrypted, subjectIv, seals }; } +export function updateChannelSubject(subject, contentKey) { + const key = CryptoJS.enc.Hex.parse(contentKey); + const iv = CryptoJS.lib.WordArray.random(128 / 8); + const encrypted = CryptoJS.AES.encrypt(JSON.stringify({ subject }), key, { iv: iv }); + const subjectEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64) + const subjectIv = iv.toString(); + + return { subjectEncrypted, subjectIv }; +} + export function decryptChannelSubject(subject, contentKey) { const { subjectEncrypted, subjectIv } = JSON.parse(subject); const iv = CryptoJS.enc.Hex.parse(subjectIv); diff --git a/net/web/src/session/cardSelect/CardSelect.jsx b/net/web/src/session/cardSelect/CardSelect.jsx index 053294a9..a9516214 100644 --- a/net/web/src/session/cardSelect/CardSelect.jsx +++ b/net/web/src/session/cardSelect/CardSelect.jsx @@ -4,7 +4,7 @@ import { SelectItem } from './selectItem/SelectItem'; import { useCardSelect } from './useCardSelect.hook'; import { Logo } from 'logo/Logo'; -export function CardSelect({ filter, unknown, select, selected, markup, emptyMessage }) { +export function CardSelect({ filter, unknown, select, selected, markup, emptyMessage, setItem, clearItem }) { const { state } = useCardSelect(filter); @@ -13,7 +13,7 @@ export function CardSelect({ filter, unknown, select, selected, markup, emptyMes { state.cards?.length > 0 && ( ( - + )} /> )} diff --git a/net/web/src/session/cardSelect/selectItem/SelectItem.jsx b/net/web/src/session/cardSelect/selectItem/SelectItem.jsx index bf8f1f4b..17b9e13a 100644 --- a/net/web/src/session/cardSelect/selectItem/SelectItem.jsx +++ b/net/web/src/session/cardSelect/selectItem/SelectItem.jsx @@ -3,7 +3,7 @@ import { SelectItemWrapper, Markup } from './SelectItem.styled'; import { useSelectItem } from './useSelectItem.hook'; import { Logo } from 'logo/Logo'; -export function SelectItem({ item, select, selected, markup }) { +export function SelectItem({ item, select, selected, markup, setItem, clearItem }) { const { state } = useSelectItem(item, selected, markup); const profile = item?.data?.cardProfile; @@ -19,6 +19,12 @@ export function SelectItem({ item, select, selected, markup }) { if (select) { select(item.id); } + if (setItem && !state.selected) { + setItem(item.id); + } + if (clearItem && state.selected) { + clearItem(item.id); + } } return ( @@ -29,7 +35,7 @@ export function SelectItem({ item, select, selected, markup }) {
{ profile?.name }
{ handle() }
- { select && ( + { (select || setItem || clearItem) && (
diff --git a/net/web/src/session/channels/useChannels.hook.js b/net/web/src/session/channels/useChannels.hook.js index 12269421..75959b4d 100644 --- a/net/web/src/session/channels/useChannels.hook.js +++ b/net/web/src/session/channels/useChannels.hook.js @@ -132,7 +132,7 @@ export function useChannels() { item.sealKey = sealKey; } - if (item.title == null || typeof item.title !== 'string') { + if (item.title == null || item.title === '' || typeof item.title !== 'string') { item.subject = item.label; } else { diff --git a/net/web/src/session/details/Details.jsx b/net/web/src/session/details/Details.jsx index 617e9a5c..53822228 100644 --- a/net/web/src/session/details/Details.jsx +++ b/net/web/src/session/details/Details.jsx @@ -9,10 +9,38 @@ import { EditSubject } from './editSubject/EditSubject'; import { EditMembers } from './editMembers/EditMembers'; import { UnlockOutlined, LockFilled } from '@ant-design/icons'; -export function Details({ cardId, channelId, closeDetails, closeConversation, openContact }) { +export function Details({ closeDetails, closeConversation, openContact }) { const [modal, modalContext] = Modal.useModal(); - const { state, actions } = useDetails(cardId, channelId); + const { state, actions } = useDetails(); + + const setMember = async (id) => { + try { + await actions.setMember(id); + } + catch(err) { + console.log(err); + modal.error({ + title: 'Failed to Set Conversation Member', + content: 'Please try again.', + bodyStyle: { padding: 16 }, + }); + } + } + + const clearMember = async (id) => { + try { + await actions.clearMember(id); + } + catch(err) { + console.log(err); + modal.error({ + title: 'Failed to Clear Conversation Member', + content: 'Please try again.', + bodyStyle: { padding: 16 }, + }); + } + } const deleteChannel = async () => { modal.confirm({ @@ -30,7 +58,7 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op const applyDeleteChannel = async () => { try { - await actions.deleteChannel(); + await actions.removeChannel(); closeConversation(); } catch(err) { @@ -58,7 +86,7 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op const applyLeaveChannel = async () => { try { - await actions.leaveChannel(); + await actions.removeChannel(); closeConversation(); } catch(err) { @@ -121,19 +149,24 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op
{ state.host && (
- { state.locked && !state.unlocked && ( + { state.sealed && !state.contentKey && ( )} - { state.locked && state.unlocked && ( + { state.sealed && state.contentKey && ( )} - { state.subject } - { state.editable && ( + { state.title && ( + { state.title } + )} + { !state.title && ( + { state.label } + )} + { (!state.sealed || state.contentKey) && ( @@ -142,13 +175,18 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op )} { !state.host && (
- { state.locked && !state.unlocked && ( + { state.sealed && !state.contentKey && ( )} - { state.locked && state.unlocked && ( + { state.sealed && state.contentKey && ( )} - { state.subject } + { state.title && ( + { state.title } + )} + { !state.title && ( + { state.label } + )}
)} { state.host && ( @@ -163,7 +201,7 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op { state.host && (
Delete Topic
)} - { state.host && !state.locked && ( + { state.host && !state.sealed && (
Edit Membership
)} { !state.host && ( @@ -172,21 +210,21 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op
Members
{ - if(state.contacts.includes(item.id)) { + if(state.members.includes(item.id)) { return true; } return false; }} unknown={state.unknown} - markup={cardId} /> + />
- - + - - + ); diff --git a/net/web/src/session/details/editMembers/EditMembers.jsx b/net/web/src/session/details/editMembers/EditMembers.jsx index d552d9b1..7a4cf125 100644 --- a/net/web/src/session/details/editMembers/EditMembers.jsx +++ b/net/web/src/session/details/editMembers/EditMembers.jsx @@ -1,14 +1,15 @@ import { EditMembersWrapper } from './EditMembers.styled'; import { CardSelect } from '../../cardSelect/CardSelect'; -export function EditMembers({ state, actions }) { +export function EditMembers({ members, setMember, clearMember }) { return (
card?.data?.cardDetail?.status === 'connected'} />
diff --git a/net/web/src/session/details/editSubject/EditSubject.jsx b/net/web/src/session/details/editSubject/EditSubject.jsx index fc3a00fb..8befcf04 100644 --- a/net/web/src/session/details/editSubject/EditSubject.jsx +++ b/net/web/src/session/details/editSubject/EditSubject.jsx @@ -1,12 +1,12 @@ import { Input } from 'antd'; import { EditSubjectWrapper } from './EditSubject.styled'; -export function EditSubject({ state, actions }) { +export function EditSubject({ subject, setSubject }) { return ( actions.setSubjectUpdate(e.target.value)} /> + value={subject} onChange={(e) => setSubject(e.target.value)} /> ); } diff --git a/net/web/src/session/details/useDetails.hook.js b/net/web/src/session/details/useDetails.hook.js index fdf9d86a..87673599 100644 --- a/net/web/src/session/details/useDetails.hook.js +++ b/net/web/src/session/details/useDetails.hook.js @@ -1,190 +1,236 @@ -import { useContext, useState, useEffect } from 'react'; +import { useContext, useState, useEffect, useRef } from 'react'; import { CardContext } from 'context/CardContext'; -import { ChannelContext } from 'context/ChannelContext'; +import { ConversationContext } from 'context/ConversationContext'; import { AccountContext } from 'context/AccountContext'; +import { ProfileContext } from 'context/ProfileContext'; import { ViewportContext } from 'context/ViewportContext'; +import { getCardByGuid } from 'context/cardUtil'; +import { decryptChannelSubject, updateChannelSubject, getContentKey, getChannelSeals, isUnsealed } from 'context/sealUtil'; -export function useDetails(cardId, channelId) { +export function useDetails() { const [state, setState] = useState({ logo: null, img: null, - subject: null, - server: null, started: null, - host: null, - contacts: [], - members: new Set(), - editSubject: false, - editMembers: false, - busy: false, - subjectUpdate: null, + host: false, + title: null, + label: null, + members: [], unknown: 0, + + showEditMembers: false, + editMembers: new Set(), + + showEditSubject: false, + editSubject: null, + display: null, - locked: false, - unlocked: false, - editable: false, + sealed: false, + contentKey: null, + seals: null, }); + const conversation = useContext(ConversationContext); const card = useContext(CardContext); const account = useContext(AccountContext); - const channel = useContext(ChannelContext); - const viewport = useContext(ViewportContext); + const viewport = useContext(ViewportContext); + const profile = useContext(ProfileContext); + + const cardId = useRef(); + const channelId = useRef(); + const key = useRef(); + const detailRevision = useRef(); const updateState = (value) => { setState((s) => ({ ...s, ...value })); } + useEffect(() => { + 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 decKey = getContentKey(seals, sealKey); + updateState({ sealed: true, contentKey: decKey, seals }); + } + else { + updateState({ sealed: true, contentKey: null }); + } + } + catch (err) { + console.log(err); + updateState({ sealed: true, contentKey: null }); + } + } + else { + updateState({ sealed: false, contentKey: null }); + } + // eslint-disable-next-line + }, [account.state.sealKey, conversation.state.channel?.data?.channelDetail]); + useEffect(() => { updateState({ display: viewport.state.display }); }, [viewport]); useEffect(() => { - let img, subject, subjectUpdate, host, started, contacts, locked, unlocked, editable; - let chan; - if (cardId) { - const cardChan = card.state.cards.get(cardId); - if (cardChan) { - chan = cardChan.channels.get(channelId); - } + + const cardValue = conversation.state.card; + const channelValue = conversation.state.channel; + + // extract channel created info + let started; + let host; + const date = new Date(channelValue?.data?.channelDetail?.created * 1000); + const now = new Date(); + if(now.getTime() - date.getTime() < 86400000) { + started = date.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'}); } else { - chan = channel.state.channels.get(channelId); + started = date.toLocaleDateString("en-US"); } - - if (chan) { - if (chan.contacts?.length === 0) { - img = 'solution'; - subject = 'Notes'; - } - else if (chan.contacts?.length > 1) { - img = 'appstore' - subject = 'Group'; - } - else { - img = 'team'; - subject = 'Conversation' - } - if (chan.data.channelDetail.dataType === 'sealed') { - editable = false; - try { - const seals = JSON.parse(chan.data.channelDetail.data).seals; - seals.forEach(seal => { - if (account.state.sealKey?.public === seal.publicKey) { - editable = true; - } - }); - } - catch (err) { - console.log(err); - } - locked = true; - unlocked = chan.data.unsealedChannel != null; - if (chan.data.unsealedChannel?.subject) { - subject = chan.data.unsealedChannel.subject; - subjectUpdate = subject; - } - } - else { - locked = false; - editable = (chan.cardId == null); - try { - const parsed = JSON.parse(chan.data.channelDetail.data); - if (parsed.subject) { - subject = parsed.subject; - subjectUpdate = subject; - } - } - catch(err) { - console.log(err); - } - } - const date = new Date(chan.data.channelDetail.created * 1000); - const now = new Date(); - if(now.getTime() - date.getTime() < 86400000) { - started = date.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'}); - } - else { - started = date.toLocaleDateString("en-US"); - } - if (chan.cardId) { - host = false; - } - else { - host = true; - } - } - - if (chan?.contacts ) { - contacts = chan.contacts.map((contact) => contact?.id); + if (cardValue) { + host = false; } else { - contacts = []; + host = true; } - - let members = new Set(contacts); + + // extract member info + let memberCount = 0; + let names = []; + let img; + let logo; + let members = []; let unknown = 0; - contacts.forEach(id => { - if (id == null) { - unknown++; + if (cardValue) { + members.push(cardValue.id); + const profile = cardValue.data?.cardProfile; + if (profile?.name) { + names.push(profile.name); } - }); + if (profile?.imageSet) { + img = null; + logo = card.actions.getCardImageUrl(cardValue.id); + } + else { + img = 'avatar'; + logo = null; + } + memberCount++; + } + if (channelValue?.data?.channelDetail?.members) { + for (let guid of channelValue.data.channelDetail.members) { + if (guid !== profile.state.identity.guid) { + const contact = getCardByGuid(card.state.cards, guid); + if (contact) { + members.push(contact.id); + } + else { + unknown++; + } + + const profile = contact?.data?.cardProfile; + if (profile?.name) { + names.push(profile.name); + } + if (profile?.imageSet) { + img = null; + logo = card.actions.getCardImageUrl(contact.id); + } + else { + img = 'avatar'; + logo = null; + } + memberCount++; + } + } + } - updateState({ img, subject, host, started, contacts, members, unknown, subjectUpdate, locked, unlocked, editable }); - }, [cardId, channelId, card, channel, account]); + let label; + if (memberCount === 0) { + img = 'solution'; + label = 'Notes'; + } + else if (memberCount === 1) { + label = names.join(','); + } + else { + img = 'appstore'; + label = names.join(','); + } + + if (cardId.current !== cardValue?.id || channelId.current !== channelValue?.id || + detailRevision.current !== channelValue?.data?.detailRevision || key.current !== state.contentKey) { + let title; + try { + const detail = channelValue?.data?.channelDetail; + if (detail?.dataType === 'sealed' && state.contentKey) { + const unsealed = decryptChannelSubject(detail.data, state.contentKey); + title = unsealed.subject; + } + else if (detail?.dataType === 'superbasic') { + const data = JSON.parse(detail.data); + title = data.subject; + } + } + catch(err) { + console.log(err); + } + cardId.current = cardValue?.id; + channelId.current = channelValue?.id; + detailRevision.current = channelValue?.data?.detailRevision; + key.current = state.contentKey; + updateState({ started, host, title, label, img, logo, unknown, members, + editSubject: title, editMembers: new Set(members) }); + } + else { + updateState({ started, host, label, img, logo, unknown, members, + editMembers: new Set(members) }); + } + // eslint-disable-next-line + }, [conversation.state, card.state, state.contentKey]); const actions = { setEditSubject: () => { - updateState({ editSubject: true }); + updateState({ showEditSubject: true }); }, clearEditSubject: () => { - updateState({ editSubject: false }); + updateState({ showEditSubject: false }); }, - setSubjectUpdate: (subjectUpdate) => { - updateState({ subjectUpdate }); + setSubjectUpdate: (editSubject) => { + updateState({ editSubject }); }, setSubject: async () => { - if (!state.busy) { - try { - updateState({ busy: true }); - if (state.locked) { - channel.actions.setChannelSealedSubject(channelId, state.subjectUpdate, account.state.sealKey); - } - else { - channel.actions.setChannelSubject(channelId, state.subjectUpdate); - } - updateState({ busy: false }); - } - catch(err) { - console.log(err); - updateState({ busy: false }); - throw new Error("set channel subject failed"); + if (state.sealed) { + if (state.contentKey) { + const updated = updateChannelSubject(state.editSubject, state.contentKey); + updated.seals = state.seals; + await conversation.actions.setChannelSubject('sealed', updated); } } else { - throw new Error('operation in progress'); + const subject = { subject: state.editSubject }; + await conversation.actions.setChannelSubject('superbasic', subject); } }, setEditMembers: () => { - updateState({ editMembers: true }); + updateState({ editMembers: new Set(state.members), showEditMembers: true }); }, clearEditMembers: () => { - updateState({ editMembers: false }); + updateState({ showEditMembers: false }); }, - onMember: async (card) => { - if (state.members.has(card)) { - channel.actions.clearChannelCard(channelId, card); - } - else { - channel.actions.setChannelCard(channelId, card); - } + setMember: async (id) => { + await conversation.actions.setChannelCard(id); }, - deleteChannel: async () => { - await channel.actions.removeChannel(channelId); + clearMember: async (id) => { + await conversation.actions.clearChannelCard(id); + }, + removeChannel: async () => { + await conversation.actions.removeChannel(); }, - leaveChannel: async () => { - await card.actions.removeChannel(cardId, channelId); - } }; return { state, actions };