support deleting topics

This commit is contained in:
Roland Osborne 2022-05-26 15:19:58 -07:00
parent 04c965847b
commit 08517c4abd
12 changed files with 134 additions and 25 deletions

View File

@ -41,8 +41,8 @@ func RemoveChannelTopic(w http.ResponseWriter, r *http.Request) {
}
// check permission
if topicSlot.Topic.Guid != guid {
ErrResponse(w, http.StatusUnauthorized, errors.New("not creator of topic"))
if act.Guid != guid && topicSlot.Topic.Guid != guid {
ErrResponse(w, http.StatusUnauthorized, errors.New("not creator of topic or host"))
return
}

View File

@ -1,7 +1,5 @@
import { useContext, useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { addChannelTopic } from 'api/addChannelTopic';
import { addContactChannelTopic } from 'api/addContactChannelTopic';
import { CardContext } from 'context/CardContext';
import { ChannelContext } from 'context/ChannelContext';

View File

@ -28,7 +28,7 @@ export function Conversation() {
}, [state]);
const topicRenderer = (topic) => {
return (<TopicItem topic={topic} />)
return (<TopicItem host={state.cardId == null} topic={topic} />)
}
const onSaveSubject = () => {

View File

@ -5,10 +5,11 @@ import { VideoAsset } from './VideoAsset/VideoAsset';
import { AudioAsset } from './AudioAsset/AudioAsset';
import { ImageAsset } from './ImageAsset/ImageAsset';
import { Avatar } from 'avatar/Avatar';
import { CommentOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Carousel } from 'Carousel/Carousel';
export function TopicItem({ topic }) {
export function TopicItem({ host, topic }) {
const { state, actions } = useTopicItem(topic);
@ -37,6 +38,39 @@ export function TopicItem({ topic }) {
return <></>
}
const onEdit = () => {
console.log("EDIT TOPIC");
}
const onDelete = () => {
console.log("DELETE TOPIC");
}
const Options = () => {
if (state.owner) {
return (
<div class="buttons">
<div class="button" onClick={() => onEdit()}>
<EditOutlined />
</div>
<div class="button" onClick={() => actions.removeTopic()}>
<DeleteOutlined />
</div>
</div>
);
}
if (host) {
return (
<div class="buttons">
<div class="button" onClick={() => actions.removeTopic()}>
<DeleteOutlined />
</div>
</div>
);
}
return <></>;
}
return (
<TopicItemWrapper>
<div class="avatar">
@ -49,6 +83,9 @@ export function TopicItem({ topic }) {
</div>
<Carousel ready={state.ready} items={state.assets} itemRenderer={renderAsset} />
<div class="message">{ state.message?.text }</div>
<div class="options">
<Options />
</div>
</div>
</TopicItemWrapper>
)

View File

@ -15,6 +15,34 @@ export const TopicItemWrapper = styled.div`
display: flex;
flex-direction: column;
padding-left: 8px;
flex-grow: 1;
&:hover .options {
visibility: visible;
}
.options {
position: absolute;
top: 0;
right: 0;
visibility: hidden;
.buttons {
display: flex;
flex-direction: row;
border-radius: 4px;
background-color: #eeeeee;
border: 1px solid #555555;
margin-top: 2px;
.button {
font-size: 14px;
margin-left: 8px;
margin-right: 8px;
cursor: pointer;
}
}
}
.info {
display: flex;

View File

@ -14,6 +14,7 @@ export function useTopicItem(topic) {
message: null,
created: null,
ready: false,
owner: false,
assets: [],
});
@ -26,6 +27,10 @@ export function useTopicItem(topic) {
}
useEffect(() => {
let owner = false;
if (profile.state.profile.guid == topic?.data?.topicDetail.guid) {
owner = true;
}
if (!topic?.data) {
console.log("invalid topic:", topic);
@ -56,11 +61,11 @@ export function useTopicItem(topic) {
const { guid, created } = topic.data.topicDetail;
if (profile.state.profile.guid == guid) {
const { name, handle, imageUrl } = profile.actions.getProfile();
updateState({ name, handle, imageUrl, status, message, transform, assets, ready, created });
updateState({ name, handle, imageUrl, status, message, transform, assets, ready, created, owner });
}
else {
const { name, handle, imageUrl } = card.actions.getCardProfileByGuid(guid);
updateState({ name, handle, imageUrl, status, message, transform, assets, ready, created });
updateState({ name, handle, imageUrl, status, message, transform, assets, ready, created, owner });
}
}
}, [profile, card, conversation, topic]);
@ -68,6 +73,9 @@ export function useTopicItem(topic) {
const actions = {
getAssetUrl: (assetId) => {
return conversation.actions.getAssetUrl(topic?.id, assetId);
},
removeTopic: async () => {
return conversation.actions.removeTopic(topic.id);
}
};

View File

@ -22,6 +22,7 @@ export function VirtualList({ id, items, itemRenderer }) {
let containers = useRef([]);
let listRef = useRef();
let key = useRef(null);
let itemView = useRef([]);
const addSlot = (id, slot) => {
setSlots((m) => { m.set(id, slot); return new Map(m); })
@ -59,6 +60,7 @@ export function VirtualList({ id, items, itemRenderer }) {
containers.current = [];
clearSlots();
}
itemView.current = items;
setItems();
}, [items, id]);
@ -87,7 +89,7 @@ export function VirtualList({ id, items, itemRenderer }) {
const limitScroll = () => {
let view = getPlacement();
if (view && containers.current[containers.current.length - 1].index == items.length - 1) {
if (view && containers.current[containers.current.length - 1].index == itemView.current.length - 1) {
if (view?.overscan?.bottom <= 0) {
if (view.position.height < viewHeight.current) {
if (scrollTop.current != view.position.top) {
@ -116,14 +118,14 @@ export function VirtualList({ id, items, itemRenderer }) {
let view = getPlacement();
if (view) {
if (view.overscan.top < OVERSCAN) {
if (containers.current[0].index > 0 && containers.current[0].index < items.length) {
if (containers.current[0].index > 0 && containers.current[0].index < itemView.current.length) {
let below = containers.current[0];
let container = {
top: below.top - (DEFAULT_ITEM_HEIGHT + 2 * GUTTER),
height: DEFAULT_ITEM_HEIGHT,
index: containers.current[0].index - 1,
id: items[containers.current[0].index - 1].id,
revision: items[containers.current[0].index - 1].revision,
id: itemView.current[containers.current[0].index - 1].id,
revision: itemView.current[containers.current[0].index - 1].revision,
}
containers.current.unshift(container);
addSlot(container.id, getSlot(container))
@ -131,14 +133,14 @@ export function VirtualList({ id, items, itemRenderer }) {
}
}
if (view.overscan.bottom < OVERSCAN) {
if (containers.current[containers.current.length - 1].index + 1 < items.length) {
if (containers.current[containers.current.length - 1].index + 1 < itemView.current.length) {
let above = containers.current[containers.current.length - 1];
let container = {
top: above.top + above.height + 2 * GUTTER,
height: DEFAULT_ITEM_HEIGHT,
index: containers.current[containers.current.length - 1].index + 1,
id: items[containers.current[containers.current.length - 1].index + 1].id,
revision: items[containers.current[containers.current.length - 1].index + 1].revision,
id: itemView.current[containers.current[containers.current.length - 1].index + 1].id,
revision: itemView.current[containers.current[containers.current.length - 1].index + 1].revision,
}
containers.current.push(container);
addSlot(container.id, getSlot(container))
@ -239,21 +241,21 @@ export function VirtualList({ id, items, itemRenderer }) {
for (let i = 0; i < containers.current.length; i++) {
let container = containers.current[i];
if (items.length <= container.index || items[container.index].id != container.id) {
for (let j = i; j < containers.current.length; j++) {
if (itemView.current.length <= container.index || itemView.current[container.index].id != container.id) {
while (containers.current.length > i) {
let popped = containers.current.pop();
removeSlot(popped.id);
}
break;
}
else if (items[container.index].revision != container.revision) {
else if (itemView.current[container.index].revision != container.revision) {
updateSlot(container.id, getSlot(containers.current[i]));
containers.revision = items[container.index].revision;
containers.revision = itemView.current[container.index].revision;
}
}
// place first slot
if (items.length > 0 && canvasHeight > 0) {
if (itemView.current.length > 0 && canvasHeight > 0) {
let view = getPlacement();
if (!view) {
let pos = canvasHeight / 2;
@ -263,9 +265,9 @@ export function VirtualList({ id, items, itemRenderer }) {
let container = {
top: pos - DEFAULT_ITEM_HEIGHT,
height: DEFAULT_ITEM_HEIGHT,
index: items.length - 1,
id: items[items.length - 1].id,
revision: items[items.length - 1].revision,
index: itemView.current.length - 1,
id: itemView.current[itemView.current.length - 1].id,
revision: itemView.current[itemView.current.length - 1].revision,
}
containers.current.push(container);
@ -289,7 +291,7 @@ export function VirtualList({ id, items, itemRenderer }) {
if (typeof height !== 'undefined') {
onItemHeight(container, height);
}
return itemRenderer(items[container.index]);
return itemRenderer(itemView.current[container.index]);
}}
</ReactResizeDetector>
</VirtualItem>

View File

@ -0,0 +1,8 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function removeChannelTopic(token, channelId, topicId) {
let channel = await fetchWithTimeout(`/content/channels/${channelId}/topics/${topicId}?agent=${token}`,
{ method: 'DELETE' });
checkResponse(channel);
}

View File

@ -0,0 +1,8 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function removeContactChannelTopic(server, token, channelId, topicId) {
let channel = await fetchWithTimeout(`https://${server}//content/channels/${channelId}/topics/${topicId}?contact=${token}`,
{ method: 'DELETE' });
checkResponse(channel);
}

View File

@ -9,6 +9,7 @@ import { getCardImageUrl } from 'api/getCardImageUrl';
import { getCardProfile } from 'api/getCardProfile';
import { getCardDetail } from 'api/getCardDetail';
import { removeContactChannel } from 'api/removeContactChannel';
import { removeContactChannelTopic } from 'api/removeContactChannelTopic';
import { addContactChannelTopic } from 'api/addContactChannelTopic';
import { setCardConnecting, setCardConnected, setCardConfirmed } from 'api/setCardStatus';
import { getCardOpenMessage } from 'api/getCardOpenMessage';
@ -220,6 +221,12 @@ export function useCardContext() {
let node = cardProfile.node;
await removeContactChannel(node, token, channelId);
},
removeChannelTopic: async (cardId, channelId, topicId) => {
let { cardProfile, cardDetail } = cards.current.get(cardId).data;
let token = cardProfile.guid + '.' + cardDetail.token;
let node = cardProfile.node;
await removeContactChannelTopic(node, token, channelId, topicId);
},
addChannelTopic: async (cardId, channelId, message, assets) => {
let { cardProfile, cardDetail } = cards.current.get(cardId).data;
let token = cardProfile.guid + '.' + cardDetail.token;

View File

@ -4,6 +4,7 @@ import { getChannelDetail } from 'api/getChannelDetail';
import { getChannelSummary } from 'api/getChannelSummary';
import { addChannel } from 'api/addChannel';
import { removeChannel } from 'api/removeChannel';
import { removeChannelTopic } from 'api/removeChannelTopic';
import { addChannelTopic } from 'api/addChannelTopic';
import { getChannelTopics } from 'api/getChannelTopics';
import { getChannelTopic } from 'api/getChannelTopic';
@ -109,6 +110,9 @@ export function useChannelContext() {
removeChannel: async (channelId) => {
return await removeChannel(access.current, channelId);
},
removeChannelTopic: async (channelId, topicId) => {
return await removeChannelTopic(access.current, channelId, topicId);
},
addChannelTopic: async (channelId, message, assets) => {
await addChannelTopic(access.current, channelId, message, assets);
},

View File

@ -247,6 +247,15 @@ export function useConversationContext() {
return await channel.actions.removeChannel(channelId);
}
},
removeTopic: async (topicId) => {
const { cardId, channelId } = conversationId.current;
if (cardId) {
return await card.actions.removeChannelTopic(cardId, channelId, topicId);
}
else {
return await channel.actions.removeChannelTopic(channelId, topicId);
}
},
}
return { state, actions }