mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Implement chat create and refactor sidebar query
This commit is contained in:
84
desktop/src/components/chats/chat-create-popover.tsx
Normal file
84
desktop/src/components/chats/chat-create-popover.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@/components/ui/icon';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command';
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import { useUserSearchQuery } from '@/queries/use-user-search-query';
|
||||
import { useChatCreateMutation } from '@/mutations/use-chat-create-mutation';
|
||||
import { useWorkspace } from '@/contexts/workspace';
|
||||
|
||||
export const ChatCreatePopover = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const { data } = useUserSearchQuery(query);
|
||||
const { mutate } = useChatCreateMutation();
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
||||
<PopoverTrigger asChild>
|
||||
<Icon name="add-line" className="mr-2 h-3 w-3 cursor-pointer" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-96 p-1">
|
||||
<Command className="min-h-min" shouldFilter={false}>
|
||||
<CommandInput
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
placeholder="Search users..."
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandEmpty>No user found.</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup className="h-min max-h-96">
|
||||
{data?.map((user) => (
|
||||
<CommandItem
|
||||
key={user.id}
|
||||
onSelect={() => {
|
||||
mutate(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
onSuccess: (id) => {
|
||||
workspace.navigateToNode(id);
|
||||
},
|
||||
},
|
||||
);
|
||||
setQuery('');
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center gap-2">
|
||||
<Avatar
|
||||
id={user.id}
|
||||
name={user.name}
|
||||
avatar={user.avatar}
|
||||
className="h-7 w-7"
|
||||
/>
|
||||
<div className="flex flex-grow flex-col">
|
||||
<p className="text-sm">{user.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
} from '@/components/ui/command';
|
||||
import { Icon } from '@/components/ui/icon';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import { useNodeCollaboratorSearchQuery } from '@/queries/use-node-collaborator-search-query';
|
||||
import { Avatar } from '../ui/avatar';
|
||||
|
||||
interface NodeCollaboratorSearchProps {
|
||||
excluded: string[];
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace } from '@/contexts/workspace';
|
||||
import { SidebarChatNode } from '@/types/workspaces';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import { Icon } from '@/components/ui/icon';
|
||||
|
||||
interface SidebarChatItemProps {
|
||||
node: SidebarChatNode;
|
||||
}
|
||||
|
||||
export const SidebarChatItem = ({
|
||||
node,
|
||||
}: SidebarChatItemProps): React.ReactNode => {
|
||||
const workspace = useWorkspace();
|
||||
const isActive = false;
|
||||
const isUnread = false;
|
||||
const directCount = 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={node.id}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center rounded-md p-1 text-sm text-foreground/80 hover:bg-gray-100',
|
||||
isActive && 'bg-gray-100',
|
||||
)}
|
||||
onClick={() => {
|
||||
workspace.navigateToNode(node.id);
|
||||
}}
|
||||
>
|
||||
<Avatar id={node.id} avatar={node.avatar} name={node.name} size="small" />
|
||||
<span
|
||||
className={cn(
|
||||
'line-clamp-1 w-full flex-grow pl-2 text-left',
|
||||
isUnread && 'font-bold',
|
||||
)}
|
||||
>
|
||||
{node.name ?? 'Unnamed'}
|
||||
</span>
|
||||
{directCount > 0 && (
|
||||
<span className="mr-1 rounded-md bg-red-500 px-1 py-0.5 text-xs text-white">
|
||||
{directCount}
|
||||
</span>
|
||||
)}
|
||||
{directCount == 0 && isUnread && (
|
||||
<Icon
|
||||
name="checkbox-blank-circle-fill"
|
||||
className="mr-2 h-3 w-3 p-0.5 text-red-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +1,20 @@
|
||||
import React from 'react';
|
||||
import { SidebarChatNode } from '@/types/workspaces';
|
||||
import { ChatCreatePopover } from '@/components/chats/chat-create-popover';
|
||||
import { useSidebarChatsQuery } from '@/queries/use-sidebar-chats-query';
|
||||
import { SidebarChatItem } from './sidebar-chat-item';
|
||||
|
||||
interface SidebarChatsProps {
|
||||
chats: SidebarChatNode[];
|
||||
}
|
||||
export const SidebarChats = () => {
|
||||
const { data } = useSidebarChatsQuery();
|
||||
|
||||
export const SidebarChats = ({ chats }: SidebarChatsProps) => {
|
||||
return (
|
||||
<div className="pt-2 first:pt-0">
|
||||
<div className="flex items-center justify-between p-1 pb-2 text-xs text-muted-foreground">
|
||||
<span>Chats</span>
|
||||
<ChatCreatePopover />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{data?.map((chat) => <SidebarChatItem key={chat.id} node={chat} />)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import React from 'react';
|
||||
import { SpaceCreateButton } from '@/components/spaces/space-create-button';
|
||||
import { SidebarSpaceNode } from '@/types/workspaces';
|
||||
import { SidebarSpaceItem } from '@/components/workspaces/sidebars/sidebar-space-item';
|
||||
import { useSidebarSpacesQuery } from '@/queries/use-sidebar-spaces-query';
|
||||
|
||||
interface SidebarSpacesProps {
|
||||
spaces: SidebarSpaceNode[];
|
||||
}
|
||||
|
||||
export const SidebarSpaces = ({ spaces }: SidebarSpacesProps) => {
|
||||
export const SidebarSpaces = () => {
|
||||
const { data } = useSidebarSpacesQuery();
|
||||
return (
|
||||
<div className="pt-2 first:pt-0">
|
||||
<div className="flex items-center justify-between p-1 pb-2 text-xs text-muted-foreground">
|
||||
@@ -15,9 +12,7 @@ export const SidebarSpaces = ({ spaces }: SidebarSpacesProps) => {
|
||||
<SpaceCreateButton />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{spaces.map((space) => (
|
||||
<SidebarSpaceItem node={space} key={space.id} />
|
||||
))}
|
||||
{data?.map((space) => <SidebarSpaceItem node={space} key={space.id} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,17 +2,9 @@ import React from 'react';
|
||||
import { SidebarHeader } from '@/components/workspaces/sidebars/sidebar-header';
|
||||
import { SidebarSpaces } from '@/components/workspaces/sidebars/sidebar-spaces';
|
||||
import { SidebarChats } from '@/components/workspaces/sidebars/sidebar-chats';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Icon } from '@/components/ui/icon';
|
||||
import { useSidebarQuery } from '@/queries/use-sidebar-query';
|
||||
|
||||
export const Sidebar = () => {
|
||||
const { data, isPending } = useSidebarQuery();
|
||||
|
||||
if (isPending) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full max-h-screen w-full border-r border-gray-200">
|
||||
<SidebarHeader />
|
||||
@@ -25,8 +17,8 @@ export const Sidebar = () => {
|
||||
<Icon name="inbox-line" className="mr-2 h-4 w-4" />
|
||||
<span>Inbox</span>
|
||||
</div>
|
||||
<SidebarChats chats={data?.chats ?? []} />
|
||||
<SidebarSpaces spaces={data?.spaces ?? []} />
|
||||
<SidebarChats />
|
||||
<SidebarSpaces />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ enum IdType {
|
||||
Space = 'sp',
|
||||
Page = 'pg',
|
||||
Channel = 'ch',
|
||||
Chat = 'ct',
|
||||
Node = 'nd',
|
||||
Message = 'ms',
|
||||
Subscriber = 'sb',
|
||||
|
||||
76
desktop/src/mutations/use-chat-create-mutation.tsx
Normal file
76
desktop/src/mutations/use-chat-create-mutation.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useWorkspace } from '@/contexts/workspace';
|
||||
import { NodeTypes } from '@/lib/constants';
|
||||
import { NeuronId } from '@/lib/id';
|
||||
import { buildNodeInsertMutation } from '@/lib/nodes';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
interface CreateChatInput {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const useChatCreateMutation = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateChatInput) => {
|
||||
const existingChats = workspace.schema
|
||||
.selectFrom('nodes')
|
||||
.where('type', '=', NodeTypes.Chat)
|
||||
.where(
|
||||
'id',
|
||||
'in',
|
||||
workspace.schema
|
||||
.selectFrom('node_collaborators')
|
||||
.select('node_id')
|
||||
.where('collaborator_id', 'in', [workspace.userId, input.userId])
|
||||
.groupBy('node_id')
|
||||
.having(workspace.schema.fn.count('collaborator_id'), '=', 2),
|
||||
)
|
||||
.selectAll()
|
||||
.compile();
|
||||
|
||||
const result = await workspace.query(existingChats);
|
||||
if (result.rows.length > 0) {
|
||||
const chat = result.rows[0];
|
||||
return chat.id;
|
||||
}
|
||||
|
||||
const id = NeuronId.generate(NeuronId.Type.Chat);
|
||||
const insertChatQuery = buildNodeInsertMutation(
|
||||
workspace.schema,
|
||||
workspace.userId,
|
||||
{
|
||||
id: id,
|
||||
attributes: {
|
||||
type: NodeTypes.Chat,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const insertCollaboratorsQuery = workspace.schema
|
||||
.insertInto('node_collaborators')
|
||||
.values([
|
||||
{
|
||||
node_id: id,
|
||||
collaborator_id: workspace.userId,
|
||||
role: 'owner',
|
||||
created_at: new Date().toISOString(),
|
||||
created_by: workspace.userId,
|
||||
version_id: NeuronId.generate(NeuronId.Type.Version),
|
||||
},
|
||||
{
|
||||
node_id: id,
|
||||
collaborator_id: input.userId,
|
||||
role: 'owner',
|
||||
created_at: new Date().toISOString(),
|
||||
created_by: workspace.userId,
|
||||
version_id: NeuronId.generate(NeuronId.Type.Version),
|
||||
},
|
||||
])
|
||||
.compile();
|
||||
|
||||
await workspace.mutate([insertChatQuery, insertCollaboratorsQuery]);
|
||||
return id;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { NodeTypes } from '@/lib/constants';
|
||||
import { buildNodeWithChildren, mapNode } from '@/lib/nodes';
|
||||
import { compareString } from '@/lib/utils';
|
||||
import { MessageNode, MessageReactionCount } from '@/types/messages';
|
||||
import { User } from '@/types/users';
|
||||
import { UserNode } from '@/types/users';
|
||||
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { QueryResult, sql } from 'kysely';
|
||||
|
||||
@@ -132,15 +132,17 @@ const buildMessages = (rows: MessageRow[]): MessageNode[] => {
|
||||
.map(mapNode);
|
||||
|
||||
const messages: MessageNode[] = [];
|
||||
const authorMap = new Map<string, User>();
|
||||
const authorMap = new Map<string, UserNode>();
|
||||
for (const authorRow of authorRows) {
|
||||
const authorNode = mapNode(authorRow);
|
||||
const name = authorNode.attributes.name;
|
||||
const email = authorNode.attributes.email;
|
||||
const avatar = authorNode.attributes.avatar;
|
||||
|
||||
authorMap.set(authorRow.id, {
|
||||
id: authorRow.id,
|
||||
name: name ?? 'Unknown User',
|
||||
email,
|
||||
avatar,
|
||||
});
|
||||
}
|
||||
@@ -163,6 +165,7 @@ const buildMessages = (rows: MessageRow[]): MessageNode[] => {
|
||||
author: author ?? {
|
||||
id: messageNode.createdBy,
|
||||
name: 'Unknown User',
|
||||
email: 'unknown@neuron.com',
|
||||
avatar: null,
|
||||
},
|
||||
content: children,
|
||||
|
||||
@@ -57,11 +57,13 @@ const buildRecord = (
|
||||
id: authorNode.id,
|
||||
name: authorNode.attributes.name,
|
||||
avatar: authorNode.attributes.avatar,
|
||||
email: authorNode.attributes.email,
|
||||
}
|
||||
: {
|
||||
id: recordNode.createdBy,
|
||||
name: 'Unknown User',
|
||||
avatar: null,
|
||||
email: 'unknown@neuron.com',
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
ViewFilter,
|
||||
ViewSort,
|
||||
} from '@/types/databases';
|
||||
import { User } from '@/types/users';
|
||||
import { UserNode } from '@/types/users';
|
||||
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import { QueryResult, sql } from 'kysely';
|
||||
@@ -113,15 +113,17 @@ const buildRecords = (rows: SelectNode[]): RecordNode[] => {
|
||||
|
||||
const authorNodes = nodes.filter((node) => node.type === NodeTypes.User);
|
||||
const records: RecordNode[] = [];
|
||||
const authorMap = new Map<string, User>();
|
||||
const authorMap = new Map<string, UserNode>();
|
||||
|
||||
for (const author of authorNodes) {
|
||||
const name = author.attributes.name;
|
||||
const avatar = author.attributes.avatar;
|
||||
const email = author.attributes.email;
|
||||
|
||||
authorMap.set(author.id, {
|
||||
id: author.id,
|
||||
name: name ?? 'Unknown User',
|
||||
email,
|
||||
avatar,
|
||||
});
|
||||
}
|
||||
@@ -139,6 +141,7 @@ const buildRecords = (rows: SelectNode[]): RecordNode[] => {
|
||||
createdBy: author ?? {
|
||||
id: node.createdBy,
|
||||
name: 'Unknown User',
|
||||
email: 'unknown@neuron.com',
|
||||
avatar: null,
|
||||
},
|
||||
versionId: node.versionId,
|
||||
|
||||
103
desktop/src/queries/use-sidebar-chats-query.tsx
Normal file
103
desktop/src/queries/use-sidebar-chats-query.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useWorkspace } from '@/contexts/workspace';
|
||||
import { SelectNode } from '@/electron/schemas/workspace';
|
||||
import { NodeTypes } from '@/lib/constants';
|
||||
import { mapNode } from '@/lib/nodes';
|
||||
import { SidebarChatNode } from '@/types/workspaces';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { QueryResult, sql } from 'kysely';
|
||||
|
||||
type ChatRow = SelectNode & {
|
||||
collaborators: string;
|
||||
};
|
||||
|
||||
export const useSidebarChatsQuery = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return useQuery<QueryResult<ChatRow>, Error, SidebarChatNode[], string[]>({
|
||||
queryKey: ['sidebar-chats', workspace.id],
|
||||
queryFn: async ({ queryKey }) => {
|
||||
const query = sql<ChatRow>`
|
||||
WITH chat_nodes AS (
|
||||
SELECT *
|
||||
FROM nodes
|
||||
WHERE parent_id IS NULL AND type = ${NodeTypes.Chat}
|
||||
),
|
||||
collaborator_nodes AS (
|
||||
SELECT *
|
||||
FROM nodes
|
||||
WHERE id IN
|
||||
(
|
||||
SELECT collaborator_id
|
||||
FROM node_collaborators
|
||||
WHERE collaborator_id != ${workspace.userId}
|
||||
AND node_id IN
|
||||
(
|
||||
SELECT id
|
||||
FROM chat_nodes
|
||||
)
|
||||
)
|
||||
),
|
||||
all_nodes AS (
|
||||
SELECT * FROM chat_nodes
|
||||
UNION ALL
|
||||
SELECT * FROM collaborator_nodes
|
||||
),
|
||||
chat_collaborators AS (
|
||||
SELECT
|
||||
nc.node_id,
|
||||
json_group_array(nc.collaborator_id) AS collaborators
|
||||
FROM node_collaborators nc
|
||||
WHERE nc.node_id IN (SELECT id FROM chat_nodes)
|
||||
AND nc.collaborator_id != ${workspace.userId}
|
||||
GROUP BY nc.node_id
|
||||
)
|
||||
SELECT
|
||||
n.*,
|
||||
COALESCE(cc.collaborators, json('[]')) AS collaborators
|
||||
FROM all_nodes n
|
||||
LEFT JOIN chat_collaborators cc ON n.id = cc.node_id
|
||||
`.compile(workspace.schema);
|
||||
|
||||
return await workspace.queryAndSubscribe({
|
||||
key: queryKey,
|
||||
query,
|
||||
});
|
||||
},
|
||||
select: (data: QueryResult<ChatRow>): SidebarChatNode[] => {
|
||||
const rows = data?.rows ?? [];
|
||||
return buildSidebarSpaceNodes(rows);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const buildSidebarSpaceNodes = (rows: ChatRow[]): SidebarChatNode[] => {
|
||||
const chats: SidebarChatNode[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.type !== NodeTypes.Chat) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const chatNode = mapNode(row);
|
||||
const collaboratorIds = JSON.parse(row.collaborators) as string[];
|
||||
if (!collaboratorIds || collaboratorIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const collaboratorId = collaboratorIds[0];
|
||||
const collaboratorRow = rows.find((r) => r.id === collaboratorId);
|
||||
if (!collaboratorRow) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const collaboratorNode = mapNode(collaboratorRow);
|
||||
chats.push({
|
||||
id: chatNode.id,
|
||||
type: chatNode.type,
|
||||
name: collaboratorNode.attributes.name,
|
||||
avatar: chatNode.attributes.avatar,
|
||||
});
|
||||
}
|
||||
|
||||
return chats;
|
||||
};
|
||||
@@ -3,25 +3,16 @@ import { SelectNode } from '@/electron/schemas/workspace';
|
||||
import { NodeTypes } from '@/lib/constants';
|
||||
import { mapNode } from '@/lib/nodes';
|
||||
import { LocalNode } from '@/types/nodes';
|
||||
import {
|
||||
SidebarChatNode,
|
||||
SidebarNode,
|
||||
SidebarSpaceNode,
|
||||
} from '@/types/workspaces';
|
||||
import { SidebarNode, SidebarSpaceNode } from '@/types/workspaces';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { QueryResult, sql } from 'kysely';
|
||||
|
||||
export type SidebarQueryResult = {
|
||||
spaces: SidebarSpaceNode[];
|
||||
chats: SidebarChatNode[];
|
||||
};
|
||||
|
||||
export const useSidebarQuery = () => {
|
||||
export const useSidebarSpacesQuery = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return useQuery<QueryResult<SelectNode>, Error, SidebarQueryResult, string[]>(
|
||||
return useQuery<QueryResult<SelectNode>, Error, SidebarSpaceNode[], string[]>(
|
||||
{
|
||||
queryKey: ['sidebar', workspace.id],
|
||||
queryKey: ['sidebar-spaces', workspace.id],
|
||||
queryFn: async ({ queryKey }) => {
|
||||
const query = sql<SelectNode>`
|
||||
WITH space_nodes AS (
|
||||
@@ -33,17 +24,10 @@ export const useSidebarQuery = () => {
|
||||
SELECT *
|
||||
FROM nodes
|
||||
WHERE parent_id IN (SELECT id FROM space_nodes)
|
||||
),
|
||||
chat_nodes AS (
|
||||
SELECT *
|
||||
FROM nodes
|
||||
WHERE parent_id IS NULL AND type = ${NodeTypes.Chat}
|
||||
)
|
||||
SELECT * FROM space_nodes
|
||||
UNION ALL
|
||||
SELECT * FROM space_children_nodes
|
||||
UNION ALL
|
||||
SELECT * FROM chat_nodes;
|
||||
SELECT * FROM space_children_nodes;
|
||||
`.compile(workspace.schema);
|
||||
|
||||
return await workspace.queryAndSubscribe({
|
||||
@@ -51,33 +35,28 @@ export const useSidebarQuery = () => {
|
||||
query,
|
||||
});
|
||||
},
|
||||
select: (data: QueryResult<SelectNode>): SidebarQueryResult => {
|
||||
select: (data: QueryResult<SelectNode>): SidebarSpaceNode[] => {
|
||||
const rows = data?.rows ?? [];
|
||||
return buildSidebarNodes(rows);
|
||||
return buildSidebarSpaceNodes(rows);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const buildSidebarNodes = (rows: SelectNode[]): SidebarQueryResult => {
|
||||
const buildSidebarSpaceNodes = (rows: SelectNode[]): SidebarSpaceNode[] => {
|
||||
const nodes: LocalNode[] = rows.map(mapNode);
|
||||
|
||||
const spaces: SidebarSpaceNode[] = [];
|
||||
const chats: SidebarChatNode[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.type === NodeTypes.Space) {
|
||||
const children = nodes.filter((n) => n.parentId === node.id);
|
||||
spaces.push(buildSpaceNode(node, children));
|
||||
} else if (node.type === NodeTypes.Chat) {
|
||||
chats.push(buildChatNode(node));
|
||||
if (node.type !== NodeTypes.Space) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const children = nodes.filter((n) => n.parentId === node.id);
|
||||
spaces.push(buildSpaceNode(node, children));
|
||||
}
|
||||
|
||||
return {
|
||||
spaces,
|
||||
chats,
|
||||
};
|
||||
return spaces;
|
||||
};
|
||||
|
||||
const buildSpaceNode = (
|
||||
@@ -101,12 +80,3 @@ const buildSidearNode = (node: LocalNode): SidebarNode => {
|
||||
avatar: node.attributes.avatar ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
const buildChatNode = (node: LocalNode): SidebarChatNode => {
|
||||
return {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
name: node.attributes.name ?? null,
|
||||
avatar: node.attributes.avatar ?? null,
|
||||
};
|
||||
};
|
||||
42
desktop/src/queries/use-user-search-query.tsx
Normal file
42
desktop/src/queries/use-user-search-query.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useWorkspace } from '@/contexts/workspace';
|
||||
import { SelectNode } from '@/electron/schemas/workspace';
|
||||
import { NodeTypes } from '@/lib/constants';
|
||||
import { UserNode } from '@/types/users';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { QueryResult, sql } from 'kysely';
|
||||
|
||||
export const useUserSearchQuery = (searchQuery: string) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return useQuery<QueryResult<SelectNode>, Error, UserNode[], string[]>({
|
||||
queryKey: ['user-search', searchQuery],
|
||||
enabled: searchQuery.length > 0,
|
||||
queryFn: async ({ queryKey }) => {
|
||||
const query = sql<SelectNode>`
|
||||
SELECT n.*
|
||||
FROM nodes n
|
||||
JOIN node_names nn ON n.id = nn.id
|
||||
WHERE n.type = ${NodeTypes.User}
|
||||
AND n.id != ${workspace.userId}
|
||||
AND nn.name MATCH ${searchQuery + '*'}
|
||||
`.compile(workspace.schema);
|
||||
|
||||
return await workspace.queryAndSubscribe({
|
||||
key: queryKey,
|
||||
query,
|
||||
});
|
||||
},
|
||||
select: (data: QueryResult<SelectNode>): UserNode[] => {
|
||||
const rows = data?.rows ?? [];
|
||||
return rows.map((row) => {
|
||||
const attributes = JSON.parse(row.attributes);
|
||||
return {
|
||||
id: row.id,
|
||||
name: attributes.name,
|
||||
email: attributes.email,
|
||||
avatar: attributes.avatar,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { User } from '@/types/users';
|
||||
import { UserNode } from '@/types/users';
|
||||
|
||||
export type DatabaseNode = {
|
||||
id: string;
|
||||
@@ -173,7 +173,7 @@ export type RecordNode = {
|
||||
parentId: string;
|
||||
index: string;
|
||||
createdAt: Date;
|
||||
createdBy: User;
|
||||
createdBy: UserNode;
|
||||
versionId: string;
|
||||
|
||||
attributes: any;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { LocalNodeWithChildren } from '@/types/nodes';
|
||||
import { User } from '@/types/users';
|
||||
import { UserNode } from '@/types/users';
|
||||
|
||||
export type MessageNode = {
|
||||
id: string;
|
||||
content: LocalNodeWithChildren[];
|
||||
createdAt: string;
|
||||
author: User;
|
||||
author: UserNode;
|
||||
reactionCounts: MessageReactionCount[];
|
||||
userReactions: string[];
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export type User = {
|
||||
export type UserNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
avatar: string | null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user