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();
};
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;

View File

@ -91,7 +91,7 @@ export function AccountAccess() {
<LockOutlined />
<div className="label">Change Login</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>
<div className="switch">
<Switch size="small" checked={state.sealEnabled} onChange={enable => actions.enableSeal(enable)} />
@ -130,7 +130,7 @@ export function AccountAccess() {
</SealModal>
</Modal>
<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.Item name="username" validateStatus={state.editStatus} help={state.editMessage}>
<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 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

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 { 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 <ImageAsset thumbUrl={topic.assetUrl(asset.image.thumb, topic.id)}
@ -62,8 +79,8 @@ export function TopicItem({ host, topic, remove }) {
</div>
<div class="topic-options">
<div class="buttons">
{ topic.creator && (
<div class="button edit" onClick={() => console.log('edit')}>
{ !sealed && topic.creator && (
<div class="button edit" onClick={() => actions.setEditing(topic.text)}>
<EditOutlined />
</div>
)}
@ -101,9 +118,27 @@ export function TopicItem({ host, topic, remove }) {
)}
</>
)}
<div class="message">
<div style={{ color: topic.textColor, fontSize: topic.textSize }}>{ topic.text }</div>
</div>
{ sealed && (
<div class="sealed-message">sealed message</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>

View File

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

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