deepspace 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +7 -6
  3. package/dist/{chunk-2GUGZLAX.js → chunk-YBXIUDZJ.js} +2 -2
  4. package/dist/{chunk-2GUGZLAX.js.map → chunk-YBXIUDZJ.js.map} +1 -1
  5. package/dist/cli.js +1036 -966
  6. package/dist/cli.js.map +1 -1
  7. package/dist/documentation-client-core.js +1 -1
  8. package/dist/documentation-default-renderer.cjs +1 -1
  9. package/dist/documentation-react.js +1 -1
  10. package/dist/documentation-runtime.js +1 -1
  11. package/dist/documentation-server-core.js +1 -1
  12. package/dist/documentation.js +1 -1
  13. package/dist/documentation.js.map +1 -1
  14. package/dist/index.d.ts +26 -434
  15. package/dist/index.js +99 -864
  16. package/dist/index.js.map +1 -1
  17. package/dist/schema.d.ts +4 -41
  18. package/dist/schema.js +2 -242
  19. package/dist/schema.js.map +1 -1
  20. package/dist/server.d.ts +6 -162
  21. package/dist/server.js +13 -281
  22. package/dist/server.js.map +1 -1
  23. package/dist/worker.d.ts +4 -160
  24. package/dist/worker.js +15 -281
  25. package/dist/worker.js.map +1 -1
  26. package/features/messaging/feature.json +3 -43
  27. package/features/messaging/src/ChatPage.tsx +12 -16
  28. package/features/messaging/src/components/chat/ChatHeader.tsx +5 -13
  29. package/features/messaging/src/components/chat/MessageItem.tsx +7 -9
  30. package/features/messaging/src/components/chat/MessageList.tsx +57 -40
  31. package/features/messaging/src/components/chat/ThreadPanel.tsx +25 -11
  32. package/features/messaging/src/components/chat/UserProfilePopover.tsx +10 -29
  33. package/features/messaging/src/hooks/useChatChannel.ts +18 -23
  34. package/features/messaging/src/messaging-schema.ts +0 -2
  35. package/features/search-bar/feature.json +9 -20
  36. package/features/search-bar/src/InlineSearch.tsx +101 -0
  37. package/features/search-bar/src/__tests__/search-model.test.tsx +38 -0
  38. package/features/search-bar/src/search-model.ts +2 -149
  39. package/package.json +1 -1
  40. package/features/messaging/src/ChatMultiPage.tsx +0 -298
  41. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +0 -188
  42. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +0 -163
  43. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +0 -147
  44. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +0 -344
  45. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +0 -460
  46. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +0 -190
  47. package/features/messaging/src/components/chat-multi/InvitationList.tsx +0 -149
  48. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +0 -159
  49. package/features/messaging/src/hooks/useMultiChannel.ts +0 -142
  50. package/features/search-bar/src/SearchOverlay.tsx +0 -566
  51. package/features/search-bar/src/__tests__/search-model.test.ts +0 -40
  52. package/features/sidebar/feature.json +0 -19
  53. package/features/sidebar/src/AppSidebar.tsx +0 -558
