navigateToBookmark(bookmark)}
onContextMenu={e => {
@@ -258,7 +325,9 @@ function FolderBookmarkItem({ bookmark, folderIndex, bookmarkIndex, methods, isC
modalKey={key}
bookmark={bookmark}
onSave={name => {
- const folder = methods.bookmarks[folderIndex] as BookmarkFolder;
+ const folder = bookmarks[folderIndex];
+ if (!isBookmarkFolder(folder)) return;
+
const newBookmarks = [...folder.bookmarks];
newBookmarks[bookmarkIndex].name = name;
methods.editBookmark(folderIndex, { bookmarks: newBookmarks });
@@ -307,16 +376,17 @@ function FolderBookmarkItem({ bookmark, folderIndex, bookmarkIndex, methods, isC
);
}
-function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder?: () => void; isCurrentChannel?: boolean; }) {
- const { bookmarks, index, methods, isExpanded, onToggleFolder, isCurrentChannel } = props;
+function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder?: () => void; isCurrentChannel?: boolean; searchActive?: boolean; }) {
+ const { bookmarks, index, methods, isExpanded, onToggleFolder, isCurrentChannel, searchActive } = props;
const bookmark = bookmarks[index];
const { bookmarkNotificationDot } = settings.use(["bookmarkNotificationDot"]);
const ref = useRef
(null);
+ const [dropSide, setDropSide] = useState<"before" | "after" | null>(null);
- const [, drag] = useDrag(() => ({
+ const [{ isDragging }, drag] = useDrag(() => ({
type: "vc_Bookmark",
- canDrag: () => !isBookmarkFolder(bookmark), // Don't allow dragging folders
+ canDrag: () => !isBookmarkFolder(bookmark) && !searchActive,
item: () => {
// find current index dynamically instead of using stale closure
const currentIndex = bookmarks.findIndex(b =>
@@ -334,22 +404,30 @@ function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder?
collect: monitor => ({
isDragging: !!monitor.isDragging()
}),
- }), [bookmarks, bookmark, index]);
+ }), [bookmarks, bookmark, index, searchActive]);
- const [, drop] = useDrop(() => ({
+ const [{ isOver: isReorderOver, canDrop: canReorderDrop }, drop] = useDrop(() => ({
accept: "vc_Bookmark",
+ canDrop: item => !searchActive && !item.isFromFolder,
hover: (item, monitor) => {
- if (!ref.current) return;
+ if (!ref.current || searchActive || item.isFromFolder) return;
const dragIndex = item.index;
const hoverIndex = index;
- if (dragIndex === hoverIndex) return;
+ if (dragIndex === hoverIndex) {
+ setDropSide(null);
+ return;
+ }
- const hoverBoundingRect = ref.current?.getBoundingClientRect();
- const hoverMiddleX =
- (hoverBoundingRect.right - hoverBoundingRect.left) / 2;
+ const hoverBoundingRect = ref.current.getBoundingClientRect();
+ const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2;
const clientOffset = monitor.getClientOffset();
+ if (!clientOffset) return;
+
const hoverClientX = clientOffset.x - hoverBoundingRect.left;
+ const nextDropSide = hoverClientX < hoverMiddleX ? "before" : "after";
+ setDropSide(nextDropSide);
+
if (dragIndex < hoverIndex && hoverClientX < hoverMiddleX
|| dragIndex > hoverIndex && hoverClientX > hoverMiddleX) {
return;
@@ -358,7 +436,15 @@ function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder?
methods.moveDraggedBookmarks(dragIndex, hoverIndex);
item.index = hoverIndex;
},
- }), []);
+ collect: monitor => ({
+ isOver: monitor.isOver({ shallow: true }),
+ canDrop: monitor.canDrop()
+ }),
+ }), [index, methods, searchActive]);
+
+ useEffect(() => {
+ if (!isReorderOver && dropSide) setDropSide(null);
+ }, [dropSide, isReorderOver]);
// separate drop zone for folders to accept tabs and bookmarks (prevents duplicates)
const [{ isOver: isFolderDropTarget }, folderDrop] = useDrop(() => ({
@@ -416,9 +502,14 @@ function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder?
{
@@ -485,6 +576,9 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI
const { guildId, channelId, userId } = props;
const [bookmarks, methods] = useBookmarks(userId);
const [expandedFolders, setExpandedFolders] = useState
>(new Set());
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+ const searchInputRef = useRef(null);
// make setting reactive to changes
const { bookmarksIndependentFromTabs } = settings.use(["bookmarksIndependentFromTabs"]);
@@ -498,6 +592,14 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI
activeTab.channelId === currentChannelId &&
(activeTab.guildId || "@me") === currentGuildId;
+ const normalizedSearchQuery = searchQuery.trim().toLowerCase();
+ const searchActive = normalizedSearchQuery.length > 0;
+
+ useEffect(() => {
+ if (!isSearchOpen) return;
+ searchInputRef.current?.focus();
+ }, [isSearchOpen]);
+
const toggleFolder = (folderIndex: number) => {
setExpandedFolders(prev => {
const next = new Set(prev);
@@ -523,8 +625,9 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI
const [{ isOver }, dropRef] = useDrop(() => ({
accept: ["vc_ChannelTab", "vc_Bookmark"],
+ canDrop: () => !searchActive,
drop: (item: any, monitor) => {
- if (!bookmarks) return;
+ if (!bookmarks || searchActive) return;
// check if a nested drop zone (like a folder) already handled this
if (monitor.didDrop()) {
@@ -557,7 +660,9 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI
collect: monitor => ({
isOver: monitor.isOver({ shallow: true })
})
- }), [bookmarks, methods]);
+ }), [bookmarks, methods, searchActive]);
+
+ const matchesSearch = (value?: string) => value?.toLowerCase().includes(normalizedSearchQuery) ?? false;
return (
@@ -570,52 +675,121 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI
}
{Array.isArray(bookmarks) && bookmarks.length > 0 &&
bookmarks.flatMap((bookmark, i) => {
- const items = [
+ if (!isBookmarkFolder(bookmark)) {
+ if (searchActive && !matchesSearch(bookmark.name)) return [];
+
+ return [
+
+ ];
+ }
+
+ const folderMatches = searchActive ? matchesSearch(bookmark.name) : false;
+ const visibleFolderBookmarks = bookmark.bookmarks
+ .map((childBookmark, childIndex) => ({ childBookmark, childIndex }))
+ .filter(({ childBookmark }) => !searchActive || folderMatches || matchesSearch(childBookmark.name));
+
+ if (searchActive && !folderMatches && !visibleFolderBookmarks.length) return [];
+
+ const isExpanded = searchActive
+ ? visibleFolderBookmarks.length > 0
+ : expandedFolders.has(i);
+
+ const folderItems = [
toggleFolder(i)}
- isCurrentChannel={
- bookmarksIndependentFromTabs &&
- !channelMatchesActiveTab && (
- !isBookmarkFolder(bookmark) &&
- bookmark.channelId === currentChannelId &&
- (bookmark.guildId || "@me") === currentGuildId
- )
- }
+ searchActive={searchActive}
+ isExpanded={isExpanded}
+ onToggleFolder={searchActive ? undefined : () => toggleFolder(i)}
+ isCurrentChannel={false}
/>
];
- // if folder is expanded, render its bookmarks
- if (isBookmarkFolder(bookmark) && expandedFolders.has(i)) {
- bookmark.bookmarks.forEach((childBookmark, childIndex) => {
- items.push(
+ if (isExpanded) {
+ visibleFolderBookmarks.forEach(({ childBookmark, childIndex }) => {
+ folderItems.push(
);
});
}
- return items;
+ return folderItems;
})
}
+
+
+ {
+ if (!searchQuery.trim()) setIsSearchOpen(false);
+ }}
+ onKeyDown={e => {
+ if (e.key !== "Escape") return;
+ if (searchQuery.trim()) {
+ setSearchQuery("");
+ } else {
+ setIsSearchOpen(false);
+ }
+ }}
+ />
+
+
+ {p => }
+
+
+
{p =>