refactor: replace old drag state with useDragManager hook in ChannelSidebar
This commit is contained in:
@@ -19,6 +19,7 @@ import { joinVoiceChannel, broadcastVoiceStatus, broadcastDeafenViaLiveKit } fro
|
|||||||
import { useContextMenuStore, type ContextMenuItem } from '../../stores/contextMenuStore';
|
import { useContextMenuStore, type ContextMenuItem } from '../../stores/contextMenuStore';
|
||||||
import { ConfirmDialog } from '../ui/ConfirmDialog';
|
import { ConfirmDialog } from '../ui/ConfirmDialog';
|
||||||
import { DmSearchBar } from './DmSearchBar';
|
import { DmSearchBar } from './DmSearchBar';
|
||||||
|
import { useDragManager, type DropTarget } from '../../hooks/useDragManager';
|
||||||
|
|
||||||
export function ChannelSidebar() {
|
export function ChannelSidebar() {
|
||||||
const spaces = useSpaceStore((s) => s.spaces);
|
const spaces = useSpaceStore((s) => s.spaces);
|
||||||
@@ -43,8 +44,6 @@ export function ChannelSidebar() {
|
|||||||
const spaceDeafenedUserIds = useVoiceStore((s) => s.spaceDeafenedUserIds);
|
const spaceDeafenedUserIds = useVoiceStore((s) => s.spaceDeafenedUserIds);
|
||||||
const permissionMutedUserIds = useVoiceStore((s) => s.permissionMutedUserIds);
|
const permissionMutedUserIds = useVoiceStore((s) => s.permissionMutedUserIds);
|
||||||
|
|
||||||
// Drag-and-drop state for moving users between voice channels
|
|
||||||
const [voiceDragState, setVoiceDragState] = useState<{ userId: string; fromChannelId: string } | null>(null);
|
|
||||||
const isSpaceMuted = !!(myOriginId && spaceId && spaceMutedUserIds.has(`${spaceId}:${myOriginId}`));
|
const isSpaceMuted = !!(myOriginId && spaceId && spaceMutedUserIds.has(`${spaceId}:${myOriginId}`));
|
||||||
const isSpaceDeafened = !!(myOriginId && spaceId && spaceDeafenedUserIds.has(`${spaceId}:${myOriginId}`));
|
const isSpaceDeafened = !!(myOriginId && spaceId && spaceDeafenedUserIds.has(`${spaceId}:${myOriginId}`));
|
||||||
const isPermissionMuted = !!(myOriginId && spaceId && permissionMutedUserIds.has(`${spaceId}:${myOriginId}`));
|
const isPermissionMuted = !!(myOriginId && spaceId && permissionMutedUserIds.has(`${spaceId}:${myOriginId}`));
|
||||||
@@ -52,6 +51,7 @@ export function ChannelSidebar() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [floatingPanelEl, setFloatingPanelEl] = useState<HTMLDivElement | null>(null);
|
const [floatingPanelEl, setFloatingPanelEl] = useState<HTMLDivElement | null>(null);
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const floatingPanelHeight = useUIStore((s) => s.floatingPanelHeight);
|
const floatingPanelHeight = useUIStore((s) => s.floatingPanelHeight);
|
||||||
const setFloatingPanelHeight = useUIStore((s) => s.setFloatingPanelHeight);
|
const setFloatingPanelHeight = useUIStore((s) => s.setFloatingPanelHeight);
|
||||||
|
|
||||||
@@ -100,10 +100,6 @@ export function ChannelSidebar() {
|
|||||||
const canCreateInvite = hasPermissionBit(mySpacePerms, PermissionBits.CREATE_INVITE);
|
const canCreateInvite = hasPermissionBit(mySpacePerms, PermissionBits.CREATE_INVITE);
|
||||||
const categories = useSpaceStore((s) => s.categories);
|
const categories = useSpaceStore((s) => s.categories);
|
||||||
|
|
||||||
// Drag state for channel/category reordering
|
|
||||||
const [channelDragState, setChannelDragState] = useState<{ dragType: 'channel' | 'category'; dragId: string } | null>(null);
|
|
||||||
const [dropIndicator, setDropIndicator] = useState<{ targetId: string; position: 'before' | 'after'; type: 'channel' | 'category' } | null>(null);
|
|
||||||
|
|
||||||
// Delete category confirmation state
|
// Delete category confirmation state
|
||||||
const [deleteCategoryId, setDeleteCategoryId] = useState<string | null>(null);
|
const [deleteCategoryId, setDeleteCategoryId] = useState<string | null>(null);
|
||||||
const [deleteCategoryLoading, setDeleteCategoryLoading] = useState(false);
|
const [deleteCategoryLoading, setDeleteCategoryLoading] = useState(false);
|
||||||
@@ -160,6 +156,130 @@ export function ChannelSidebar() {
|
|||||||
return chs.some(ch => unreadChannels.has(ch.id));
|
return chs.some(ch => unreadChannels.has(ch.id));
|
||||||
}, [channelsByCategory, unreadChannels]);
|
}, [channelsByCategory, unreadChannels]);
|
||||||
|
|
||||||
|
// --- Centralized drag-and-drop ---
|
||||||
|
const canMoveMembers = hasPermissionBit(mySpacePerms, PermissionBits.MOVE_MEMBERS);
|
||||||
|
|
||||||
|
const handleChannelDrop = useCallback((dragId: string, target: DropTarget) => {
|
||||||
|
if (!currentSpaceId) return;
|
||||||
|
|
||||||
|
const allChannelsCopy = channels.map(ch => ({
|
||||||
|
id: ch.id,
|
||||||
|
position: ch.position,
|
||||||
|
categoryId: ch.categoryId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (target.targetType === 'channel') {
|
||||||
|
const targetCh = channels.find(c => c.id === target.targetId);
|
||||||
|
if (targetCh) {
|
||||||
|
const dragCh = allChannelsCopy.find(c => c.id === dragId);
|
||||||
|
if (dragCh) dragCh.categoryId = targetCh.categoryId;
|
||||||
|
}
|
||||||
|
} else if (target.targetType === 'category') {
|
||||||
|
const dragCh = allChannelsCopy.find(c => c.id === dragId);
|
||||||
|
if (dragCh) dragCh.categoryId = target.targetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const grouped = new Map<string | null, typeof allChannelsCopy>();
|
||||||
|
for (const ch of allChannelsCopy) {
|
||||||
|
let arr = grouped.get(ch.categoryId);
|
||||||
|
if (!arr) { arr = []; grouped.set(ch.categoryId, arr); }
|
||||||
|
arr.push(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [, arr] of grouped) {
|
||||||
|
arr.sort((a, b) => a.position - b.position);
|
||||||
|
const dragIdx = arr.findIndex(c => c.id === dragId);
|
||||||
|
if (dragIdx === -1) continue;
|
||||||
|
const dragItem = arr[dragIdx]!;
|
||||||
|
arr.splice(dragIdx, 1);
|
||||||
|
|
||||||
|
if (target.targetType === 'channel') {
|
||||||
|
const targetIdx = arr.findIndex(c => c.id === target.targetId);
|
||||||
|
if (targetIdx !== -1) {
|
||||||
|
const insertIdx = target.position === 'before' ? targetIdx : targetIdx + 1;
|
||||||
|
arr.splice(insertIdx, 0, dragItem);
|
||||||
|
} else {
|
||||||
|
arr.push(dragItem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arr.unshift(dragItem);
|
||||||
|
}
|
||||||
|
arr.forEach((ch, i) => { ch.position = i; });
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelUpdates = allChannelsCopy.map(ch => ({
|
||||||
|
id: ch.id,
|
||||||
|
position: ch.position,
|
||||||
|
categoryId: ch.categoryId,
|
||||||
|
}));
|
||||||
|
const categoryUpdates = sortedCategories.map(c => ({
|
||||||
|
id: c.id,
|
||||||
|
position: c.position,
|
||||||
|
}));
|
||||||
|
|
||||||
|
useSpaceStore.getState().setChannels(
|
||||||
|
channels.map(ch => {
|
||||||
|
const update = channelUpdates.find(u => u.id === ch.id);
|
||||||
|
if (update) return { ...ch, position: update.position, categoryId: update.categoryId };
|
||||||
|
return ch;
|
||||||
|
}).sort((a, b) => a.position - b.position)
|
||||||
|
);
|
||||||
|
useSpaceStore.getState().updateChannelLayout(currentSpaceId, { channels: channelUpdates, categories: categoryUpdates });
|
||||||
|
}, [channels, sortedCategories, currentSpaceId]);
|
||||||
|
|
||||||
|
const handleCategoryDrop = useCallback((dragId: string, target: DropTarget) => {
|
||||||
|
if (!currentSpaceId) return;
|
||||||
|
|
||||||
|
const catsCopy = sortedCategories.map(c => ({ id: c.id, position: c.position }));
|
||||||
|
const dragIdx = catsCopy.findIndex(c => c.id === dragId);
|
||||||
|
if (dragIdx === -1) return;
|
||||||
|
|
||||||
|
const dragItem = catsCopy[dragIdx]!;
|
||||||
|
catsCopy.splice(dragIdx, 1);
|
||||||
|
const targetIdx = catsCopy.findIndex(c => c.id === target.targetId);
|
||||||
|
if (targetIdx !== -1) {
|
||||||
|
const insertIdx = target.position === 'before' ? targetIdx : targetIdx + 1;
|
||||||
|
catsCopy.splice(insertIdx, 0, dragItem);
|
||||||
|
} else {
|
||||||
|
catsCopy.push(dragItem);
|
||||||
|
}
|
||||||
|
catsCopy.forEach((c, i) => { c.position = i; });
|
||||||
|
|
||||||
|
const channelUpdates = channels.map(ch => ({
|
||||||
|
id: ch.id,
|
||||||
|
position: ch.position,
|
||||||
|
categoryId: ch.categoryId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
useSpaceStore.getState().setCategories(
|
||||||
|
categories.map(cat => {
|
||||||
|
const update = catsCopy.find(u => u.id === cat.id);
|
||||||
|
if (update) return { ...cat, position: update.position };
|
||||||
|
return cat;
|
||||||
|
}).sort((a, b) => a.position - b.position)
|
||||||
|
);
|
||||||
|
useSpaceStore.getState().updateChannelLayout(currentSpaceId, { channels: channelUpdates, categories: catsCopy });
|
||||||
|
}, [sortedCategories, categories, channels, currentSpaceId]);
|
||||||
|
|
||||||
|
const handleVoiceUserDrop = useCallback((userId: string, fromChannelId: string, toChannelId: string) => {
|
||||||
|
const voiceOrigin = getChannelOrigin(fromChannelId);
|
||||||
|
wsSend({ type: 'voice_move', userId, targetChannelId: toChannelId }, voiceOrigin);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
activeDrag, dropTarget,
|
||||||
|
channelHandlers, categoryHandlers,
|
||||||
|
voiceUserHandlers, voiceChannelDropZone,
|
||||||
|
containerHandlers,
|
||||||
|
} = useDragManager({
|
||||||
|
scrollContainerRef,
|
||||||
|
canManage: canManageChannels,
|
||||||
|
canMoveMembers,
|
||||||
|
onChannelDrop: handleChannelDrop,
|
||||||
|
onCategoryDrop: handleCategoryDrop,
|
||||||
|
onVoiceUserDrop: handleVoiceUserDrop,
|
||||||
|
});
|
||||||
|
|
||||||
const handleSidebarContextMenu = useCallback((e: React.MouseEvent) => {
|
const handleSidebarContextMenu = useCallback((e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -241,165 +361,6 @@ export function ChannelSidebar() {
|
|||||||
]);
|
]);
|
||||||
}, [openContextMenu, currentChannelId, navigate, setCurrentChannel]);
|
}, [openContextMenu, currentChannelId, navigate, setCurrentChannel]);
|
||||||
|
|
||||||
// DnD handlers
|
|
||||||
const handleChannelDragStart = useCallback((e: React.DragEvent, channelId: string) => {
|
|
||||||
if (!canManageChannels) return;
|
|
||||||
e.dataTransfer.setData('application/x-channel-id', channelId);
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
setChannelDragState({ dragType: 'channel', dragId: channelId });
|
|
||||||
}, [canManageChannels]);
|
|
||||||
|
|
||||||
const handleCategoryDragStart = useCallback((e: React.DragEvent, categoryId: string) => {
|
|
||||||
if (!canManageChannels) return;
|
|
||||||
e.dataTransfer.setData('application/x-category-id', categoryId);
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
setChannelDragState({ dragType: 'category', dragId: categoryId });
|
|
||||||
}, [canManageChannels]);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(() => {
|
|
||||||
setChannelDragState(null);
|
|
||||||
setDropIndicator(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleChannelDragOver = useCallback((e: React.DragEvent, targetId: string, type: 'channel' | 'category') => {
|
|
||||||
if (!channelDragState) return;
|
|
||||||
e.preventDefault();
|
|
||||||
e.dataTransfer.dropEffect = 'move';
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
|
||||||
const midY = rect.top + rect.height / 2;
|
|
||||||
const position = e.clientY < midY ? 'before' : 'after';
|
|
||||||
setDropIndicator({ targetId, position, type });
|
|
||||||
}, [channelDragState]);
|
|
||||||
|
|
||||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!channelDragState || !currentSpaceId || !dropIndicator) {
|
|
||||||
setChannelDragState(null);
|
|
||||||
setDropIndicator(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dragChannelId = e.dataTransfer.getData('application/x-channel-id');
|
|
||||||
const dragCategoryId = e.dataTransfer.getData('application/x-category-id');
|
|
||||||
|
|
||||||
if (dragChannelId && dropIndicator) {
|
|
||||||
// Channel drag — compute new layout
|
|
||||||
const allChannelsCopy = channels.map(ch => ({
|
|
||||||
id: ch.id,
|
|
||||||
position: ch.position,
|
|
||||||
categoryId: ch.categoryId,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (dropIndicator.type === 'channel') {
|
|
||||||
// Dropping on a channel — take its categoryId and insert near it
|
|
||||||
const targetCh = channels.find(c => c.id === dropIndicator.targetId);
|
|
||||||
if (targetCh) {
|
|
||||||
const dragCh = allChannelsCopy.find(c => c.id === dragChannelId);
|
|
||||||
if (dragCh) {
|
|
||||||
dragCh.categoryId = targetCh.categoryId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (dropIndicator.type === 'category') {
|
|
||||||
// Dropping on a category header — move channel into that category
|
|
||||||
const dragCh = allChannelsCopy.find(c => c.id === dragChannelId);
|
|
||||||
if (dragCh) {
|
|
||||||
dragCh.categoryId = dropIndicator.targetId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recalculate positions: group by category and assign sequential positions
|
|
||||||
const grouped = new Map<string | null, typeof allChannelsCopy>();
|
|
||||||
for (const ch of allChannelsCopy) {
|
|
||||||
const key = ch.categoryId;
|
|
||||||
let arr = grouped.get(key);
|
|
||||||
if (!arr) { arr = []; grouped.set(key, arr); }
|
|
||||||
arr.push(ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Within each group, move dragged channel to correct position
|
|
||||||
for (const [, arr] of grouped) {
|
|
||||||
arr.sort((a, b) => a.position - b.position);
|
|
||||||
const dragIdx = arr.findIndex(c => c.id === dragChannelId);
|
|
||||||
if (dragIdx === -1) continue;
|
|
||||||
const dragItem = arr[dragIdx]!;
|
|
||||||
arr.splice(dragIdx, 1);
|
|
||||||
|
|
||||||
// Find target position
|
|
||||||
if (dropIndicator.type === 'channel') {
|
|
||||||
const targetIdx = arr.findIndex(c => c.id === dropIndicator.targetId);
|
|
||||||
if (targetIdx !== -1) {
|
|
||||||
const insertIdx = dropIndicator.position === 'before' ? targetIdx : targetIdx + 1;
|
|
||||||
arr.splice(insertIdx, 0, dragItem);
|
|
||||||
} else {
|
|
||||||
arr.push(dragItem);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Dropped on category header — add at start
|
|
||||||
arr.unshift(dragItem);
|
|
||||||
}
|
|
||||||
// Reassign positions
|
|
||||||
arr.forEach((ch, i) => { ch.position = i; });
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelUpdates = allChannelsCopy.map(ch => ({
|
|
||||||
id: ch.id,
|
|
||||||
position: ch.position,
|
|
||||||
categoryId: ch.categoryId,
|
|
||||||
}));
|
|
||||||
const categoryUpdates = sortedCategories.map(c => ({
|
|
||||||
id: c.id,
|
|
||||||
position: c.position,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Optimistic update
|
|
||||||
useSpaceStore.getState().setChannels(
|
|
||||||
channels.map(ch => {
|
|
||||||
const update = channelUpdates.find(u => u.id === ch.id);
|
|
||||||
if (update) return { ...ch, position: update.position, categoryId: update.categoryId };
|
|
||||||
return ch;
|
|
||||||
}).sort((a, b) => a.position - b.position)
|
|
||||||
);
|
|
||||||
|
|
||||||
useSpaceStore.getState().updateChannelLayout(currentSpaceId, { channels: channelUpdates, categories: categoryUpdates });
|
|
||||||
} else if (dragCategoryId && dropIndicator?.type === 'category') {
|
|
||||||
// Category drag — reorder categories
|
|
||||||
const catsCopy = sortedCategories.map(c => ({ id: c.id, position: c.position }));
|
|
||||||
const dragIdx = catsCopy.findIndex(c => c.id === dragCategoryId);
|
|
||||||
if (dragIdx !== -1) {
|
|
||||||
const dragItem = catsCopy[dragIdx]!;
|
|
||||||
catsCopy.splice(dragIdx, 1);
|
|
||||||
const targetIdx = catsCopy.findIndex(c => c.id === dropIndicator.targetId);
|
|
||||||
if (targetIdx !== -1) {
|
|
||||||
const insertIdx = dropIndicator.position === 'before' ? targetIdx : targetIdx + 1;
|
|
||||||
catsCopy.splice(insertIdx, 0, dragItem);
|
|
||||||
} else {
|
|
||||||
catsCopy.push(dragItem);
|
|
||||||
}
|
|
||||||
catsCopy.forEach((c, i) => { c.position = i; });
|
|
||||||
|
|
||||||
const channelUpdates = channels.map(ch => ({
|
|
||||||
id: ch.id,
|
|
||||||
position: ch.position,
|
|
||||||
categoryId: ch.categoryId,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Optimistic update
|
|
||||||
useSpaceStore.getState().setCategories(
|
|
||||||
categories.map(cat => {
|
|
||||||
const update = catsCopy.find(u => u.id === cat.id);
|
|
||||||
if (update) return { ...cat, position: update.position };
|
|
||||||
return cat;
|
|
||||||
}).sort((a, b) => a.position - b.position)
|
|
||||||
);
|
|
||||||
|
|
||||||
useSpaceStore.getState().updateChannelLayout(currentSpaceId, { channels: channelUpdates, categories: catsCopy });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setChannelDragState(null);
|
|
||||||
setDropIndicator(null);
|
|
||||||
}, [channelDragState, dropIndicator, channels, sortedCategories, categories, currentSpaceId]);
|
|
||||||
|
|
||||||
const handleChannelClick = (channelId: string) => {
|
const handleChannelClick = (channelId: string) => {
|
||||||
setCurrentChannel(channelId);
|
setCurrentChannel(channelId);
|
||||||
navigate(`/channels/${currentSpaceId || '@me'}/${channelId}`);
|
navigate(`/channels/${currentSpaceId || '@me'}/${channelId}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user