@@ -1,149 +0,0 @@
1
- /**
2
- * InvitationList -- Inline list of pending group invitations.
3
- * Shown in the sidebar when the user has pending invitations.
4
- * Accept joins the channel + updates invitation status.
5
- * Decline updates the invitation status to 'declined'.
6
- */
7
-
8
- import { useState, useCallback } from 'react'
9
- import { useChannelMembers } from 'deepspace'
10
- import { useMutations } from 'deepspace'
11
- import { useUserLookup } from 'deepspace'
12
- import type { Channel, ChannelInvitation } from 'deepspace'
13
- import type { RecordData } from 'deepspace'
14
-
15
- interface PendingInvitation {
16
- invitation: RecordData<ChannelInvitation>
17
- channel: RecordData<Channel> | undefined
18
- }
19
-
20
- interface InvitationListProps {
21
- invitations: PendingInvitation[]
22
- onAccepted: (channelId: string) => void
23
- }
24
-
25
- export function InvitationList({ invitations, onAccepted }: InvitationListProps) {
26
- if (invitations.length === 0) return null
27
-
28
- return (
29
- <div className="mb-2">
30
- <div className="flex items-center gap-1.5 px-2 py-2">
31
- <svg
32
- className="w-3.5 h-3.5 text-warning"
33
- fill="none"
34
- viewBox="0 0 24 24"
35
- stroke="currentColor"
36
- strokeWidth={2}
37
- >
38
- <path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
39
- </svg>
40
- <span className="text-[11px] font-bold text-muted-foreground/70 uppercase tracking-widest">
41
- Invitations
42
- </span>
43
- <span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-warning/15 text-warning text-[10px] font-bold tabular-nums">
44
- {invitations.length}
45
- </span>
46
- </div>
47
- <div className="space-y-1.5 px-0.5">
48
- {invitations.map(({ invitation, channel }) => (
49
- <InvitationRow
50
- key={invitation.recordId}
51
- invitation={invitation}
52
- channel={channel}
53
- onAccepted={onAccepted}
54
- />
55
- ))}
56
- </div>
57
- </div>
58
- )
59
- }
60
-
61
- function InvitationRow({
62
- invitation,
63
- channel,
64
- onAccepted,
65
- }: {
66
- invitation: RecordData<ChannelInvitation>
67
- channel: RecordData<Channel> | undefined
68
- onAccepted: (channelId: string) => void
69
- }) {
70
- const { join } = useChannelMembers(invitation.data.channelId)
71
- const { put } = useMutations<ChannelInvitation>('channel-invitations')
72
- const { getUser } = useUserLookup()
73
- const [acting, setActing] = useState<'accept' | 'decline' | null>(null)
74
-
75
- const inviter = getUser(invitation.data.invitedBy)
76
- const channelName = channel?.data.name ?? 'Unknown group'
77
- const isPrivate = channel?.data.type === 'private'
78
-
79
- const handleAccept = useCallback(async () => {
80
- setActing('accept')
81
- try {
82
- await put(invitation.recordId, {
83
- ...invitation.data,
84
- status: 'accepted',
85
- } as unknown as ChannelInvitation)
86
- await join()
87
- onAccepted(invitation.data.channelId)
88
- } finally {
89
- setActing(null)
90
- }
91
- }, [invitation, put, join, onAccepted])
92
-
93
- const handleDecline = useCallback(async () => {
94
- setActing('decline')
95
- try {
96
- await put(invitation.recordId, {
97
- ...invitation.data,
98
- status: 'declined',
99
- } as unknown as ChannelInvitation)
100
- } finally {
101
- setActing(null)
102
- }
103
- }, [invitation, put])
104
-
105
- return (
106
- <div
107
- data-testid={`invitation-${invitation.recordId}`}
108
- className="px-3 py-3 rounded-xl bg-warning/5 border border-warning/10"
109
- >
110
- <div className="flex items-center gap-2.5 mb-2">
111
- <span className="shrink-0 w-8 h-8 rounded-lg bg-warning/10 flex items-center justify-center">
112
- {isPrivate ? (
113
- <svg className="w-3.5 h-3.5 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
114
- <path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
115
- </svg>
116
- ) : (
117
- <svg className="w-3.5 h-3.5 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
118
- <path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
119
- </svg>
120
- )}
121
- </span>
122
- <div className="flex-1 min-w-0">
123
- <p className="text-sm font-semibold text-foreground truncate">{channelName}</p>
124
- <p className="text-[11px] text-muted-foreground/70">
125
- from {inviter?.name ?? 'someone'}
126
- </p>
127
- </div>
128
- </div>
129
- <div className="flex gap-2">
130
- <button
131
- data-testid={`accept-invitation-${invitation.recordId}`}
132
- onClick={handleAccept}
133
- disabled={acting !== null}
134
- className="flex-1 px-3 py-1.5 text-xs font-semibold bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 active:bg-primary/80 disabled:opacity-40 transition-colors duration-200 cursor-pointer"
135
- >
136
- {acting === 'accept' ? 'Joining...' : 'Accept'}
137
- </button>
138
- <button
139
- data-testid={`decline-invitation-${invitation.recordId}`}
140
- onClick={handleDecline}
141
- disabled={acting !== null}
142
- className="flex-1 px-3 py-1.5 text-xs font-semibold text-muted-foreground border border-border/60 rounded-lg hover:bg-muted/50 active:bg-muted disabled:opacity-40 transition-colors duration-200 cursor-pointer"
143
- >
144
- {acting === 'decline' ? 'Declining...' : 'Decline'}
145
- </button>
146
- </div>
147
- </div>
148
- )
149
- }
@@ -1,159 +0,0 @@
1
- /**
2
- * NewMessageModal -- Start a direct message with another user.
3
- * Finds existing DM channels to avoid duplicates.
4
- */
5
-
6
- import { useState, useCallback, useEffect } from 'react'
7
- import { useUser } from 'deepspace'
8
- import { useUsers } from 'deepspace'
9
- import { useChannels } from 'deepspace'
10
- import { useMutations } from 'deepspace'
11
- import type { Channel, ChannelMember } from 'deepspace'
12
- import type { RecordData } from 'deepspace'
13
-
14
- interface NewMessageModalProps {
15
- onClose: () => void
16
- onSelected: (channelId: string) => void
17
- }
18
-
19
- export function NewMessageModal({ onClose, onSelected }: NewMessageModalProps) {
20
- const { user } = useUser()
21
- const { users } = useUsers()
22
- const { channels, create } = useChannels()
23
- const { create: createMembership } = useMutations<ChannelMember>('channel-members')
24
- const [search, setSearch] = useState('')
25
- const [isCreating, setIsCreating] = useState(false)
26
-
27
- useEffect(() => {
28
- function handleEscape(e: KeyboardEvent) {
29
- if (e.key === 'Escape') onClose()
30
- }
31
- document.addEventListener('keydown', handleEscape)
32
- return () => document.removeEventListener('keydown', handleEscape)
33
- }, [onClose])
34
-
35
- const filteredUsers = users.filter(
36
- (u) =>
37
- u.id !== user?.id &&
38
- (u.name?.toLowerCase().includes(search.toLowerCase()) ||
39
- u.email?.toLowerCase().includes(search.toLowerCase()))
40
- )
41
-
42
- const handleSelectUser = useCallback(
43
- async (targetUserId: string, targetName: string) => {
44
- if (!user || isCreating) return
45
-
46
- const ids = [user.id, targetUserId].sort()
47
- const dmChannelName = `dm-${ids[0]}-${ids[1]}`
48
-
49
- const existingDM = channels.find(
50
- (c: RecordData<Channel>) => c.data.type === 'dm' && c.data.name === dmChannelName
51
- )
52
-
53
- if (existingDM) {
54
- onSelected(existingDM.recordId)
55
- onClose()
56
- return
57
- }
58
-
59
- setIsCreating(true)
60
- try {
61
- const channelId = await create({
62
- name: dmChannelName,
63
- type: 'dm',
64
- description: targetName,
65
- })
66
- if (channelId && user) {
67
- await createMembership({
68
- channelId,
69
- userId: user.id,
70
- joinedAt: new Date().toISOString(),
71
- } as unknown as ChannelMember)
72
- onSelected(channelId)
73
- }
74
- onClose()
75
- } finally {
76
- setIsCreating(false)
77
- }
78
- },
79
- [user, isCreating, channels, create, createMembership, onSelected, onClose]
80
- )
81
-
82
- return (
83
- <>
84
- <div className="fixed inset-0 bg-black/40 z-50 animate-in fade-in duration-150" onClick={onClose} />
85
- <div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={onClose}>
86
- <div
87
- data-testid="new-message-modal"
88
- className="bg-card border border-border rounded-2xl shadow-2xl w-full max-w-md animate-in fade-in zoom-in-95 duration-200"
89
- onClick={(e) => e.stopPropagation()}
90
- >
91
- <div className="px-6 pt-6 pb-0">
92
- <h2 className="text-lg font-semibold text-foreground">New Message</h2>
93
- <p className="text-sm text-muted-foreground mt-1">Send a direct message to someone.</p>
94
- </div>
95
-
96
- <div className="px-6 pt-4 pb-2">
97
- <div className="relative">
98
- <svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
99
- <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
100
- </svg>
101
- <input
102
- data-testid="dm-search-input"
103
- type="text"
104
- value={search}
105
- onChange={(e) => setSearch(e.target.value)}
106
- placeholder="Search by name or email..."
107
- className="w-full bg-background border border-border rounded-lg pl-9 pr-3.5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-shadow"
108
- autoFocus
109
- />
110
- </div>
111
- </div>
112
-
113
- <div className="px-3 pb-3 max-h-64 overflow-y-auto">
114
- {filteredUsers.length === 0 ? (
115
- <p className="text-sm text-muted-foreground text-center py-8">
116
- {search ? 'No users found' : 'No other users available'}
117
- </p>
118
- ) : (
119
- <div className="py-1 space-y-0.5">
120
- {filteredUsers.map((u) => (
121
- <button
122
- key={u.id}
123
- data-testid={`dm-user-${u.id}`}
124
- onClick={() => handleSelectUser(u.id, u.name)}
125
- disabled={isCreating}
126
- className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-muted/60 text-left transition-colors disabled:opacity-50"
127
- >
128
- {u.imageUrl ? (
129
- <img src={u.imageUrl} alt="" className="w-9 h-9 rounded-full object-cover" />
130
- ) : (
131
- <div className="w-9 h-9 rounded-full bg-primary/15 flex items-center justify-center text-sm font-semibold text-primary">
132
- {u.name?.[0]?.toUpperCase() ?? '?'}
133
- </div>
134
- )}
135
- <div className="min-w-0 flex-1">
136
- <div className="text-sm font-medium text-foreground truncate">{u.name}</div>
137
- {u.email && (
138
- <div className="text-xs text-muted-foreground truncate">{u.email}</div>
139
- )}
140
- </div>
141
- </button>
142
- ))}
143
- </div>
144
- )}
145
- </div>
146
-
147
- <div className="px-6 pb-6 pt-2 border-t border-border">
148
- <button
149
- onClick={onClose}
150
- className="w-full px-4 py-2 text-sm text-muted-foreground hover:text-foreground rounded-lg border border-border hover:bg-muted transition-colors"
151
- >
152
- Cancel
153
- </button>
154
- </div>
155
- </div>
156
- </div>
157
- </>
158
- )
159
- }
@@ -1,142 +0,0 @@
1
- /**
2
- * useMultiChannel — Multi-channel state management.
3
- *
4
- * Manages the active channel selection. Provides channel lists filtered
5
- * by type and helpers for DM display names and unread.
6
- *
7
- * DM visibility: DM channels use a `dm-{id1}-{id2}` naming convention.
8
- * A DM appears in a user's sidebar if their ID is embedded in the channel
9
- * name, regardless of whether a membership record exists yet.
10
- */
11
-
12
- import { useState, useCallback, useMemo } from 'react'
13
- import { useUser } from 'deepspace'
14
- import { useUsers } from 'deepspace'
15
- import { useChannels } from 'deepspace'
16
- import { useReadReceipts } from 'deepspace'
17
- import { useQuery } from 'deepspace'
18
- import type { Channel, ChannelMember, ChannelInvitation } from 'deepspace'
19
- import type { RecordData } from 'deepspace'
20
-
21
- const CHANNEL_TYPES = {
22
- PUBLIC: 'public',
23
- PRIVATE: 'private',
24
- DM: 'dm',
25
- } as const
26
-
27
- export type ChannelType = (typeof CHANNEL_TYPES)[keyof typeof CHANNEL_TYPES]
28
-
29
- export { CHANNEL_TYPES }
30
-
31
- export function useMultiChannel() {
32
- const { user } = useUser()
33
- const { users } = useUsers()
34
- const { channels, status } = useChannels()
35
- const { receipts, markAsRead } = useReadReceipts()
36
- const [activeChannelId, setActiveChannelId] = useState<string | null>(null)
37
-
38
- const { records: myMemberships } = useQuery<ChannelMember>('channel-members', {
39
- where: user ? { userId: user.id } : { userId: '__none__' },
40
- })
41
-
42
- const myChannelIds = useMemo(
43
- () => new Set(
44
- myMemberships.map((m: RecordData<ChannelMember>) => m.data.channelId)
45
- ),
46
- [myMemberships]
47
- )
48
-
49
- const liveChannels = useMemo(
50
- () => channels.filter((c: RecordData<Channel>) => !c.data.archived),
51
- [channels]
52
- )
53
-
54
- const groups = useMemo(
55
- () => {
56
- if (!user) return []
57
- return liveChannels.filter(
58
- (c: RecordData<Channel>) =>
59
- (c.data.type === CHANNEL_TYPES.PUBLIC || c.data.type === CHANNEL_TYPES.PRIVATE) &&
60
- myChannelIds.has(c.recordId)
61
- )
62
- },
63
- [liveChannels, myChannelIds, user]
64
- )
65
-
66
- /** DMs visible to the current user -- matched by user ID in channel name, not membership */
67
- const dms = useMemo(
68
- () => {
69
- if (!user) return []
70
- return liveChannels.filter(
71
- (c: RecordData<Channel>) =>
72
- c.data.type === CHANNEL_TYPES.DM &&
73
- c.data.name.includes(user.id)
74
- )
75
- },
76
- [liveChannels, user]
77
- )
78
-
79
- // Use `where` for the immutable `invitedUserId` field so the SDK filters
80
- // at query-time and gates real-time creates. `status` is mutable (pending ->
81
- // accepted/declined), so we filter it client-side in the `useMemo` below.
82
- const { records: myInvitations } = useQuery<ChannelInvitation>('channel-invitations', {
83
- where: user ? { invitedUserId: user.id } : { invitedUserId: '__none__' },
84
- })
85
-
86
- const pendingInvitations = useMemo(
87
- () => {
88
- return myInvitations
89
- .filter((inv: RecordData<ChannelInvitation>) => inv.data.status === 'pending')
90
- .filter((inv: RecordData<ChannelInvitation>) => {
91
- const ch = liveChannels.find((c: RecordData<Channel>) => c.recordId === inv.data.channelId)
92
- return ch && !ch.data.archived
93
- })
94
- .map((inv: RecordData<ChannelInvitation>) => {
95
- const ch = liveChannels.find((c: RecordData<Channel>) => c.recordId === inv.data.channelId)
96
- return { invitation: inv, channel: ch }
97
- })
98
- },
99
- [myInvitations, liveChannels]
100
- )
101
-
102
- const hasUnread = useCallback(
103
- (channelId: string): boolean => {
104
- if (channelId === activeChannelId) return false
105
- const receipt = receipts.find((r) => r.data.channelId === channelId)
106
- return !receipt
107
- },
108
- [receipts, activeChannelId]
109
- )
110
-
111
- const getDMDisplayName = useCallback(
112
- (channel: RecordData<Channel>): string => {
113
- if (!user) return channel.data.name
114
- const dmName = channel.data.name
115
- const parts = dmName.replace('dm-', '').split('-')
116
- const otherId = parts.find((id) => id !== user.id) ?? parts[0]
117
- const otherUser = users.find((u) => u.id === otherId)
118
- return otherUser?.name ?? channel.data.description ?? 'Direct Message'
119
- },
120
- [user, users]
121
- )
122
-
123
- const selectChannel = useCallback(
124
- (id: string) => {
125
- setActiveChannelId(id)
126
- markAsRead(id)
127
- },
128
- [markAsRead]
129
- )
130
-
131
- return {
132
- activeChannelId,
133
- selectChannel,
134
- channels: liveChannels,
135
- groups,
136
- dms,
137
- pendingInvitations,
138
- status,
139
- hasUnread,
140
- getDMDisplayName,
141
- }
142
- }