From 1875ca759d8907aa7389c5892a92430fd22f35a7 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Thu, 26 Jan 2023 16:01:35 -0800 Subject: [PATCH] more conversation refactor --- .../context/useConversationContext.hook.js | 10 +- .../profile/accountAccess/AccountAccess.jsx | 4 +- .../src/session/conversation/Conversation.jsx | 6 +- .../channelHeader/ChannelHeader.jsx | 48 +++++++ .../channelHeader/ChannelHeader.styled.js | 51 +++++++ .../channelHeader/useChannelHeader.hook.js | 131 ++++++++++++++++++ .../conversation/topicItem/TopicItem.jsx | 47 ++++++- .../topicItem/TopicItem.styled.js | 33 +++-- .../topicItem/useTopicItem.hook.js | 28 ++++ .../conversation/useConversation.hook.js | 72 +++++++--- 10 files changed, 380 insertions(+), 50 deletions(-) create mode 100644 net/web/src/session/conversation/channelHeader/ChannelHeader.jsx create mode 100644 net/web/src/session/conversation/channelHeader/ChannelHeader.styled.js create mode 100644 net/web/src/session/conversation/channelHeader/useChannelHeader.hook.js create mode 100644 net/web/src/session/conversation/topicItem/useTopicItem.hook.js diff --git a/net/web/src/context/useConversationContext.hook.js b/net/web/src/context/useConversationContext.hook.js index 64b45ad0..01d0806b 100644 --- a/net/web/src/context/useConversationContext.hook.js +++ b/net/web/src/context/useConversationContext.hook.js @@ -106,12 +106,12 @@ export function useConversationContext() { await resync(); }; - const setTopicSubject = async (cardId, channelId, type, subject) => { + const setTopicSubject = async (cardId, channelId, topicId, type, subject) => { if (cardId) { - await card.actions.setTopicSubject(cardId, channelId, type, subject); + await card.actions.setTopicSubject(cardId, channelId, topicId, type, subject); } else { - await channel.actions.setTopicSubject(channelId, type, subject); + await channel.actions.setTopicSubject(channelId, topicId, type, subject); } await resync(); }; @@ -308,9 +308,9 @@ export function useConversationContext() { const { cardId, channelId } = conversationId.current; await removeTopic(cardId, channelId, topicId); }, - setTopicSubject: async (type, subject) => { + setTopicSubject: async (topicId, type, subject) => { const { cardId, channelId } = conversationId.current; - await setTopicSubject(cardId, channelId, type, subject); + await setTopicSubject(cardId, channelId, topicId, type, subject); }, getTopicAssetUrl: (assetId, topicId) => { const { cardId, channelId } = conversationId.current; diff --git a/net/web/src/session/account/profile/accountAccess/AccountAccess.jsx b/net/web/src/session/account/profile/accountAccess/AccountAccess.jsx index 6c3b88ad..da45c038 100644 --- a/net/web/src/session/account/profile/accountAccess/AccountAccess.jsx +++ b/net/web/src/session/account/profile/accountAccess/AccountAccess.jsx @@ -91,7 +91,7 @@ export function AccountAccess() {
Change Login
- +
actions.enableSeal(enable)} /> @@ -130,7 +130,7 @@ export function AccountAccess() { + bodyStyle={{ paddingLeft: 16, paddingRight: 16 }} onCancel={actions.clearEditLogin}>
actions.setEditHandle(e.target.value)} diff --git a/net/web/src/session/conversation/Conversation.jsx b/net/web/src/session/conversation/Conversation.jsx index 84311394..db3db325 100644 --- a/net/web/src/session/conversation/Conversation.jsx +++ b/net/web/src/session/conversation/Conversation.jsx @@ -15,7 +15,11 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId const thread = useRef(null); const topicRenderer = (topic) => { - return ( actions.removeTopic(topic.id)}/>) + return ( actions.removeTopic(topic.id)} + update={(text) => actions.updateTopic(topic, text)} + sealed={state.sealed && !state.contentKey} + />) } // an unfortunate cludge for the mobile browser diff --git a/net/web/src/session/conversation/channelHeader/ChannelHeader.jsx b/net/web/src/session/conversation/channelHeader/ChannelHeader.jsx new file mode 100644 index 00000000..cdea22ec --- /dev/null +++ b/net/web/src/session/conversation/channelHeader/ChannelHeader.jsx @@ -0,0 +1,48 @@ +import { useChannelHeader } from './useChannelHeader.hook'; +import { ChannelHeaderWrapper, StatusError } from './ChannelHeader.styled'; +import { ExclamationCircleOutlined, SettingOutlined, CloseOutlined } from '@ant-design/icons'; +import { Tooltip } from 'antd'; +import { Logo } from 'logo/Logo'; + +export function ChannelHeader({ closeConversation, openDetails, contentKey }) { + + const { state, actions } = useChannelHeader(contentKey); + + return ( + +
+ + { state.title && ( +
{ state.title }
+ )} + { !state.title && ( +
{ state.label }
+ )} + { state.error && state.display === 'small' && ( + + + + )} + { state.error && state.display !== 'small' && ( + + + + + + )} + { state.display !== 'xlarge' && ( +
+ +
+ )} +
+ { state.display !== 'xlarge' && ( +
+ +
+ )} +
+ ); +} diff --git a/net/web/src/session/conversation/channelHeader/ChannelHeader.styled.js b/net/web/src/session/conversation/channelHeader/ChannelHeader.styled.js new file mode 100644 index 00000000..6dd01f79 --- /dev/null +++ b/net/web/src/session/conversation/channelHeader/ChannelHeader.styled.js @@ -0,0 +1,51 @@ +import styled from 'styled-components'; +import Colors from 'constants/Colors'; + +export const ChannelHeaderWrapper = styled.div` + margin-left: 16px; + margin-right: 16px; + height: 48px; + border-bottom: 1px solid ${Colors.profileDivider}; + display: flex; + flex-direction: row; + align-items: center; + flex-shrink: 0; + + .title { + font-size: 18px; + font-weight: bold; + flex-grow: 1; + padding-left: 16px; + display: flex; + flex-direction: row; + align-items: center; + min-width: 0; + + .label { + padding-left: 8px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + min-width: 0; + } + + .logo { + flex-shrink: 0; + } + } + + .button { + font-size: 18px; + color: ${Colors.grey}; + cursor: pointer; + padding-right: 16px; + padding-left: 16px; + } +` + +export const StatusError = styled.div` + color: ${Colors.error}; + font-size: 14px; + padding-left: 8px; + cursor: pointer; +` diff --git a/net/web/src/session/conversation/channelHeader/useChannelHeader.hook.js b/net/web/src/session/conversation/channelHeader/useChannelHeader.hook.js new file mode 100644 index 00000000..ef9e674b --- /dev/null +++ b/net/web/src/session/conversation/channelHeader/useChannelHeader.hook.js @@ -0,0 +1,131 @@ +import { useState, useContext, useEffect, useRef } from 'react'; +import { ViewportContext } from 'context/ViewportContext'; +import { ConversationContext } from 'context/ConversationContext'; +import { CardContext } from 'context/CardContext'; +import { ProfileContext } from 'context/ProfileContext'; +import { getCardByGuid } from 'context/cardUtil'; +import { decryptChannelSubject } from 'context/sealUtil'; + +export function useChannelHeader(contentKey) { + + const [state, setState] = useState({ + logoImg: null, + logoUrl: null, + label: null, + title: null, + offsync: false, + display: null, + }); + + const viewport = useContext(ViewportContext); + const card = useContext(CardContext); + const conversation = useContext(ConversationContext); + const profile = useContext(ProfileContext); + + const cardId = useRef(); + const channelId = useRef(); + const detailRevision = useRef(); + const key = useRef(); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + useEffect(() => { + updateState({ display: viewport.state.display }); + }, [viewport.state]); + + useEffect(() => { + + const cardValue = conversation.state.card; + const channelValue = conversation.state.channel; + + // extract member info + let memberCount = 0; + let names = []; + let img = null; + let logo = null; + if (cardValue) { + 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); + 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++; + } + } + } + + 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 !== contentKey) { + let title; + try { + const detail = channelValue?.data?.channelDetail; + if (detail?.dataType === 'sealed' && contentKey) { + const unsealed = decryptChannelSubject(detail.data, 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 = contentKey; + updateState({ title, label, img, logo }); + } + else { + updateState({ label, img, logo }); + } + }, [conversation.state, card.state, contentKey]); + + const actions = { + resync: () => { + }, + }; + + return { state, actions }; +} diff --git a/net/web/src/session/conversation/topicItem/TopicItem.jsx b/net/web/src/session/conversation/topicItem/TopicItem.jsx index f40fce62..df85f839 100644 --- a/net/web/src/session/conversation/topicItem/TopicItem.jsx +++ b/net/web/src/session/conversation/topicItem/TopicItem.jsx @@ -6,10 +6,12 @@ import { Logo } from 'logo/Logo'; import { Space, Skeleton, Button, Modal, Input } from 'antd'; import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined, PictureOutlined } from '@ant-design/icons'; import { Carousel } from 'carousel/Carousel'; +import { useTopicItem } from './useTopicItem.hook'; -export function TopicItem({ host, topic, remove }) { +export function TopicItem({ host, sealed, topic, update, remove }) { const [ modal, modalContext ] = Modal.useModal(); + const { state, actions } = useTopicItem(); const removeTopic = () => { modal.confirm({ @@ -34,6 +36,21 @@ export function TopicItem({ host, topic, remove }) { }); } + const updateTopic = async () => { + try { + await update(state.message); + actions.clearEditing(); + } + catch(err) { + console.log(err); + modal.error({ + title: 'Failed to Update Message', + content: 'Please try again.', + bodyStyle: { padding: 16 }, + }); + } + }; + const renderAsset = (asset, idx) => { if (asset.image) { return
- { topic.creator && ( -
console.log('edit')}> + { !sealed && topic.creator && ( +
actions.setEditing(topic.text)}>
)} @@ -101,9 +118,27 @@ export function TopicItem({ host, topic, remove }) { )} )} -
-
{ topic.text }
-
+ { sealed && ( +
sealed message
+ )} + { !sealed && !state.editing && ( +
+
{ topic.text }
+
+ )} + { state.editing && ( +
+ actions.setMessage(e.target.value)} rows={3} bordered={false}/> +
+ + + + +
+
+ )} )} diff --git a/net/web/src/session/conversation/topicItem/TopicItem.styled.js b/net/web/src/session/conversation/topicItem/TopicItem.styled.js index 99c0da3d..3b50a130 100644 --- a/net/web/src/session/conversation/topicItem/TopicItem.styled.js +++ b/net/web/src/session/conversation/topicItem/TopicItem.styled.js @@ -88,6 +88,7 @@ export const TopicItemWrapper = styled.div` .sealed-message { font-style: italic; color: #aaaaaa; + padding-left: 72px; } .asset-placeholder { @@ -103,6 +104,7 @@ export const TopicItemWrapper = styled.div` .topic-assets { padding-top: 4px; + padding-bottom: 4px; } .skeleton { @@ -116,23 +118,24 @@ export const TopicItemWrapper = styled.div` padding-left: 72px; white-space: pre-line; min-height: 4px; + } - .editing { + .editing { + display: flex; + flex-direction: column; + border-radius: 4px; + border: 1px solid #aaaaaa; + margin-top: 8px; + margin-bottom: 8px; + margin-right: 16px; + margin-left: 72px; + + .controls { display: flex; - flex-direction: column; - border-radius: 4px; - border: 1px solid #aaaaaa; - width: 100%; - margin-top: 8px; - margin-bottom: 8px; - - .controls { - display: flex; - flex-direction: row; - justify-content: flex-end; - padding-bottom: 8px; - padding-right: 8px; - } + flex-direction: row; + justify-content: flex-end; + padding-bottom: 8px; + padding-right: 8px; } } `; diff --git a/net/web/src/session/conversation/topicItem/useTopicItem.hook.js b/net/web/src/session/conversation/topicItem/useTopicItem.hook.js new file mode 100644 index 00000000..9cb1d2fd --- /dev/null +++ b/net/web/src/session/conversation/topicItem/useTopicItem.hook.js @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; + +export function useTopicItem() { + + const [state, setState] = useState({ + editing: false, + message: null, + }); + + const updateState = (value) => { + setState((s) => ({ ...s, ...value })); + } + + const actions = { + setEditing: (message) => { + updateState({ editing: true, message }); + }, + clearEditing: () => { + updateState({ editing: false }); + }, + setMessage: (message) => { + updateState({ message }); + }, + }; + + return { state, actions }; +} + diff --git a/net/web/src/session/conversation/useConversation.hook.js b/net/web/src/session/conversation/useConversation.hook.js index e895240c..491a8055 100644 --- a/net/web/src/session/conversation/useConversation.hook.js +++ b/net/web/src/session/conversation/useConversation.hook.js @@ -6,7 +6,7 @@ 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 { isUnsealed, getChannelSeals, getContentKey, encryptTopicSubject } from 'context/sealUtil'; import { JSEncrypt } from 'jsencrypt' import { decryptTopicSubject } from 'context/sealUtil'; @@ -23,6 +23,7 @@ export function useConversation(cardId, channelId) { loading: false, sealed: false, contentKey: null, + busy: false, }); const profile = useContext(ProfileContext); @@ -120,7 +121,17 @@ export function useConversation(cardId, channelId) { // eslint-disable-next-line }, [cardId, channelId]); - const syncTopic = async (item, value) => { + useEffect(() => { + syncChannel(); + // eslint-disable-next-line + }, [conversation.state, profile.state, card.state]); + + useEffect(() => { + topics.current = new Map(); + syncChannel(); + }, [state.contentKey]); + + const syncTopic = (item, value) => { const revision = value.data?.detailRevision; const detail = value.data?.topicDetail || {}; const identity = profile.state.identity || {}; @@ -172,42 +183,34 @@ export function useConversation(cardId, channelId) { } } - if (detail.dataType === 'superbasictopic') { - if (item.revision !== revision) { - try { + if (item.revision !== revision) { + try { + if (detail.dataType === 'superbasictopic') { const message = JSON.parse(detail.data); item.assets = message.assets; 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 { + if (detail.dataType === 'sealedtopic') { const subject = decryptTopicSubject(detail.data, state.contentKey); item.assets = subject.message.assets; 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); - } } + catch (err) { + console.log(err); + } + item.revision = revision; } item.transform = detail.transform; item.status = detail.status; item.assetUrl = conversation.actions.getTopicAssetUrl; - item.revision = revision; }; - useEffect(() => { + const syncChannel = () => { const messages = new Map(); conversation.state.topics.forEach((value, id) => { const curCardId = conversation.state.card?.id; @@ -233,8 +236,7 @@ export function useConversation(cardId, channelId) { }); updateState({ topics: sorted }); - // eslint-disable-next-line - }, [conversation.state, profile.state, card.state, state.contentKey]); + } const actions = { more: () => { @@ -251,6 +253,34 @@ export function useConversation(cardId, channelId) { removeTopic: async (topicId) => { await conversation.actions.removeTopic(topicId); }, + updateTopic: async (topic, text) => { + const { assets, textSize, textColor } = topic; + const message = { text, textSize, textColor, assets }; + console.log("UPDATE", message); + + if (!state.busy) { + updateState({ busy: true }); + try { + if (state.sealed) { + if (state.contentKey) { + const subject = encryptTopicSubject({ message }, state.contentKey); + await conversation.actions.setTopicSubject(topic.id, 'sealedtopic', subject); + } + } + else { + await conversation.actions.setTopicSubject(topic.id, 'superbasictopic', message); + } + updateState({ busy: false }); + } + catch(err) { + updateState({ busy: false }); + throw new Error("topic update failed"); + } + } + else { + throw new Error("operation in progress"); + } + }, }; return { state, actions };