mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-14 22:52:15 +02:00
feat: implement long-press dropdown menu for mobile avatars and streamline tooltip usage in sidebar components
This commit is contained in:
parent
6857c1d7e8
commit
f7278f75f4
3 changed files with 172 additions and 40 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Settings, Trash2, Users } from "lucide-react";
|
import { Settings, Trash2, Users } from "lucide-react";
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
|
@ -9,6 +10,13 @@ import {
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -73,6 +81,35 @@ export function SearchSpaceAvatar({
|
||||||
const initials = getInitials(name);
|
const initials = getInitials(name);
|
||||||
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
|
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
|
||||||
|
|
||||||
|
// Long-press state for mobile
|
||||||
|
const [longPressMenuOpen, setLongPressMenuOpen] = useState(false);
|
||||||
|
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const touchMoved = useRef(false);
|
||||||
|
|
||||||
|
const handleTouchStart = useCallback(() => {
|
||||||
|
touchMoved.current = false;
|
||||||
|
longPressTimer.current = setTimeout(() => {
|
||||||
|
if (!touchMoved.current) {
|
||||||
|
setLongPressMenuOpen(true);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTouchMove = useCallback(() => {
|
||||||
|
touchMoved.current = true;
|
||||||
|
if (longPressTimer.current) {
|
||||||
|
clearTimeout(longPressTimer.current);
|
||||||
|
longPressTimer.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTouchEnd = useCallback(() => {
|
||||||
|
if (longPressTimer.current) {
|
||||||
|
clearTimeout(longPressTimer.current);
|
||||||
|
longPressTimer.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const tooltipContent = (
|
const tooltipContent = (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
|
|
@ -112,26 +149,67 @@ export function SearchSpaceAvatar({
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const menuItems = (
|
||||||
|
<>
|
||||||
|
{onSettings && (
|
||||||
|
<DropdownMenuItem onClick={onSettings}>
|
||||||
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
{tCommon("settings")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{onSettings && onDelete && <DropdownMenuSeparator />}
|
||||||
|
{onDelete && isOwner && (
|
||||||
|
<DropdownMenuItem variant="destructive" onClick={onDelete}>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
{tCommon("delete")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{onDelete && !isOwner && (
|
||||||
|
<DropdownMenuItem variant="destructive" onClick={onDelete}>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
{t("leave")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
// If delete or settings handlers are provided, wrap with context menu
|
// If delete or settings handlers are provided, wrap with context menu
|
||||||
if (onDelete || onSettings) {
|
if (onDelete || onSettings) {
|
||||||
|
// Mobile: use long-press triggered DropdownMenu
|
||||||
|
if (disableTooltip) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={longPressMenuOpen} onOpenChange={setLongPressMenuOpen}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<div
|
||||||
|
className="inline-block"
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchMove={handleTouchMove}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
onTouchCancel={handleTouchEnd}
|
||||||
|
>
|
||||||
|
{avatarButton}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-48">
|
||||||
|
{menuItems}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop: use right-click ContextMenu + Tooltip
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
{disableTooltip ? (
|
<Tooltip>
|
||||||
<ContextMenuTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="inline-block">{avatarButton}</div>
|
<ContextMenuTrigger asChild>
|
||||||
</ContextMenuTrigger>
|
<div className="inline-block">{avatarButton}</div>
|
||||||
) : (
|
</ContextMenuTrigger>
|
||||||
<Tooltip>
|
</TooltipTrigger>
|
||||||
<TooltipTrigger asChild>
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
<ContextMenuTrigger asChild>
|
{tooltipContent}
|
||||||
<div className="inline-block">{avatarButton}</div>
|
</TooltipContent>
|
||||||
</ContextMenuTrigger>
|
</Tooltip>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
|
||||||
{tooltipContent}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<ContextMenuContent className="w-48">
|
<ContextMenuContent className="w-48">
|
||||||
{onSettings && (
|
{onSettings && (
|
||||||
<ContextMenuItem onClick={onSettings}>
|
<ContextMenuItem onClick={onSettings}>
|
||||||
|
|
|
||||||
|
|
@ -594,20 +594,15 @@ export function InboxSidebar({
|
||||||
{/* Mobile: Button that opens bottom drawer */}
|
{/* Mobile: Button that opens bottom drawer */}
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<>
|
<>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="ghost"
|
||||||
<Button
|
size="icon"
|
||||||
variant="ghost"
|
className="h-8 w-8 rounded-full"
|
||||||
size="icon"
|
onClick={() => setFilterDrawerOpen(true)}
|
||||||
className="h-8 w-8 rounded-full"
|
>
|
||||||
onClick={() => setFilterDrawerOpen(true)}
|
<ListFilter className="h-4 w-4 text-muted-foreground" />
|
||||||
>
|
<span className="sr-only">{t("filter") || "Filter"}</span>
|
||||||
<ListFilter className="h-4 w-4 text-muted-foreground" />
|
</Button>
|
||||||
<span className="sr-only">{t("filter") || "Filter"}</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="z-80">{t("filter") || "Filter"}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Drawer
|
<Drawer
|
||||||
open={filterDrawerOpen}
|
open={filterDrawerOpen}
|
||||||
onOpenChange={setFilterDrawerOpen}
|
onOpenChange={setFilterDrawerOpen}
|
||||||
|
|
@ -811,6 +806,18 @@ export function InboxSidebar({
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
{isMobile ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 rounded-full"
|
||||||
|
onClick={handleMarkAllAsRead}
|
||||||
|
disabled={totalUnreadCount === 0}
|
||||||
|
>
|
||||||
|
<CheckCheck className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="sr-only">{t("mark_all_read") || "Mark all as read"}</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -828,6 +835,7 @@ export function InboxSidebar({
|
||||||
{t("mark_all_read") || "Mark all as read"}
|
{t("mark_all_read") || "Mark all as read"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
{/* Dock/Undock button - desktop only */}
|
{/* Dock/Undock button - desktop only */}
|
||||||
{!isMobile && onDockedChange && (
|
{!isMobile && onDockedChange && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -976,6 +984,29 @@ export function InboxSidebar({
|
||||||
isMarkingAsRead && "opacity-50 pointer-events-none"
|
isMarkingAsRead && "opacity-50 pointer-events-none"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleItemClick(item)}
|
||||||
|
disabled={isMarkingAsRead}
|
||||||
|
className="flex items-center gap-3 flex-1 min-w-0 text-left overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="shrink-0">{getStatusIcon(item)}</div>
|
||||||
|
<div className="flex-1 min-w-0 overflow-hidden">
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-xs font-medium line-clamp-2",
|
||||||
|
!item.read && "font-semibold"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground line-clamp-2 mt-0.5">
|
||||||
|
{convertRenderedToDisplay(item.message)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
@ -1007,6 +1038,7 @@ export function InboxSidebar({
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Time and unread dot - fixed width to prevent content shift */}
|
{/* Time and unread dot - fixed width to prevent content shift */}
|
||||||
<div className="flex items-center justify-end gap-1.5 shrink-0 w-10">
|
<div className="flex items-center justify-end gap-1.5 shrink-0 w-10">
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,18 @@ export function Sidebar({
|
||||||
defaultOpen={true}
|
defaultOpen={true}
|
||||||
fillHeight={false}
|
fillHeight={false}
|
||||||
className="shrink-0 max-h-[50%] flex flex-col"
|
className="shrink-0 max-h-[50%] flex flex-col"
|
||||||
action={
|
action={
|
||||||
onViewAllSharedChats ? (
|
onViewAllSharedChats ? (
|
||||||
|
disableTooltips ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0"
|
||||||
|
onClick={onViewAllSharedChats}
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -166,10 +176,11 @@ export function Sidebar({
|
||||||
{t("view_all_shared_chats") || "View all shared chats"}
|
{t("view_all_shared_chats") || "View all shared chats"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : undefined
|
)
|
||||||
}
|
) : undefined
|
||||||
>
|
}
|
||||||
{isLoadingChats ? (
|
>
|
||||||
|
{isLoadingChats ? (
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<ChatListItemSkeleton />
|
<ChatListItemSkeleton />
|
||||||
<ChatListItemSkeleton />
|
<ChatListItemSkeleton />
|
||||||
|
|
@ -210,8 +221,18 @@ export function Sidebar({
|
||||||
title={t("chats")}
|
title={t("chats")}
|
||||||
defaultOpen={true}
|
defaultOpen={true}
|
||||||
fillHeight={true}
|
fillHeight={true}
|
||||||
action={
|
action={
|
||||||
onViewAllPrivateChats ? (
|
onViewAllPrivateChats ? (
|
||||||
|
disableTooltips ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0"
|
||||||
|
onClick={onViewAllPrivateChats}
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -227,8 +248,9 @@ export function Sidebar({
|
||||||
{t("view_all_private_chats") || "View all private chats"}
|
{t("view_all_private_chats") || "View all private chats"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : undefined
|
)
|
||||||
}
|
) : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isLoadingChats ? (
|
{isLoadingChats ? (
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue