diff --git a/net/server/internal/api_removeChannelTopic.go b/net/server/internal/api_removeChannelTopic.go index bc26d3fb..9d40380a 100644 --- a/net/server/internal/api_removeChannelTopic.go +++ b/net/server/internal/api_removeChannelTopic.go @@ -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 } diff --git a/net/web/src/User/Conversation/AddTopic/useAddTopic.hook.js b/net/web/src/User/Conversation/AddTopic/useAddTopic.hook.js index b115d718..bf389265 100644 --- a/net/web/src/User/Conversation/AddTopic/useAddTopic.hook.js +++ b/net/web/src/User/Conversation/AddTopic/useAddTopic.hook.js @@ -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'; diff --git a/net/web/src/User/Conversation/Conversation.jsx b/net/web/src/User/Conversation/Conversation.jsx index 998eab0c..bbd8b56a 100644 --- a/net/web/src/User/Conversation/Conversation.jsx +++ b/net/web/src/User/Conversation/Conversation.jsx @@ -28,7 +28,7 @@ export function Conversation() { }, [state]); const topicRenderer = (topic) => { - return () + return () } const onSaveSubject = () => { diff --git a/net/web/src/User/Conversation/TopicItem/TopicItem.jsx b/net/web/src/User/Conversation/TopicItem/TopicItem.jsx index bba42057..48c874ea 100644 --- a/net/web/src/User/Conversation/TopicItem/TopicItem.jsx +++ b/net/web/src/User/Conversation/TopicItem/TopicItem.jsx @@ -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 ( +
+
onEdit()}> + +
+
actions.removeTopic()}> + +
+
+ ); + } + if (host) { + return ( +
+
actions.removeTopic()}> + +
+
+ ); + } + return <>; + } + return (
@@ -49,6 +83,9 @@ export function TopicItem({ topic }) {
{ state.message?.text }
+
+ +
) diff --git a/net/web/src/User/Conversation/TopicItem/TopicItem.styled.js b/net/web/src/User/Conversation/TopicItem/TopicItem.styled.js index c848aeb2..e4e22704 100644 --- a/net/web/src/User/Conversation/TopicItem/TopicItem.styled.js +++ b/net/web/src/User/Conversation/TopicItem/TopicItem.styled.js @@ -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; diff --git a/net/web/src/User/Conversation/TopicItem/useTopicItem.hook.js b/net/web/src/User/Conversation/TopicItem/useTopicItem.hook.js index f8db4630..770b468d 100644 --- a/net/web/src/User/Conversation/TopicItem/useTopicItem.hook.js +++ b/net/web/src/User/Conversation/TopicItem/useTopicItem.hook.js @@ -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); } }; diff --git a/net/web/src/VirtualList/VirtualList.jsx b/net/web/src/VirtualList/VirtualList.jsx index c465b367..0284659a 100644 --- a/net/web/src/VirtualList/VirtualList.jsx +++ b/net/web/src/VirtualList/VirtualList.jsx @@ -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]); }} diff --git a/net/web/src/api/removeChannelTopic.js b/net/web/src/api/removeChannelTopic.js new file mode 100644 index 00000000..bfe0ece5 --- /dev/null +++ b/net/web/src/api/removeChannelTopic.js @@ -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); +} diff --git a/net/web/src/api/removeContactChannelTopic.js b/net/web/src/api/removeContactChannelTopic.js new file mode 100644 index 00000000..1d5a2efa --- /dev/null +++ b/net/web/src/api/removeContactChannelTopic.js @@ -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); +} diff --git a/net/web/src/context/useCardContext.hook.js b/net/web/src/context/useCardContext.hook.js index 022e125e..dac727cb 100644 --- a/net/web/src/context/useCardContext.hook.js +++ b/net/web/src/context/useCardContext.hook.js @@ -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; diff --git a/net/web/src/context/useChannelContext.hook.js b/net/web/src/context/useChannelContext.hook.js index 9790bbb6..11b7e751 100644 --- a/net/web/src/context/useChannelContext.hook.js +++ b/net/web/src/context/useChannelContext.hook.js @@ -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); }, diff --git a/net/web/src/context/useConversationContext.hook.js b/net/web/src/context/useConversationContext.hook.js index 1fe86005..a6cd855c 100644 --- a/net/web/src/context/useConversationContext.hook.js +++ b/net/web/src/context/useConversationContext.hook.js @@ -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 }