birdclaw 0.4.1 → 0.5.0
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 +33 -1
- package/README.md +108 -7
- package/package.json +24 -23
- package/playwright.config.ts +1 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +2 -2
- package/src/cli.ts +450 -18
- package/src/components/AppNav.tsx +66 -29
- package/src/components/AvatarChip.tsx +10 -5
- package/src/components/ConversationThread.tsx +125 -0
- package/src/components/DmWorkspace.tsx +96 -98
- package/src/components/EmbeddedTweetCard.tsx +20 -14
- package/src/components/InboxCard.tsx +92 -90
- package/src/components/LinkPreviewCard.tsx +270 -0
- package/src/components/ProfilePreview.tsx +8 -3
- package/src/components/SavedTimelineView.tsx +48 -38
- package/src/components/TimelineCard.tsx +228 -84
- package/src/components/TweetRichText.tsx +6 -2
- package/src/lib/archive-import.ts +1565 -65
- package/src/lib/authored-live.ts +1074 -0
- package/src/lib/backup.ts +316 -14
- package/src/lib/bird-actions.ts +1 -10
- package/src/lib/bird-command.ts +57 -0
- package/src/lib/bird.ts +89 -5
- package/src/lib/config.ts +1 -1
- package/src/lib/db.ts +191 -4
- package/src/lib/follow-graph.ts +1053 -0
- package/src/lib/link-index.ts +11 -98
- package/src/lib/link-insights.ts +834 -0
- package/src/lib/link-preview-metadata.ts +334 -0
- package/src/lib/media-fetch.ts +787 -0
- package/src/lib/media-includes.ts +165 -0
- package/src/lib/mention-threads-live.ts +535 -43
- package/src/lib/mentions-live.ts +623 -19
- package/src/lib/moderation-target.ts +6 -0
- package/src/lib/profile-hydration.ts +1 -1
- package/src/lib/profile-resolver.ts +115 -1
- package/src/lib/queries.ts +233 -7
- package/src/lib/seed.ts +127 -8
- package/src/lib/timeline-collections-live.ts +145 -22
- package/src/lib/timeline-live.ts +10 -15
- package/src/lib/tweet-account-edges.ts +9 -2
- package/src/lib/tweet-render.ts +97 -0
- package/src/lib/types.ts +185 -2
- package/src/lib/ui.ts +375 -147
- package/src/lib/url-expansion-store.ts +110 -0
- package/src/lib/url-expansion.ts +20 -3
- package/src/lib/x-profile.ts +7 -2
- package/src/lib/xurl.ts +296 -12
- package/src/routeTree.gen.ts +105 -0
- package/src/routes/__root.tsx +7 -3
- package/src/routes/api/conversation.tsx +34 -0
- package/src/routes/api/link-insights.tsx +79 -0
- package/src/routes/api/link-preview.tsx +43 -0
- package/src/routes/api/profile-hydrate.tsx +51 -0
- package/src/routes/blocks.tsx +111 -86
- package/src/routes/dms.tsx +90 -78
- package/src/routes/inbox.tsx +98 -86
- package/src/routes/index.tsx +63 -50
- package/src/routes/links.tsx +883 -0
- package/src/routes/mentions.tsx +63 -50
- package/src/styles.css +106 -43
package/src/routes/blocks.tsx
CHANGED
|
@@ -9,22 +9,21 @@ import type {
|
|
|
9
9
|
QueryEnvelope,
|
|
10
10
|
} from "#/lib/types";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
contentCardClass,
|
|
12
|
+
blockRowBodyClass,
|
|
13
|
+
blockRowClass,
|
|
15
14
|
cx,
|
|
15
|
+
dangerButtonClass,
|
|
16
|
+
emptyStateClass,
|
|
16
17
|
errorCopyClass,
|
|
17
|
-
eyebrowClass,
|
|
18
|
-
heroControlsBlocksClass,
|
|
19
|
-
heroControlsClass,
|
|
20
|
-
heroCopyClass,
|
|
21
|
-
heroShellClass,
|
|
22
|
-
heroTitleClass,
|
|
23
|
-
identityBlockClass,
|
|
24
|
-
metaRowClass,
|
|
25
18
|
mutedDotClass,
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
pageHeaderClass,
|
|
20
|
+
pageHeaderRowClass,
|
|
21
|
+
pageSubtitleClass,
|
|
22
|
+
pageTitleClass,
|
|
23
|
+
primaryButtonClass,
|
|
24
|
+
secondaryButtonClass,
|
|
25
|
+
selectFieldClass,
|
|
26
|
+
statusCopyClass,
|
|
28
27
|
textFieldClass,
|
|
29
28
|
textFieldShortClass,
|
|
30
29
|
textFieldWideClass,
|
|
@@ -142,7 +141,7 @@ function BlocksRoute() {
|
|
|
142
141
|
}
|
|
143
142
|
setMessage(
|
|
144
143
|
data.transport?.output ??
|
|
145
|
-
`Synced ${data.syncedCount ?? 0} remote blocks`,
|
|
144
|
+
`Synced ${String(data.syncedCount ?? 0)} remote blocks`,
|
|
146
145
|
);
|
|
147
146
|
},
|
|
148
147
|
)
|
|
@@ -162,12 +161,12 @@ function BlocksRoute() {
|
|
|
162
161
|
const subtitle = useMemo(() => {
|
|
163
162
|
if (!meta) {
|
|
164
163
|
return items.length > 0
|
|
165
|
-
? `${items.length} blocked profiles
|
|
164
|
+
? `${String(items.length)} blocked profiles · loading transport...`
|
|
166
165
|
: "Loading local blocklist...";
|
|
167
166
|
}
|
|
168
167
|
if (isSyncing)
|
|
169
168
|
return `Syncing remote blocklist · ${meta.transport.statusText}`;
|
|
170
|
-
return `${items.length} blocked profiles
|
|
169
|
+
return `${String(items.length)} blocked profiles · ${meta.transport.statusText}`;
|
|
171
170
|
}, [isSyncing, items.length, meta]);
|
|
172
171
|
|
|
173
172
|
async function submit(
|
|
@@ -192,9 +191,14 @@ function BlocksRoute() {
|
|
|
192
191
|
}),
|
|
193
192
|
});
|
|
194
193
|
const data = (await response.json()) as {
|
|
194
|
+
ok?: boolean;
|
|
195
195
|
profile?: { handle?: string };
|
|
196
|
-
transport?: { output?: string };
|
|
196
|
+
transport?: { ok?: boolean; output?: string };
|
|
197
197
|
};
|
|
198
|
+
if (data.ok === false || data.transport?.ok === false) {
|
|
199
|
+
setError(data.transport?.output ?? "Blocklist action failed");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
198
202
|
|
|
199
203
|
setMessage(
|
|
200
204
|
`${kind === "blockProfile" ? "Blocked" : "Unblocked"} @${
|
|
@@ -214,18 +218,20 @@ function BlocksRoute() {
|
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
return (
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
<div>
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
<>
|
|
222
|
+
<header className={pageHeaderClass}>
|
|
223
|
+
<div className={pageHeaderRowClass}>
|
|
224
|
+
<div className="flex min-w-0 flex-col">
|
|
225
|
+
<h1 className={pageTitleClass}>Blocks</h1>
|
|
226
|
+
<h2 className={cx(pageSubtitleClass, "text-[14px]")}>
|
|
227
|
+
Maintain a clean blocklist locally.
|
|
228
|
+
</h2>
|
|
229
|
+
<p className={pageSubtitleClass}>{subtitle}</p>
|
|
230
|
+
</div>
|
|
225
231
|
</div>
|
|
226
|
-
<div className=
|
|
232
|
+
<div className="flex flex-wrap items-center gap-2 px-4 pb-3">
|
|
227
233
|
<select
|
|
228
|
-
className={cx(
|
|
234
|
+
className={cx(selectFieldClass, textFieldShortClass)}
|
|
229
235
|
disabled={!isReady}
|
|
230
236
|
onChange={(event) => setAccountId(event.target.value)}
|
|
231
237
|
value={accountId}
|
|
@@ -237,14 +243,18 @@ function BlocksRoute() {
|
|
|
237
243
|
))}
|
|
238
244
|
</select>
|
|
239
245
|
<input
|
|
240
|
-
className={cx(
|
|
246
|
+
className={cx(
|
|
247
|
+
textFieldClass,
|
|
248
|
+
textFieldWideClass,
|
|
249
|
+
"flex-1 min-w-[200px]",
|
|
250
|
+
)}
|
|
241
251
|
disabled={!hasAccountId}
|
|
242
252
|
onChange={(event) => setSearch(event.target.value)}
|
|
243
253
|
placeholder="Handle, name, bio, or Twitter URL"
|
|
244
254
|
value={search}
|
|
245
255
|
/>
|
|
246
256
|
<button
|
|
247
|
-
className={
|
|
257
|
+
className={primaryButtonClass}
|
|
248
258
|
disabled={!hasAccountId || isSubmitting || !search.trim()}
|
|
249
259
|
onClick={() => void submit("blockProfile", search)}
|
|
250
260
|
type="button"
|
|
@@ -252,29 +262,31 @@ function BlocksRoute() {
|
|
|
252
262
|
{isSubmitting ? "Working..." : "Block"}
|
|
253
263
|
</button>
|
|
254
264
|
</div>
|
|
255
|
-
</
|
|
265
|
+
</header>
|
|
256
266
|
|
|
257
|
-
{message ? <p className={
|
|
267
|
+
{message ? <p className={statusCopyClass}>{message}</p> : null}
|
|
258
268
|
{error ? <p className={errorCopyClass}>{error}</p> : null}
|
|
259
269
|
|
|
260
270
|
{matches.length > 0 ? (
|
|
261
|
-
<section className=
|
|
271
|
+
<section className="flex flex-col">
|
|
272
|
+
<h2 className="px-4 pt-3 pb-1 text-[13px] font-semibold uppercase tracking-wide text-[var(--ink-soft)]">
|
|
273
|
+
Search matches
|
|
274
|
+
</h2>
|
|
262
275
|
{matches.map((match) => (
|
|
263
|
-
<article
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<
|
|
277
|
-
<div className={metaRowClass}>
|
|
276
|
+
<article className={blockRowClass} key={match.profile.id}>
|
|
277
|
+
<AvatarChip
|
|
278
|
+
avatarUrl={match.profile.avatarUrl}
|
|
279
|
+
hue={match.profile.avatarHue}
|
|
280
|
+
name={match.profile.displayName}
|
|
281
|
+
profileId={match.profile.id}
|
|
282
|
+
/>
|
|
283
|
+
<div className={blockRowBodyClass}>
|
|
284
|
+
<div className="flex items-center justify-between gap-2">
|
|
285
|
+
<div className="flex min-w-0 flex-col">
|
|
286
|
+
<strong className="truncate text-[15px] text-[var(--ink)]">
|
|
287
|
+
{match.profile.displayName}
|
|
288
|
+
</strong>
|
|
289
|
+
<div className="flex flex-wrap items-center gap-1.5 text-[13px] text-[var(--ink-soft)]">
|
|
278
290
|
<span>@{match.profile.handle}</span>
|
|
279
291
|
<span className={mutedDotClass} />
|
|
280
292
|
<span>
|
|
@@ -283,43 +295,52 @@ function BlocksRoute() {
|
|
|
283
295
|
</span>
|
|
284
296
|
</div>
|
|
285
297
|
</div>
|
|
298
|
+
<button
|
|
299
|
+
className={
|
|
300
|
+
match.isBlocked ? secondaryButtonClass : dangerButtonClass
|
|
301
|
+
}
|
|
302
|
+
onClick={() =>
|
|
303
|
+
void submit(
|
|
304
|
+
match.isBlocked ? "unblockProfile" : "blockProfile",
|
|
305
|
+
match.profile.id,
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
type="button"
|
|
309
|
+
>
|
|
310
|
+
{match.isBlocked ? "Unblock" : "Block"}
|
|
311
|
+
</button>
|
|
286
312
|
</div>
|
|
287
|
-
<
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
void submit(
|
|
291
|
-
match.isBlocked ? "unblockProfile" : "blockProfile",
|
|
292
|
-
match.profile.id,
|
|
293
|
-
)
|
|
294
|
-
}
|
|
295
|
-
type="button"
|
|
296
|
-
>
|
|
297
|
-
{match.isBlocked ? "Unblock" : "Block"}
|
|
298
|
-
</button>
|
|
313
|
+
<p className="text-[14px] leading-[1.4] text-[var(--ink)]">
|
|
314
|
+
{match.profile.bio}
|
|
315
|
+
</p>
|
|
299
316
|
</div>
|
|
300
|
-
<p className="mt-2.5">{match.profile.bio}</p>
|
|
301
317
|
</article>
|
|
302
318
|
))}
|
|
303
319
|
</section>
|
|
304
320
|
) : null}
|
|
305
321
|
|
|
306
|
-
<section className=
|
|
322
|
+
<section className="flex flex-col">
|
|
323
|
+
{items.length === 0 && matches.length === 0 ? (
|
|
324
|
+
<div className={emptyStateClass}>No blocks in this account.</div>
|
|
325
|
+
) : null}
|
|
307
326
|
{items.map((item) => (
|
|
308
327
|
<article
|
|
309
|
-
className={
|
|
328
|
+
className={blockRowClass}
|
|
310
329
|
key={item.accountId + item.profile.id}
|
|
311
330
|
>
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<div>
|
|
321
|
-
<strong>
|
|
322
|
-
|
|
331
|
+
<AvatarChip
|
|
332
|
+
avatarUrl={item.profile.avatarUrl}
|
|
333
|
+
hue={item.profile.avatarHue}
|
|
334
|
+
name={item.profile.displayName}
|
|
335
|
+
profileId={item.profile.id}
|
|
336
|
+
/>
|
|
337
|
+
<div className={blockRowBodyClass}>
|
|
338
|
+
<div className="flex items-center justify-between gap-2">
|
|
339
|
+
<div className="flex min-w-0 flex-col">
|
|
340
|
+
<strong className="truncate text-[15px] text-[var(--ink)]">
|
|
341
|
+
{item.profile.displayName}
|
|
342
|
+
</strong>
|
|
343
|
+
<div className="flex flex-wrap items-center gap-1.5 text-[13px] text-[var(--ink-soft)]">
|
|
323
344
|
<span>@{item.profile.handle}</span>
|
|
324
345
|
<span className={mutedDotClass} />
|
|
325
346
|
<span>{item.accountHandle}</span>
|
|
@@ -330,23 +351,27 @@ function BlocksRoute() {
|
|
|
330
351
|
</span>
|
|
331
352
|
</div>
|
|
332
353
|
</div>
|
|
354
|
+
<button
|
|
355
|
+
className={secondaryButtonClass}
|
|
356
|
+
onClick={() => void submit("unblockProfile", item.profile.id)}
|
|
357
|
+
type="button"
|
|
358
|
+
>
|
|
359
|
+
Unblock
|
|
360
|
+
</button>
|
|
333
361
|
</div>
|
|
334
|
-
|
|
335
|
-
className=
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
362
|
+
{item.profile.bio ? (
|
|
363
|
+
<p className="text-[14px] leading-[1.4] text-[var(--ink)]">
|
|
364
|
+
{item.profile.bio}
|
|
365
|
+
</p>
|
|
366
|
+
) : null}
|
|
367
|
+
<p className={timestampClass}>
|
|
368
|
+
Blocked {new Date(item.blockedAt).toLocaleString()} ·{" "}
|
|
369
|
+
{item.source}
|
|
370
|
+
</p>
|
|
341
371
|
</div>
|
|
342
|
-
<p className="mt-2.5">{item.profile.bio}</p>
|
|
343
|
-
<p className={timestampClass}>
|
|
344
|
-
Blocked {new Date(item.blockedAt).toLocaleString()} ·{" "}
|
|
345
|
-
{item.source}
|
|
346
|
-
</p>
|
|
347
372
|
</article>
|
|
348
373
|
))}
|
|
349
374
|
</section>
|
|
350
|
-
|
|
375
|
+
</>
|
|
351
376
|
);
|
|
352
377
|
}
|
package/src/routes/dms.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { Search } from "lucide-react";
|
|
2
3
|
import { useEffect, useMemo, useState } from "react";
|
|
3
4
|
import { DmWorkspace } from "#/components/DmWorkspace";
|
|
4
5
|
import type {
|
|
@@ -10,27 +11,34 @@ import type {
|
|
|
10
11
|
} from "#/lib/types";
|
|
11
12
|
import {
|
|
12
13
|
cx,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
heroTitleClass,
|
|
21
|
-
pageWrapClass,
|
|
14
|
+
pageHeaderClass,
|
|
15
|
+
pageHeaderRowClass,
|
|
16
|
+
pageSubtitleClass,
|
|
17
|
+
pageTitleClass,
|
|
18
|
+
searchFieldIconClass,
|
|
19
|
+
searchFieldInputClass,
|
|
20
|
+
searchFieldShellClass,
|
|
22
21
|
segmentActiveClass,
|
|
23
22
|
segmentClass,
|
|
24
23
|
segmentedClass,
|
|
24
|
+
tabButtonActiveClass,
|
|
25
|
+
tabButtonClass,
|
|
26
|
+
tabButtonIndicatorClass,
|
|
27
|
+
tabStripClass,
|
|
25
28
|
textFieldClass,
|
|
26
29
|
textFieldShortClass,
|
|
27
|
-
textFieldWideClass,
|
|
28
30
|
} from "#/lib/ui";
|
|
29
31
|
|
|
30
32
|
export const Route = createFileRoute("/dms")({
|
|
31
33
|
component: DmsRoute,
|
|
32
34
|
});
|
|
33
35
|
|
|
36
|
+
const TABS: Array<{ value: ReplyFilter; label: string }> = [
|
|
37
|
+
{ value: "all", label: "All" },
|
|
38
|
+
{ value: "unreplied", label: "Unreplied" },
|
|
39
|
+
{ value: "replied", label: "Replied" },
|
|
40
|
+
];
|
|
41
|
+
|
|
34
42
|
function DmsRoute() {
|
|
35
43
|
const [meta, setMeta] = useState<QueryEnvelope | null>(null);
|
|
36
44
|
const [items, setItems] = useState<DmConversationItem[]>([]);
|
|
@@ -110,7 +118,7 @@ function DmsRoute() {
|
|
|
110
118
|
|
|
111
119
|
const subtitle = useMemo(() => {
|
|
112
120
|
if (!meta) return "Loading direct messages...";
|
|
113
|
-
return `${meta.stats.dms} conversations cached locally
|
|
121
|
+
return `${String(meta.stats.dms)} conversations cached locally`;
|
|
114
122
|
}, [meta]);
|
|
115
123
|
|
|
116
124
|
async function replyToConversation(conversationId: string) {
|
|
@@ -183,80 +191,84 @@ function DmsRoute() {
|
|
|
183
191
|
}
|
|
184
192
|
|
|
185
193
|
return (
|
|
186
|
-
|
|
187
|
-
<
|
|
188
|
-
<
|
|
189
|
-
<div>
|
|
190
|
-
<
|
|
191
|
-
<
|
|
192
|
-
Influence, bio, and reply state. No hunting.
|
|
193
|
-
</h2>
|
|
194
|
-
<p className={heroCopyClass}>{subtitle}</p>
|
|
194
|
+
<>
|
|
195
|
+
<header className={pageHeaderClass}>
|
|
196
|
+
<div className={pageHeaderRowClass}>
|
|
197
|
+
<div className="flex min-w-0 flex-col">
|
|
198
|
+
<h1 className={pageTitleClass}>Messages</h1>
|
|
199
|
+
<p className={pageSubtitleClass}>{subtitle}</p>
|
|
195
200
|
</div>
|
|
196
|
-
|
|
201
|
+
</div>
|
|
202
|
+
<div className="flex flex-wrap items-center gap-2 px-4 pb-3">
|
|
203
|
+
<label className={cx(searchFieldShellClass, "flex-1 min-w-[200px]")}>
|
|
204
|
+
<Search className={searchFieldIconClass} strokeWidth={2} />
|
|
197
205
|
<input
|
|
198
|
-
className={
|
|
206
|
+
className={searchFieldInputClass}
|
|
199
207
|
onChange={(event) => setSearch(event.target.value)}
|
|
200
208
|
placeholder="Search DMs"
|
|
201
209
|
value={search}
|
|
202
210
|
/>
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
<div className={segmentedClass}>
|
|
233
|
-
{(["all", "replied", "unreplied"] as const).map((value) => (
|
|
234
|
-
<button
|
|
235
|
-
key={value}
|
|
236
|
-
className={cx(
|
|
237
|
-
segmentClass,
|
|
238
|
-
value === replyFilter && segmentActiveClass,
|
|
239
|
-
)}
|
|
240
|
-
onClick={() => setReplyFilter(value)}
|
|
241
|
-
type="button"
|
|
242
|
-
>
|
|
243
|
-
{value}
|
|
244
|
-
</button>
|
|
245
|
-
))}
|
|
246
|
-
</div>
|
|
211
|
+
</label>
|
|
212
|
+
<input
|
|
213
|
+
className={cx(textFieldClass, textFieldShortClass)}
|
|
214
|
+
inputMode="numeric"
|
|
215
|
+
onChange={(event) => setMinFollowers(event.target.value)}
|
|
216
|
+
placeholder="Min followers"
|
|
217
|
+
value={minFollowers}
|
|
218
|
+
/>
|
|
219
|
+
<input
|
|
220
|
+
className={cx(textFieldClass, textFieldShortClass)}
|
|
221
|
+
inputMode="numeric"
|
|
222
|
+
onChange={(event) => setMinInfluenceScore(event.target.value)}
|
|
223
|
+
placeholder="Min score"
|
|
224
|
+
value={minInfluenceScore}
|
|
225
|
+
/>
|
|
226
|
+
<div className={segmentedClass}>
|
|
227
|
+
{(["recent", "influence"] as const).map((value) => (
|
|
228
|
+
<button
|
|
229
|
+
key={value}
|
|
230
|
+
className={cx(
|
|
231
|
+
segmentClass,
|
|
232
|
+
value === sort && segmentActiveClass,
|
|
233
|
+
)}
|
|
234
|
+
onClick={() => setSort(value)}
|
|
235
|
+
type="button"
|
|
236
|
+
>
|
|
237
|
+
{value}
|
|
238
|
+
</button>
|
|
239
|
+
))}
|
|
247
240
|
</div>
|
|
248
|
-
</
|
|
241
|
+
</div>
|
|
242
|
+
<div className={tabStripClass}>
|
|
243
|
+
{TABS.map((tab) => {
|
|
244
|
+
const active = replyFilter === tab.value;
|
|
245
|
+
return (
|
|
246
|
+
<button
|
|
247
|
+
key={tab.value}
|
|
248
|
+
type="button"
|
|
249
|
+
aria-pressed={active}
|
|
250
|
+
className={cx(tabButtonClass, active && tabButtonActiveClass)}
|
|
251
|
+
onClick={() => setReplyFilter(tab.value)}
|
|
252
|
+
>
|
|
253
|
+
<span className="relative inline-flex flex-col items-center justify-center py-1">
|
|
254
|
+
{tab.value}
|
|
255
|
+
{active ? <span className={tabButtonIndicatorClass} /> : null}
|
|
256
|
+
</span>
|
|
257
|
+
</button>
|
|
258
|
+
);
|
|
259
|
+
})}
|
|
260
|
+
</div>
|
|
261
|
+
</header>
|
|
249
262
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
</main>
|
|
263
|
+
<DmWorkspace
|
|
264
|
+
conversations={items}
|
|
265
|
+
onReplyDraftChange={setReplyDraft}
|
|
266
|
+
onReplySend={replyToConversation}
|
|
267
|
+
onSelectConversation={setSelectedConversationId}
|
|
268
|
+
replyDraft={replyDraft}
|
|
269
|
+
selectedConversation={selectedConversation}
|
|
270
|
+
selectedMessages={messages}
|
|
271
|
+
/>
|
|
272
|
+
</>
|
|
261
273
|
);
|
|
262
274
|
}
|