more conversation refactor

This commit is contained in:
Roland Osborne 2023-01-26 16:01:35 -08:00
parent 86d0e31fa7
commit 1875ca759d
10 changed files with 380 additions and 50 deletions

View File

@ -106,12 +106,12 @@ export function useConversationContext() {
await resync(); await resync();
}; };
const setTopicSubject = async (cardId, channelId, type, subject) => { const setTopicSubject = async (cardId, channelId, topicId, type, subject) => {
if (cardId) { if (cardId) {
await card.actions.setTopicSubject(cardId, channelId, type, subject); await card.actions.setTopicSubject(cardId, channelId, topicId, type, subject);
} }
else { else {
await channel.actions.setTopicSubject(channelId, type, subject); await channel.actions.setTopicSubject(channelId, topicId, type, subject);
} }
await resync(); await resync();
}; };
@ -308,9 +308,9 @@ export function useConversationContext() {
const { cardId, channelId } = conversationId.current; const { cardId, channelId } = conversationId.current;
await removeTopic(cardId, channelId, topicId); await removeTopic(cardId, channelId, topicId);
}, },
setTopicSubject: async (type, subject) => { setTopicSubject: async (topicId, type, subject) => {
const { cardId, channelId } = conversationId.current; const { cardId, channelId } = conversationId.current;
await setTopicSubject(cardId, channelId, type, subject); await setTopicSubject(cardId, channelId, topicId, type, subject);
}, },
getTopicAssetUrl: (assetId, topicId) => { getTopicAssetUrl: (assetId, topicId) => {
const { cardId, channelId } = conversationId.current; const { cardId, channelId } = conversationId.current;

View File

@ -91,7 +91,7 @@ export function AccountAccess() {
<LockOutlined /> <LockOutlined />
<div className="label">Change Login</div> <div className="label">Change Login</div>
</div> </div>
<Modal title="Topic Sealing Key" centered visible={state.editSeal} footer={editSealFooter} onCancel={actions.clearEditSeal}> <Modal title="Topic Sealing Key" centered visible={state.editSeal} footer={editSealFooter} onCancel={actions.clearEditSeal} bodyStyle={{ padding: 16 }}>
<SealModal> <SealModal>
<div className="switch"> <div className="switch">
<Switch size="small" checked={state.sealEnabled} onChange={enable => actions.enableSeal(enable)} /> <Switch size="small" checked={state.sealEnabled} onChange={enable => actions.enableSeal(enable)} />
@ -130,7 +130,7 @@ export function AccountAccess() {
</SealModal> </SealModal>
</Modal> </Modal>
<Modal title="Account Login" centered visible={state.editLogin} footer={editLoginFooter} <Modal title="Account Login" centered visible={state.editLogin} footer={editLoginFooter}
onCancel={actions.clearEditLogin}> bodyStyle={{ paddingLeft: 16, paddingRight: 16 }} onCancel={actions.clearEditLogin}>
<Form name="basic" wrapperCol={{ span: 24, }}> <Form name="basic" wrapperCol={{ span: 24, }}>
<Form.Item name="username" validateStatus={state.editStatus} help={state.editMessage}> <Form.Item name="username" validateStatus={state.editStatus} help={state.editMessage}>
<Input placeholder="Username" spellCheck="false" onChange={(e) => actions.setEditHandle(e.target.value)} <Input placeholder="Username" spellCheck="false" onChange={(e) => actions.setEditHandle(e.target.value)}

View File

@ -15,7 +15,11 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
const thread = useRef(null); const thread = useRef(null);
const topicRenderer = (topic) => { const topicRenderer = (topic) => {
return (<TopicItem host={cardId == null} topic={topic} remove={() => actions.removeTopic(topic.id)}/>) return (<TopicItem host={cardId == null} topic={topic}
remove={() => actions.removeTopic(topic.id)}
update={(text) => actions.updateTopic(topic, text)}
sealed={state.sealed && !state.contentKey}
/>)
} }
// an unfortunate cludge for the mobile browser // an unfortunate cludge for the mobile browser

View File

@ -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 (
<ChannelHeaderWrapper>
<div class="title">
<div class="logo">
<Logo img={state.img} url={state.logo} width={32} height={32} radius={4} />
</div>
{ state.title && (
<div class="label">{ state.title }</div>
)}
{ !state.title && (
<div class="label">{ state.label }</div>
)}
{ state.error && state.display === 'small' && (
<StatusError onClick={actions.resync}>
<ExclamationCircleOutlined />
</StatusError>
)}
{ state.error && state.display !== 'small' && (
<Tooltip placement="bottom" title="sync error">
<StatusError onClick={actions.resync}>
<ExclamationCircleOutlined />
</StatusError>
</Tooltip>
)}
{ state.display !== 'xlarge' && (
<div class="button" onClick={openDetails}>
<SettingOutlined />
</div>
)}
</div>
{ state.display !== 'xlarge' && (
<div class="button" onClick={closeConversation}>
<CloseOutlined />
</div>
)}
</ChannelHeaderWrapper>
);
}

View File

@ -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;
`

View File

@ -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 };
}

View File

@ -6,10 +6,12 @@ import { Logo } from 'logo/Logo';
import { Space, Skeleton, Button, Modal, Input } from 'antd'; import { Space, Skeleton, Button, Modal, Input } from 'antd';
import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined, PictureOutlined } from '@ant-design/icons'; import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined, PictureOutlined } from '@ant-design/icons';
import { Carousel } from 'carousel/Carousel'; 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 [ modal, modalContext ] = Modal.useModal();
const { state, actions } = useTopicItem();
const removeTopic = () => { const removeTopic = () => {
modal.confirm({ 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) => { const renderAsset = (asset, idx) => {
if (asset.image) { if (asset.image) {
return <ImageAsset thumbUrl={topic.assetUrl(asset.image.thumb, topic.id)} return <ImageAsset thumbUrl={topic.assetUrl(asset.image.thumb, topic.id)}
@ -62,8 +79,8 @@ export function TopicItem({ host, topic, remove }) {
</div> </div>
<div class="topic-options"> <div class="topic-options">
<div class="buttons"> <div class="buttons">
{ topic.creator && ( { !sealed && topic.creator && (
<div class="button edit" onClick={() => console.log('edit')}> <div class="button edit" onClick={() => actions.setEditing(topic.text)}>
<EditOutlined /> <EditOutlined />
</div> </div>
)} )}
@ -101,9 +118,27 @@ export function TopicItem({ host, topic, remove }) {
)} )}
</> </>
)} )}
<div class="message"> { sealed && (
<div style={{ color: topic.textColor, fontSize: topic.textSize }}>{ topic.text }</div> <div class="sealed-message">sealed message</div>
</div> )}
{ !sealed && !state.editing && (
<div class="message">
<div style={{ color: topic.textColor, fontSize: topic.textSize }}>{ topic.text }</div>
</div>
)}
{ state.editing && (
<div class="editing">
<Input.TextArea defaultValue={state.message} placeholder="message"
style={{ resize: 'none', color: state.textColor, fontSize: state.textSize }}
onChange={(e) => actions.setMessage(e.target.value)} rows={3} bordered={false}/>
<div class="controls">
<Space>
<Button onClick={actions.clearEditing}>Cancel</Button>
<Button type="primary" onClick={updateTopic}>Save</Button>
</Space>
</div>
</div>
)}
</> </>
)} )}
</TopicItemWrapper> </TopicItemWrapper>

View File

@ -88,6 +88,7 @@ export const TopicItemWrapper = styled.div`
.sealed-message { .sealed-message {
font-style: italic; font-style: italic;
color: #aaaaaa; color: #aaaaaa;
padding-left: 72px;
} }
.asset-placeholder { .asset-placeholder {
@ -103,6 +104,7 @@ export const TopicItemWrapper = styled.div`
.topic-assets { .topic-assets {
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px;
} }
.skeleton { .skeleton {
@ -116,23 +118,24 @@ export const TopicItemWrapper = styled.div`
padding-left: 72px; padding-left: 72px;
white-space: pre-line; white-space: pre-line;
min-height: 4px; 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; display: flex;
flex-direction: column; flex-direction: row;
border-radius: 4px; justify-content: flex-end;
border: 1px solid #aaaaaa; padding-bottom: 8px;
width: 100%; padding-right: 8px;
margin-top: 8px;
margin-bottom: 8px;
.controls {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding-bottom: 8px;
padding-right: 8px;
}
} }
} }
`; `;

View File

@ -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 };
}

