deepspace 0.26.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.
- package/CHANGELOG.md +35 -0
- package/dist/{chunk-LPXWXF6R.js → chunk-YBXIUDZJ.js} +2 -2
- package/dist/{chunk-LPXWXF6R.js.map → chunk-YBXIUDZJ.js.map} +1 -1
- package/dist/cli.js +80 -45
- package/dist/cli.js.map +1 -1
- package/dist/documentation-client-core.js +1 -1
- package/dist/documentation-default-renderer.cjs +1 -1
- package/dist/documentation-react.js +1 -1
- package/dist/documentation-runtime.js +1 -1
- package/dist/documentation-server-core.js +1 -1
- package/dist/documentation.js +1 -1
- package/dist/documentation.js.map +1 -1
- package/dist/index.d.ts +26 -434
- package/dist/index.js +99 -864
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +4 -41
- package/dist/schema.js +2 -242
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +6 -162
- package/dist/server.js +13 -281
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +4 -160
- package/dist/worker.js +15 -281
- package/dist/worker.js.map +1 -1
- package/features/messaging/feature.json +3 -43
- package/features/messaging/src/ChatPage.tsx +12 -16
- package/features/messaging/src/components/chat/ChatHeader.tsx +5 -13
- package/features/messaging/src/components/chat/MessageItem.tsx +7 -9
- package/features/messaging/src/components/chat/MessageList.tsx +57 -40
- package/features/messaging/src/components/chat/ThreadPanel.tsx +25 -11
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +10 -29
- package/features/messaging/src/hooks/useChatChannel.ts +18 -23
- package/features/messaging/src/messaging-schema.ts +0 -2
- package/features/search-bar/feature.json +9 -20
- package/features/search-bar/src/InlineSearch.tsx +101 -0
- package/features/search-bar/src/__tests__/search-model.test.tsx +38 -0
- package/features/search-bar/src/search-model.ts +2 -149
- package/package.json +1 -1
- package/features/messaging/src/ChatMultiPage.tsx +0 -298
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +0 -188
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +0 -163
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +0 -147
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +0 -344
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +0 -460
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +0 -190
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +0 -149
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +0 -159
- package/features/messaging/src/hooks/useMultiChannel.ts +0 -142
- package/features/search-bar/src/SearchOverlay.tsx +0 -566
- package/features/search-bar/src/__tests__/search-model.test.ts +0 -40
- package/features/sidebar/feature.json +0 -19
- 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
|
-
}
|