Sidebar polish: search copy, dividers, project reorder, fix DnD (#8568)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,9 @@ const DEFAULT_LIMIT = 500;
|
|||||||
// Add narrowly scoped exceptions here with justification
|
// Add narrowly scoped exceptions here with justification
|
||||||
const EXCEPTIONS = {
|
const EXCEPTIONS = {
|
||||||
"src/features/sidebar/ui/SidebarProjectsSection.tsx": {
|
"src/features/sidebar/ui/SidebarProjectsSection.tsx": {
|
||||||
limit: 560,
|
limit: 570,
|
||||||
justification:
|
justification:
|
||||||
"Drag-and-drop handlers plus activeProjectId highlight for draft-in-project sessions.",
|
"Drag-and-drop handlers for session-to-project moves and project reorder, plus activeProjectId highlight.",
|
||||||
},
|
},
|
||||||
"src/features/chat/ui/ChatView.tsx": {
|
"src/features/chat/ui/ChatView.tsx": {
|
||||||
limit: 535,
|
limit: 535,
|
||||||
@@ -50,6 +50,11 @@ const EXCEPTIONS = {
|
|||||||
justification:
|
justification:
|
||||||
"ACP-backed session overlay persistence, draft migration, and sidebar-facing session merge logic live together for now.",
|
"ACP-backed session overlay persistence, draft migration, and sidebar-facing session merge logic live together for now.",
|
||||||
},
|
},
|
||||||
|
"src-tauri/src/commands/projects.rs": {
|
||||||
|
limit: 520,
|
||||||
|
justification:
|
||||||
|
"Project CRUD plus reorder_projects command for sidebar drag-and-drop ordering.",
|
||||||
|
},
|
||||||
"src-tauri/src/services/acp/manager/dispatcher.rs": {
|
"src-tauri/src/services/acp/manager/dispatcher.rs": {
|
||||||
limit: 540,
|
limit: 540,
|
||||||
justification:
|
justification:
|
||||||
|
|||||||
@@ -384,6 +384,20 @@ pub fn delete_project(id: String) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn reorder_projects(order: Vec<(String, i32)>) -> Result<(), String> {
|
||||||
|
for (id, new_order) in order {
|
||||||
|
let (dir, mut stored) = find_project_by_id(&id)?;
|
||||||
|
stored.order = new_order;
|
||||||
|
let project_path = dir.join("project.json");
|
||||||
|
let json = serde_json::to_string_pretty(&stored)
|
||||||
|
.map_err(|e| format!("Failed to serialize project: {}", e))?;
|
||||||
|
fs::write(&project_path, json)
|
||||||
|
.map_err(|e| format!("Failed to write project.json: {}", e))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_project(id: String) -> Result<ProjectInfo, String> {
|
pub fn get_project(id: String) -> Result<ProjectInfo, String> {
|
||||||
let (dir, info) = find_project_by_id(&id)?;
|
let (dir, info) = find_project_by_id(&id)?;
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ pub fn run() {
|
|||||||
commands::projects::update_project,
|
commands::projects::update_project,
|
||||||
commands::projects::delete_project,
|
commands::projects::delete_project,
|
||||||
commands::projects::get_project,
|
commands::projects::get_project,
|
||||||
|
commands::projects::reorder_projects,
|
||||||
commands::projects::list_archived_projects,
|
commands::projects::list_archived_projects,
|
||||||
commands::projects::archive_project,
|
commands::projects::archive_project,
|
||||||
commands::projects::restore_project,
|
commands::projects::restore_project,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"visible": false,
|
"visible": false,
|
||||||
"titleBarStyle": "Overlay",
|
"titleBarStyle": "Overlay",
|
||||||
"hiddenTitle": true,
|
"hiddenTitle": true,
|
||||||
"dragDropEnabled": true,
|
"dragDropEnabled": false,
|
||||||
"trafficLightPosition": { "x": 12, "y": 22 }
|
"trafficLightPosition": { "x": 12, "y": 22 }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -553,6 +553,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
onArchiveChat={handleArchiveChat}
|
onArchiveChat={handleArchiveChat}
|
||||||
onRenameChat={handleRenameChat}
|
onRenameChat={handleRenameChat}
|
||||||
onMoveToProject={handleMoveToProject}
|
onMoveToProject={handleMoveToProject}
|
||||||
|
onReorderProject={projectStore.reorderProjects}
|
||||||
onSelectSession={handleSelectSession}
|
onSelectSession={handleSelectSession}
|
||||||
onSelectSearchResult={handleSelectSearchResult}
|
onSelectSearchResult={handleSelectSearchResult}
|
||||||
activeView={activeView}
|
activeView={activeView}
|
||||||
|
|||||||
@@ -88,6 +88,12 @@ export async function archiveProject(id: string): Promise<void> {
|
|||||||
return invoke("archive_project", { id });
|
return invoke("archive_project", { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function reorderProjects(
|
||||||
|
order: [string, number][],
|
||||||
|
): Promise<void> {
|
||||||
|
return invoke("reorder_projects", { order });
|
||||||
|
}
|
||||||
|
|
||||||
export async function restoreProject(id: string): Promise<void> {
|
export async function restoreProject(id: string): Promise<void> {
|
||||||
return invoke("restore_project", { id });
|
return invoke("restore_project", { id });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
createProject,
|
createProject,
|
||||||
updateProject,
|
updateProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
|
reorderProjects as apiReorderProjects,
|
||||||
type ProjectInfo,
|
type ProjectInfo,
|
||||||
} from "../api/projects";
|
} from "../api/projects";
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ interface ProjectState {
|
|||||||
useWorktrees: boolean,
|
useWorktrees: boolean,
|
||||||
) => Promise<ProjectInfo>;
|
) => Promise<ProjectInfo>;
|
||||||
removeProject: (id: string) => Promise<void>;
|
removeProject: (id: string) => Promise<void>;
|
||||||
|
reorderProjects: (fromId: string, toId: string) => void;
|
||||||
setActiveProject: (id: string | null) => void;
|
setActiveProject: (id: string | null) => void;
|
||||||
getActiveProject: () => ProjectInfo | null;
|
getActiveProject: () => ProjectInfo | null;
|
||||||
}
|
}
|
||||||
@@ -152,6 +154,28 @@ export const useProjectStore = create<ProjectState>((set, get) => ({
|
|||||||
persistProjects(get().projects);
|
persistProjects(get().projects);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reorderProjects: (fromId, toId) => {
|
||||||
|
set((state) => {
|
||||||
|
const projects = [...state.projects];
|
||||||
|
const fromIndex = projects.findIndex((p) => p.id === fromId);
|
||||||
|
const toIndex = projects.findIndex((p) => p.id === toId);
|
||||||
|
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex)
|
||||||
|
return state;
|
||||||
|
const [moved] = projects.splice(fromIndex, 1);
|
||||||
|
// When dragging down, removing the source shifts the target index
|
||||||
|
const insertAt = fromIndex < toIndex ? toIndex - 1 : toIndex;
|
||||||
|
projects.splice(insertAt, 0, moved);
|
||||||
|
// Update order fields so views sorting by .order stay consistent
|
||||||
|
for (let i = 0; i < projects.length; i++) {
|
||||||
|
projects[i] = { ...projects[i], order: i };
|
||||||
|
}
|
||||||
|
return { projects };
|
||||||
|
});
|
||||||
|
const projects = get().projects;
|
||||||
|
persistProjects(projects);
|
||||||
|
void apiReorderProjects(projects.map((p, i) => [p.id, i]));
|
||||||
|
},
|
||||||
|
|
||||||
setActiveProject: (id) => set({ activeProjectId: id }),
|
setActiveProject: (id) => set({ activeProjectId: id }),
|
||||||
|
|
||||||
getActiveProject: () => {
|
getActiveProject: () => {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface SidebarProps {
|
|||||||
onArchiveChat?: (sessionId: string) => void;
|
onArchiveChat?: (sessionId: string) => void;
|
||||||
onRenameChat?: (sessionId: string, nextTitle: string) => void;
|
onRenameChat?: (sessionId: string, nextTitle: string) => void;
|
||||||
onMoveToProject?: (sessionId: string, projectId: string | null) => void;
|
onMoveToProject?: (sessionId: string, projectId: string | null) => void;
|
||||||
|
onReorderProject?: (fromId: string, toId: string) => void;
|
||||||
onNavigate?: (view: AppView) => void;
|
onNavigate?: (view: AppView) => void;
|
||||||
onSelectSession?: (sessionId: string) => void;
|
onSelectSession?: (sessionId: string) => void;
|
||||||
onSelectSearchResult?: (
|
onSelectSearchResult?: (
|
||||||
@@ -62,6 +63,7 @@ export function Sidebar({
|
|||||||
onArchiveChat,
|
onArchiveChat,
|
||||||
onRenameChat,
|
onRenameChat,
|
||||||
onMoveToProject,
|
onMoveToProject,
|
||||||
|
onReorderProject,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
onSelectSession,
|
onSelectSession,
|
||||||
onSelectSearchResult,
|
onSelectSearchResult,
|
||||||
@@ -491,8 +493,6 @@ export function Sidebar({
|
|||||||
|
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<>
|
<>
|
||||||
<div className="relative z-10 my-2 -mx-1.5 bg-border h-px" />
|
|
||||||
|
|
||||||
{sidebarSearch.submittedQuery ? (
|
{sidebarSearch.submittedQuery ? (
|
||||||
<div className="relative z-10 space-y-2">
|
<div className="relative z-10 space-y-2">
|
||||||
{sidebarSearch.error && (
|
{sidebarSearch.error && (
|
||||||
@@ -550,6 +550,7 @@ export function Sidebar({
|
|||||||
onArchiveChat={onArchiveChat}
|
onArchiveChat={onArchiveChat}
|
||||||
onRenameChat={onRenameChat}
|
onRenameChat={onRenameChat}
|
||||||
onMoveToProject={onMoveToProject}
|
onMoveToProject={onMoveToProject}
|
||||||
|
onReorderProject={onReorderProject}
|
||||||
onItemMouseEnter={onItemMouseEnter}
|
onItemMouseEnter={onItemMouseEnter}
|
||||||
activeSessionRefCallback={activeSessionRefCallback}
|
activeSessionRefCallback={activeSessionRefCallback}
|
||||||
activeProjectRefCallback={activeProjectRefCallback}
|
activeProjectRefCallback={activeProjectRefCallback}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ interface SidebarProjectsSectionProps {
|
|||||||
onArchiveChat?: (sessionId: string) => void;
|
onArchiveChat?: (sessionId: string) => void;
|
||||||
onRenameChat?: (sessionId: string, nextTitle: string) => void;
|
onRenameChat?: (sessionId: string, nextTitle: string) => void;
|
||||||
onMoveToProject?: (sessionId: string, projectId: string | null) => void;
|
onMoveToProject?: (sessionId: string, projectId: string | null) => void;
|
||||||
|
onReorderProject?: (fromId: string, toId: string) => void;
|
||||||
onItemMouseEnter?: (e: React.MouseEvent<HTMLElement>) => void;
|
onItemMouseEnter?: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
activeSessionRefCallback?: (el: HTMLElement | null) => void;
|
activeSessionRefCallback?: (el: HTMLElement | null) => void;
|
||||||
activeProjectRefCallback?: (el: HTMLElement | null) => void;
|
activeProjectRefCallback?: (el: HTMLElement | null) => void;
|
||||||
@@ -117,9 +118,10 @@ function ProjectSection({
|
|||||||
const sessionId = e.dataTransfer.getData("text/x-session-id");
|
const sessionId = e.dataTransfer.getData("text/x-session-id");
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
onMoveToProject?.(sessionId, project.id);
|
onMoveToProject?.(sessionId, project.id);
|
||||||
|
if (!isExpanded) toggleProject(project.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onMoveToProject, project.id],
|
[onMoveToProject, project.id, isExpanded, toggleProject],
|
||||||
);
|
);
|
||||||
const visibleChats = projectChats.slice(
|
const visibleChats = projectChats.slice(
|
||||||
0,
|
0,
|
||||||
@@ -277,15 +279,21 @@ export function SidebarProjectsSection({
|
|||||||
onArchiveChat,
|
onArchiveChat,
|
||||||
onRenameChat,
|
onRenameChat,
|
||||||
onMoveToProject,
|
onMoveToProject,
|
||||||
|
onReorderProject,
|
||||||
onItemMouseEnter,
|
onItemMouseEnter,
|
||||||
activeSessionRefCallback,
|
activeSessionRefCallback,
|
||||||
activeProjectRefCallback,
|
activeProjectRefCallback,
|
||||||
}: SidebarProjectsSectionProps) {
|
}: SidebarProjectsSectionProps) {
|
||||||
const { t } = useTranslation(["sidebar", "common"]);
|
const { t } = useTranslation(["sidebar", "common"]);
|
||||||
const [recentsDragOver, setRecentsDragOver] = useState(false);
|
const [recentsDragOver, setRecentsDragOver] = useState(false);
|
||||||
|
const [draggedProjectId, setDraggedProjectId] = useState<string | null>(null);
|
||||||
|
const [dropTargetProjectId, setDropTargetProjectId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const handleRecentsDragOver = useCallback((e: React.DragEvent) => {
|
const handleRecentsDragOver = useCallback((e: React.DragEvent) => {
|
||||||
if (e.dataTransfer.types.includes("text/x-session-id")) {
|
const hasSession = e.dataTransfer.types.includes("text/x-session-id");
|
||||||
|
if (hasSession) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = "move";
|
e.dataTransfer.dropEffect = "move";
|
||||||
setRecentsDragOver(true);
|
setRecentsDragOver(true);
|
||||||
@@ -325,7 +333,7 @@ export function SidebarProjectsSection({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex items-center transition-all duration-300",
|
"group flex items-center transition-all duration-300",
|
||||||
collapsed ? "px-0 pt-0 pb-1 justify-center" : "pt-2 pb-1",
|
collapsed ? "px-0 pt-0 pb-1 justify-center" : "pt-4 pb-1",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -378,26 +386,76 @@ export function SidebarProjectsSection({
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<ProjectSection
|
// biome-ignore lint/a11y/noStaticElementInteractions: drag-and-drop reorder target
|
||||||
|
<div
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
draggable
|
||||||
projectChats={projectSessions.byProject[project.id] ?? []}
|
onDragStart={(e) => {
|
||||||
isExpanded={expandedProjects[project.id] ?? false}
|
// Skip if a child chat row already initiated a session drag
|
||||||
toggleProject={toggleProject}
|
if (e.dataTransfer.types.includes("text/x-session-id")) return;
|
||||||
activeSessionId={activeSessionId}
|
e.dataTransfer.setData("text/x-project-id", project.id);
|
||||||
activeProjectId={activeProjectId}
|
e.dataTransfer.effectAllowed = "move";
|
||||||
onSelectSession={onSelectSession}
|
setDraggedProjectId(project.id);
|
||||||
onNewChatInProject={onNewChatInProject}
|
}}
|
||||||
onNavigate={onNavigate}
|
onDragOver={(e) => {
|
||||||
onEditProject={onEditProject}
|
if (e.dataTransfer.types.includes("text/x-project-id")) {
|
||||||
onArchiveProject={onArchiveProject}
|
e.preventDefault();
|
||||||
onArchiveChat={onArchiveChat}
|
e.dataTransfer.dropEffect = "move";
|
||||||
onRenameChat={onRenameChat}
|
if (project.id !== draggedProjectId) {
|
||||||
onMoveToProject={onMoveToProject}
|
setDropTargetProjectId(project.id);
|
||||||
onItemMouseEnter={onItemMouseEnter}
|
}
|
||||||
activeSessionRefCallback={activeSessionRefCallback}
|
}
|
||||||
activeProjectRefCallback={activeProjectRefCallback}
|
}}
|
||||||
/>
|
onDragLeave={(e) => {
|
||||||
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||||
|
setDropTargetProjectId((prev) =>
|
||||||
|
prev === project.id ? null : prev,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDrop={(e) => {
|
||||||
|
const fromId = e.dataTransfer.getData("text/x-project-id");
|
||||||
|
if (fromId && fromId !== project.id) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onReorderProject?.(fromId, project.id);
|
||||||
|
}
|
||||||
|
setDraggedProjectId(null);
|
||||||
|
setDropTargetProjectId(null);
|
||||||
|
}}
|
||||||
|
onDragEnd={() => {
|
||||||
|
setDraggedProjectId(null);
|
||||||
|
setDropTargetProjectId(null);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"relative",
|
||||||
|
draggedProjectId === project.id && "opacity-40",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{dropTargetProjectId === project.id &&
|
||||||
|
draggedProjectId !== project.id && (
|
||||||
|
<div className="absolute top-0 left-3 right-3 h-0.5 rounded-full bg-foreground" />
|
||||||
|
)}
|
||||||
|
<ProjectSection
|
||||||
|
project={project}
|
||||||
|
projectChats={projectSessions.byProject[project.id] ?? []}
|
||||||
|
isExpanded={expandedProjects[project.id] ?? false}
|
||||||
|
toggleProject={toggleProject}
|
||||||
|
activeSessionId={activeSessionId}
|
||||||
|
activeProjectId={activeProjectId}
|
||||||
|
onSelectSession={onSelectSession}
|
||||||
|
onNewChatInProject={onNewChatInProject}
|
||||||
|
onNavigate={onNavigate}
|
||||||
|
onEditProject={onEditProject}
|
||||||
|
onArchiveProject={onArchiveProject}
|
||||||
|
onArchiveChat={onArchiveChat}
|
||||||
|
onRenameChat={onRenameChat}
|
||||||
|
onMoveToProject={onMoveToProject}
|
||||||
|
onItemMouseEnter={onItemMouseEnter}
|
||||||
|
activeSessionRefCallback={activeSessionRefCallback}
|
||||||
|
activeProjectRefCallback={activeProjectRefCallback}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -409,16 +467,10 @@ export function SidebarProjectsSection({
|
|||||||
onDragLeave={handleRecentsDragLeave}
|
onDragLeave={handleRecentsDragLeave}
|
||||||
onDrop={handleRecentsDrop}
|
onDrop={handleRecentsDrop}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"my-2 -mx-1.5 bg-border transition-all duration-300",
|
|
||||||
collapsed ? "w-5 mx-auto h-px" : "h-px",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative group flex items-center transition-all duration-300",
|
"relative group flex items-center transition-all duration-300",
|
||||||
collapsed ? "px-0 pt-0 pb-1 justify-center" : "pt-2 pb-1",
|
collapsed ? "px-0 pt-0 pb-1 justify-center" : "pt-4 pb-1",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"emptyTitle": "No sessions yet",
|
"emptyTitle": "No sessions yet",
|
||||||
"searchArchivedPlaceholder": "Search archived sessions...",
|
"searchArchivedPlaceholder": "Search archived sessions...",
|
||||||
"searchError": "Message search failed. Showing title, persona, and project matches only.",
|
"searchError": "Message search failed. Showing title, persona, and project matches only.",
|
||||||
"searchPlaceholder": "Search sessions by title, persona, project, or message content...",
|
"searchPlaceholder": "Search conversations",
|
||||||
"searching": "Searching sessions...",
|
"searching": "Searching sessions...",
|
||||||
"subtitle": "Browse and search past sessions",
|
"subtitle": "Browse and search past sessions",
|
||||||
"toggleArchived": "Archived",
|
"toggleArchived": "Archived",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"error": "Message search failed. Showing metadata matches only.",
|
"error": "Message search failed. Showing metadata matches only.",
|
||||||
"placeholder": "Search chats by title, persona, project, or message...",
|
"placeholder": "Search conversations",
|
||||||
"searching": "Searching chats..."
|
"searching": "Searching chats..."
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"emptyTitle": "Aún no hay sesiones",
|
"emptyTitle": "Aún no hay sesiones",
|
||||||
"searchArchivedPlaceholder": "Buscar sesiones archivadas...",
|
"searchArchivedPlaceholder": "Buscar sesiones archivadas...",
|
||||||
"searchError": "La búsqueda de mensajes falló. Mostrando solo coincidencias por título, persona y proyecto.",
|
"searchError": "La búsqueda de mensajes falló. Mostrando solo coincidencias por título, persona y proyecto.",
|
||||||
"searchPlaceholder": "Busca sesiones por título, persona, proyecto o contenido del mensaje...",
|
"searchPlaceholder": "Buscar conversaciones",
|
||||||
"searching": "Buscando sesiones...",
|
"searching": "Buscando sesiones...",
|
||||||
"subtitle": "Explora y busca sesiones anteriores",
|
"subtitle": "Explora y busca sesiones anteriores",
|
||||||
"toggleArchived": "Archivadas",
|
"toggleArchived": "Archivadas",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"error": "La búsqueda de mensajes falló. Mostrando solo coincidencias de metadatos.",
|
"error": "La búsqueda de mensajes falló. Mostrando solo coincidencias de metadatos.",
|
||||||
"placeholder": "Busca chats por título, persona, proyecto o mensaje...",
|
"placeholder": "Buscar conversaciones",
|
||||||
"searching": "Buscando chats..."
|
"searching": "Buscando chats..."
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
|
|||||||
Reference in New Issue
Block a user