View File

@ -6,7 +6,7 @@ import { UploadContext } from 'context/UploadContext';
import { StoreContext } from 'context/StoreContext'; import { StoreContext } from 'context/StoreContext';
import { CardContext } from 'context/CardContext'; import { CardContext } from 'context/CardContext';
import { ProfileContext } from 'context/ProfileContext'; 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 { JSEncrypt } from 'jsencrypt'
import { decryptTopicSubject } from 'context/sealUtil'; import { decryptTopicSubject } from 'context/sealUtil';
@ -23,6 +23,7 @@ export function useConversation(cardId, channelId) {
loading: false, loading: false,
sealed: false, sealed: false,
contentKey: null, contentKey: null,
busy: false,
}); });
const profile = useContext(ProfileContext); const profile = useContext(ProfileContext);
@ -120,7 +121,17 @@ export function useConversation(cardId, channelId) {
// eslint-disable-next-line // eslint-disable-next-line
}, [cardId, channelId]); }, [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 revision = value.data?.detailRevision;
const detail = value.data?.topicDetail || {}; const detail = value.data?.topicDetail || {};
const identity = profile.state.identity || {}; const identity = profile.state.identity || {};
@ -172,42 +183,34 @@ export function useConversation(cardId, channelId) {
} }
} }
if (detail.dataType === 'superbasictopic') { if (item.revision !== revision) {
if (item.revision !== revision) { try {
try { if (detail.dataType === 'superbasictopic') {
const message = JSON.parse(detail.data); const message = JSON.parse(detail.data);
item.assets = message.assets; item.assets = message.assets;
item.text = message.text; item.text = message.text;
item.textColor = message.textColor ? message.textColor : '#444444'; item.textColor = message.textColor ? message.textColor : '#444444';
item.textSize = message.textSize ? message.textSize : 14; item.textSize = message.textSize ? message.textSize : 14;
} }
catch (err) { if (detail.dataType === 'sealedtopic') {
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); const subject = decryptTopicSubject(detail.data, state.contentKey);
item.assets = subject.message.assets; item.assets = subject.message.assets;
item.text = subject.message.text; item.text = subject.message.text;
item.textColor = subject.message.textColor ? subject.message.textColor : '#444444'; item.textColor = subject.message.textColor ? subject.message.textColor : '#444444';
item.textSize = subject.message.textSize ? subject.message.textSize : 14; 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.transform = detail.transform;
item.status = detail.status; item.status = detail.status;
item.assetUrl = conversation.actions.getTopicAssetUrl; item.assetUrl = conversation.actions.getTopicAssetUrl;
item.revision = revision;
}; };
useEffect(() => { const syncChannel = () => {
const messages = new Map(); const messages = new Map();
conversation.state.topics.forEach((value, id) => { conversation.state.topics.forEach((value, id) => {
const curCardId = conversation.state.card?.id; const curCardId = conversation.state.card?.id;
@ -233,8 +236,7 @@ export function useConversation(cardId, channelId) {
}); });
updateState({ topics: sorted }); updateState({ topics: sorted });
// eslint-disable-next-line }
}, [conversation.state, profile.state, card.state, state.contentKey]);
const actions = { const actions = {
more: () => { more: () => {
@ -251,6 +253,34 @@ export function useConversation(cardId, channelId) {
removeTopic: async (topicId) => { removeTopic: async (topicId) => {
await conversation.actions.removeTopic(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 }; return { state, actions };