diff --git a/desktop/src/components/chats/chat-create-popover.tsx b/desktop/src/components/chats/chat-create-popover.tsx
new file mode 100644
index 00000000..c592dd26
--- /dev/null
+++ b/desktop/src/components/chats/chat-create-popover.tsx
@@ -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 (
+
+
+
+
+
+
+
+ No user found.
+
+
+ {data?.map((user) => (
+ {
+ mutate(
+ {
+ userId: user.id,
+ },
+ {
+ onSuccess: (id) => {
+ workspace.navigateToNode(id);
+ },
+ },
+ );
+ setQuery('');
+ }}
+ >
+
+
+
+
{user.name}
+
+ {user.email}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/desktop/src/components/collaborators/node-collaborator-search.tsx b/desktop/src/components/collaborators/node-collaborator-search.tsx
index d499aa5e..73bf04a6 100644
--- a/desktop/src/components/collaborators/node-collaborator-search.tsx
+++ b/desktop/src/components/collaborators/node-collaborator-search.tsx
@@ -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[];
diff --git a/desktop/src/components/workspaces/sidebars/sidebar-chat-item.tsx b/desktop/src/components/workspaces/sidebars/sidebar-chat-item.tsx
new file mode 100644
index 00000000..0fe2a903
--- /dev/null
+++ b/desktop/src/components/workspaces/sidebars/sidebar-chat-item.tsx
@@ -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 (
+
{
+ workspace.navigateToNode(node.id);
+ }}
+ >
+
+
+ {node.name ?? 'Unnamed'}
+
+ {directCount > 0 && (
+
+ {directCount}
+
+ )}
+ {directCount == 0 && isUnread && (
+
+ )}
+
+ );
+};
diff --git a/desktop/src/components/workspaces/sidebars/sidebar-chats.tsx b/desktop/src/components/workspaces/sidebars/sidebar-chats.tsx
index 4008b079..be88a5bb 100644
--- a/desktop/src/components/workspaces/sidebars/sidebar-chats.tsx
+++ b/desktop/src/components/workspaces/sidebars/sidebar-chats.tsx
@@ -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 (
Chats
+
+
+
+ {data?.map((chat) => )}
-
);
};
diff --git a/desktop/src/components/workspaces/sidebars/sidebar-spaces.tsx b/desktop/src/components/workspaces/sidebars/sidebar-spaces.tsx
index 151cfa41..0541fc37 100644
--- a/desktop/src/components/workspaces/sidebars/sidebar-spaces.tsx
+++ b/desktop/src/components/workspaces/sidebars/sidebar-spaces.tsx
@@ -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 (
@@ -15,9 +12,7 @@ export const SidebarSpaces = ({ spaces }: SidebarSpacesProps) => {
- {spaces.map((space) => (
-
- ))}
+ {data?.map((space) => )}
);
diff --git a/desktop/src/components/workspaces/sidebars/sidebar.tsx b/desktop/src/components/workspaces/sidebars/sidebar.tsx
index 148694ad..8b9f3363 100644
--- a/desktop/src/components/workspaces/sidebars/sidebar.tsx
+++ b/desktop/src/components/workspaces/sidebars/sidebar.tsx
@@ -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 ;
- }
-
return (
@@ -25,8 +17,8 @@ export const Sidebar = () => {
Inbox
-
-
+
+
);
diff --git a/desktop/src/lib/id.ts b/desktop/src/lib/id.ts
index f8f4ced1..c587ba92 100644
--- a/desktop/src/lib/id.ts
+++ b/desktop/src/lib/id.ts
@@ -12,6 +12,7 @@ enum IdType {
Space = 'sp',
Page = 'pg',
Channel = 'ch',
+ Chat = 'ct',
Node = 'nd',
Message = 'ms',
Subscriber = 'sb',
diff --git a/desktop/src/mutations/use-chat-create-mutation.tsx b/desktop/src/mutations/use-chat-create-mutation.tsx
new file mode 100644
index 00000000..1d091e6f
--- /dev/null
+++ b/desktop/src/mutations/use-chat-create-mutation.tsx
@@ -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;
+ },
+ });
+};
diff --git a/desktop/src/queries/use-messages-query.tsx b/desktop/src/queries/use-messages-query.tsx
index 9542bfe6..3c1b879a 100644
--- a/desktop/src/queries/use-messages-query.tsx
+++ b/desktop/src/queries/use-messages-query.tsx
@@ -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();
+ const authorMap = new Map();
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,
diff --git a/desktop/src/queries/use-record-query.tsx b/desktop/src/queries/use-record-query.tsx
index 030222ef..63e4173b 100644
--- a/desktop/src/queries/use-record-query.tsx
+++ b/desktop/src/queries/use-record-query.tsx
@@ -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 {
diff --git a/desktop/src/queries/use-records-query.tsx b/desktop/src/queries/use-records-query.tsx
index e7066482..62eb56c3 100644
--- a/desktop/src/queries/use-records-query.tsx
+++ b/desktop/src/queries/use-records-query.tsx
@@ -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();
+ const authorMap = new Map();
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,
diff --git a/desktop/src/queries/use-sidebar-chats-query.tsx b/desktop/src/queries/use-sidebar-chats-query.tsx
new file mode 100644
index 00000000..b806ad9c
--- /dev/null
+++ b/desktop/src/queries/use-sidebar-chats-query.tsx
@@ -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, Error, SidebarChatNode[], string[]>({
+ queryKey: ['sidebar-chats', workspace.id],
+ queryFn: async ({ queryKey }) => {
+ const query = sql`
+ 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): 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;
+};
diff --git a/desktop/src/queries/use-sidebar-query.tsx b/desktop/src/queries/use-sidebar-spaces-query.tsx
similarity index 55%
rename from desktop/src/queries/use-sidebar-query.tsx
rename to desktop/src/queries/use-sidebar-spaces-query.tsx
index 62968d5c..c2f30deb 100644
--- a/desktop/src/queries/use-sidebar-query.tsx
+++ b/desktop/src/queries/use-sidebar-spaces-query.tsx
@@ -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, Error, SidebarQueryResult, string[]>(
+ return useQuery, Error, SidebarSpaceNode[], string[]>(
{
- queryKey: ['sidebar', workspace.id],
+ queryKey: ['sidebar-spaces', workspace.id],
queryFn: async ({ queryKey }) => {
const query = sql`
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): SidebarQueryResult => {
+ select: (data: QueryResult): 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,
- };
-};
diff --git a/desktop/src/queries/use-user-search-query.tsx b/desktop/src/queries/use-user-search-query.tsx
new file mode 100644
index 00000000..acb45483
--- /dev/null
+++ b/desktop/src/queries/use-user-search-query.tsx
@@ -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, Error, UserNode[], string[]>({
+ queryKey: ['user-search', searchQuery],
+ enabled: searchQuery.length > 0,
+ queryFn: async ({ queryKey }) => {
+ const query = sql`
+ 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): 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,
+ };
+ });
+ },
+ });
+};
diff --git a/desktop/src/types/databases.ts b/desktop/src/types/databases.ts
index 388bfe51..605992a9 100644
--- a/desktop/src/types/databases.ts
+++ b/desktop/src/types/databases.ts
@@ -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;
diff --git a/desktop/src/types/messages.ts b/desktop/src/types/messages.ts
index 5c4ea402..cef2eed2 100644
--- a/desktop/src/types/messages.ts
+++ b/desktop/src/types/messages.ts
@@ -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[];
};
diff --git a/desktop/src/types/users.ts b/desktop/src/types/users.ts
index fbfc64c2..01aa5c05 100644
--- a/desktop/src/types/users.ts
+++ b/desktop/src/types/users.ts
@@ -1,5 +1,6 @@
-export type User = {
+export type UserNode = {
id: string;
name: string;
- avatar: string;
+ email: string;
+ avatar: string | null;
};