birdclaw 0.4.0 → 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 +39 -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 +563 -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 +1567 -65
- package/src/lib/authored-live.ts +1074 -0
- package/src/lib/backup.ts +318 -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 +282 -4
- package/src/lib/follow-graph.ts +1053 -0
- package/src/lib/link-index.ts +629 -0
- 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 +219 -2
- package/src/lib/ui.ts +375 -147
- package/src/lib/url-expansion-store.ts +110 -0
- package/src/lib/url-expansion.ts +33 -8
- 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
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import { Link, useRouterState } from "@tanstack/react-router";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
Bell,
|
|
4
|
+
Bird,
|
|
5
|
+
Bookmark,
|
|
6
|
+
Heart,
|
|
7
|
+
Home,
|
|
8
|
+
Inbox,
|
|
9
|
+
Link as LinkIcon,
|
|
10
|
+
Mail,
|
|
11
|
+
ShieldOff,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import {
|
|
4
14
|
cx,
|
|
5
|
-
eyebrowClass,
|
|
6
|
-
navClass,
|
|
7
15
|
navLinkActiveClass,
|
|
8
16
|
navLinkClass,
|
|
9
|
-
|
|
17
|
+
navLinkIconClass,
|
|
18
|
+
navLinkLabelClass,
|
|
19
|
+
sidebarBrandClass,
|
|
20
|
+
sidebarBrandCopyClass,
|
|
21
|
+
sidebarBrandMarkClass,
|
|
22
|
+
sidebarBrandTaglineClass,
|
|
23
|
+
sidebarBrandTitleClass,
|
|
24
|
+
sidebarFooterClass,
|
|
25
|
+
sidebarNavClass,
|
|
26
|
+
sidebarShellClass,
|
|
10
27
|
} from "#/lib/ui";
|
|
11
28
|
import { ThemeSlider } from "./ThemeSlider";
|
|
12
29
|
|
|
13
30
|
const links = [
|
|
14
|
-
{ to: "/inbox", label: "Inbox" },
|
|
15
|
-
{ to: "/", label: "Home" },
|
|
16
|
-
{ to: "/mentions", label: "Mentions" },
|
|
17
|
-
{ to: "/likes", label: "Likes" },
|
|
18
|
-
{ to: "/bookmarks", label: "Bookmarks" },
|
|
19
|
-
{ to: "/
|
|
20
|
-
{ to: "/
|
|
31
|
+
{ to: "/inbox", label: "Inbox", icon: Inbox },
|
|
32
|
+
{ to: "/", label: "Home", icon: Home },
|
|
33
|
+
{ to: "/mentions", label: "Mentions", icon: Bell },
|
|
34
|
+
{ to: "/likes", label: "Likes", icon: Heart },
|
|
35
|
+
{ to: "/bookmarks", label: "Bookmarks", icon: Bookmark },
|
|
36
|
+
{ to: "/links", label: "Links", icon: LinkIcon },
|
|
37
|
+
{ to: "/dms", label: "DMs", icon: Mail },
|
|
38
|
+
{ to: "/blocks", label: "Blocks", icon: ShieldOff },
|
|
21
39
|
] as const;
|
|
22
40
|
|
|
23
41
|
export function AppNav() {
|
|
@@ -26,26 +44,45 @@ export function AppNav() {
|
|
|
26
44
|
});
|
|
27
45
|
|
|
28
46
|
return (
|
|
29
|
-
<
|
|
30
|
-
<div>
|
|
31
|
-
<
|
|
32
|
-
|
|
47
|
+
<aside className={sidebarShellClass}>
|
|
48
|
+
<div className="flex flex-col">
|
|
49
|
+
<Link to="/" className={sidebarBrandClass}>
|
|
50
|
+
<span className={sidebarBrandMarkClass}>
|
|
51
|
+
<Bird className="size-5" strokeWidth={2.4} />
|
|
52
|
+
</span>
|
|
53
|
+
<span className={sidebarBrandCopyClass}>
|
|
54
|
+
<span className={sidebarBrandTitleClass}>birdclaw</span>
|
|
55
|
+
<span className={sidebarBrandTaglineClass}>
|
|
56
|
+
Quiet signal for Twitter.
|
|
57
|
+
</span>
|
|
58
|
+
</span>
|
|
59
|
+
</Link>
|
|
60
|
+
<nav className={sidebarNavClass} aria-label="Primary">
|
|
61
|
+
{links.map((link) => {
|
|
62
|
+
const active = pathname === link.to;
|
|
63
|
+
const Icon = link.icon;
|
|
64
|
+
return (
|
|
65
|
+
<Link
|
|
66
|
+
key={link.to}
|
|
67
|
+
to={link.to}
|
|
68
|
+
aria-label={link.label}
|
|
69
|
+
className={cx(navLinkClass, active && navLinkActiveClass)}
|
|
70
|
+
>
|
|
71
|
+
<Icon
|
|
72
|
+
className={navLinkIconClass}
|
|
73
|
+
size={22}
|
|
74
|
+
strokeWidth={active ? 2.4 : 1.8}
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
/>
|
|
77
|
+
<span className={navLinkLabelClass}>{link.label}</span>
|
|
78
|
+
</Link>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</nav>
|
|
33
82
|
</div>
|
|
34
|
-
<div className={
|
|
35
|
-
{links.map((link) => {
|
|
36
|
-
const active = pathname === link.to;
|
|
37
|
-
return (
|
|
38
|
-
<Link
|
|
39
|
-
key={link.to}
|
|
40
|
-
to={link.to}
|
|
41
|
-
className={cx(navLinkClass, active && navLinkActiveClass)}
|
|
42
|
-
>
|
|
43
|
-
{link.label}
|
|
44
|
-
</Link>
|
|
45
|
-
);
|
|
46
|
-
})}
|
|
83
|
+
<div className={sidebarFooterClass}>
|
|
47
84
|
<ThemeSlider />
|
|
48
85
|
</div>
|
|
49
|
-
</
|
|
86
|
+
</aside>
|
|
50
87
|
);
|
|
51
88
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { getInitials } from "#/lib/present";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
avatarChipClass,
|
|
5
|
+
avatarChipLargeClass,
|
|
6
|
+
avatarChipSmallClass,
|
|
7
|
+
cx,
|
|
8
|
+
} from "#/lib/ui";
|
|
4
9
|
|
|
5
10
|
export function AvatarChip({
|
|
6
11
|
profileId,
|
|
@@ -13,7 +18,7 @@ export function AvatarChip({
|
|
|
13
18
|
avatarUrl?: string;
|
|
14
19
|
name: string;
|
|
15
20
|
hue: number;
|
|
16
|
-
size?: "default" | "large";
|
|
21
|
+
size?: "default" | "large" | "small";
|
|
17
22
|
}) {
|
|
18
23
|
const avatarSrc =
|
|
19
24
|
profileId && avatarUrl
|
|
@@ -26,10 +31,10 @@ export function AvatarChip({
|
|
|
26
31
|
<span
|
|
27
32
|
className={cx(
|
|
28
33
|
avatarChipClass,
|
|
29
|
-
"
|
|
30
|
-
size === "
|
|
34
|
+
size === "large" && avatarChipLargeClass,
|
|
35
|
+
size === "small" && avatarChipSmallClass,
|
|
31
36
|
)}
|
|
32
|
-
style={{ backgroundColor: `hsl(${hue} 72% 50%)` }}
|
|
37
|
+
style={{ backgroundColor: `hsl(${String(hue)} 72% 50%)` }}
|
|
33
38
|
>
|
|
34
39
|
{showImage ? (
|
|
35
40
|
<img
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { MessageCircle } from "lucide-react";
|
|
2
|
+
import { formatShortTimestamp } from "#/lib/present";
|
|
3
|
+
import type { EmbeddedTweet } from "#/lib/types";
|
|
4
|
+
import {
|
|
5
|
+
cx,
|
|
6
|
+
emptyStateClass,
|
|
7
|
+
feedActionIconClass,
|
|
8
|
+
feedRowHandleClass,
|
|
9
|
+
feedRowNameClass,
|
|
10
|
+
feedRowTimestampClass,
|
|
11
|
+
} from "#/lib/ui";
|
|
12
|
+
import { AvatarChip } from "./AvatarChip";
|
|
13
|
+
import { ProfilePreview } from "./ProfilePreview";
|
|
14
|
+
import { TweetMediaGrid } from "./TweetMediaGrid";
|
|
15
|
+
import { TweetRichText } from "./TweetRichText";
|
|
16
|
+
|
|
17
|
+
export function ConversationThread({
|
|
18
|
+
anchorId,
|
|
19
|
+
error,
|
|
20
|
+
items,
|
|
21
|
+
loading,
|
|
22
|
+
}: {
|
|
23
|
+
anchorId: string;
|
|
24
|
+
error?: string | null;
|
|
25
|
+
items: EmbeddedTweet[];
|
|
26
|
+
loading: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
if (loading) {
|
|
29
|
+
return (
|
|
30
|
+
<section className="mt-3 rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] px-4 py-4 text-[14px] text-[var(--ink-soft)]">
|
|
31
|
+
Loading conversation...
|
|
32
|
+
</section>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (error) {
|
|
37
|
+
return (
|
|
38
|
+
<section className="mt-3 rounded-2xl border border-[var(--alert)] bg-[var(--alert-soft)] px-4 py-3 text-[14px] text-[var(--alert)]">
|
|
39
|
+
{error}
|
|
40
|
+
</section>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (items.length <= 1) {
|
|
45
|
+
return (
|
|
46
|
+
<section
|
|
47
|
+
className={cx(
|
|
48
|
+
emptyStateClass,
|
|
49
|
+
"mt-3 rounded-2xl border border-[var(--line)] py-4",
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
No other archived replies in this conversation.
|
|
53
|
+
</section>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<section
|
|
59
|
+
aria-label="Conversation"
|
|
60
|
+
className="mt-3 overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--bg-card)]"
|
|
61
|
+
>
|
|
62
|
+
<div className="flex items-center gap-2 border-b border-[var(--line)] px-4 py-2.5 text-[13px] font-bold text-[var(--ink)]">
|
|
63
|
+
<MessageCircle className={feedActionIconClass} strokeWidth={1.8} />
|
|
64
|
+
<span>{items.length} tweets in conversation</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="flex flex-col">
|
|
67
|
+
{items.map((tweet, index) => {
|
|
68
|
+
const isAnchor = tweet.id === anchorId;
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
className={cx(
|
|
72
|
+
"flex gap-3 px-4 py-3",
|
|
73
|
+
index > 0 && "border-t border-[var(--line)]",
|
|
74
|
+
isAnchor && "bg-[var(--accent-soft)]",
|
|
75
|
+
)}
|
|
76
|
+
key={tweet.id}
|
|
77
|
+
>
|
|
78
|
+
<div className="flex flex-col items-center">
|
|
79
|
+
<AvatarChip
|
|
80
|
+
avatarUrl={tweet.author.avatarUrl}
|
|
81
|
+
hue={tweet.author.avatarHue}
|
|
82
|
+
name={tweet.author.displayName}
|
|
83
|
+
profileId={tweet.author.id}
|
|
84
|
+
size="small"
|
|
85
|
+
/>
|
|
86
|
+
{index < items.length - 1 ? (
|
|
87
|
+
<span className="mt-2 w-px flex-1 bg-[var(--line)]" />
|
|
88
|
+
) : null}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="min-w-0 flex-1">
|
|
91
|
+
<header className="flex min-w-0 items-center gap-1.5 text-[14px]">
|
|
92
|
+
<ProfilePreview profile={tweet.author}>
|
|
93
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
94
|
+
<span className={feedRowNameClass}>
|
|
95
|
+
{tweet.author.displayName}
|
|
96
|
+
</span>
|
|
97
|
+
<span className={feedRowHandleClass}>
|
|
98
|
+
@{tweet.author.handle}
|
|
99
|
+
</span>
|
|
100
|
+
</span>
|
|
101
|
+
</ProfilePreview>
|
|
102
|
+
<span className="text-[var(--ink-soft)]">·</span>
|
|
103
|
+
<span className={feedRowTimestampClass}>
|
|
104
|
+
{formatShortTimestamp(tweet.createdAt)}
|
|
105
|
+
</span>
|
|
106
|
+
{isAnchor ? (
|
|
107
|
+
<span className="ml-auto rounded-full bg-[var(--accent)] px-2 py-0.5 text-[11px] font-bold text-white">
|
|
108
|
+
selected
|
|
109
|
+
</span>
|
|
110
|
+
) : null}
|
|
111
|
+
</header>
|
|
112
|
+
<TweetRichText
|
|
113
|
+
className="mt-1 whitespace-pre-wrap break-words text-[14px] leading-[1.45] text-[var(--ink)] [overflow-wrap:anywhere]"
|
|
114
|
+
entities={tweet.entities}
|
|
115
|
+
text={tweet.text}
|
|
116
|
+
/>
|
|
117
|
+
<TweetMediaGrid items={tweet.media} />
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -1,67 +1,62 @@
|
|
|
1
1
|
import { formatCompactNumber, formatShortTimestamp } from "#/lib/present";
|
|
2
2
|
import type { DmConversationItem, DmMessageItem } from "#/lib/types";
|
|
3
3
|
import {
|
|
4
|
-
actionButtonClass,
|
|
5
4
|
composerBarClass,
|
|
6
5
|
composerInputClass,
|
|
7
|
-
composerShellClass,
|
|
8
|
-
contextBioClass,
|
|
9
|
-
contextHandleClass,
|
|
10
6
|
contextStatRowClass,
|
|
11
|
-
contextStatsClass,
|
|
12
7
|
contextStatTermClass,
|
|
13
8
|
contextStatValueClass,
|
|
14
9
|
cx,
|
|
15
|
-
|
|
10
|
+
dmComposerShellClass,
|
|
11
|
+
dmListBodyClass,
|
|
16
12
|
dmListClass,
|
|
17
|
-
|
|
13
|
+
dmListHandleClass,
|
|
14
|
+
dmListHeaderClass,
|
|
18
15
|
dmListItemActiveClass,
|
|
19
16
|
dmListItemClass,
|
|
20
|
-
|
|
17
|
+
dmListNameClass,
|
|
18
|
+
dmListPreviewClass,
|
|
19
|
+
dmListTimestampClass,
|
|
20
|
+
dmMessageBubbleClass,
|
|
21
|
+
dmMessageBubbleOutboundClass,
|
|
22
|
+
dmMessageMetaClass,
|
|
23
|
+
dmMessageRowClass,
|
|
24
|
+
dmMessageRowOutboundClass,
|
|
25
|
+
dmMessagesClass,
|
|
26
|
+
dmShellClass,
|
|
27
|
+
dmThreadClass,
|
|
28
|
+
dmThreadHeaderClass,
|
|
29
|
+
dmThreadNameClass,
|
|
30
|
+
dmThreadSubtitleClass,
|
|
31
|
+
dmThreadTitleClass,
|
|
21
32
|
emptyStateClass,
|
|
22
|
-
eyebrowClass,
|
|
23
|
-
identityRowClass,
|
|
24
|
-
messageBubbleClass,
|
|
25
|
-
messageBubbleOutboundClass,
|
|
26
|
-
messageMetaClass,
|
|
27
|
-
messageRowClass,
|
|
28
|
-
messageRowOutboundClass,
|
|
29
|
-
messageStackClass,
|
|
30
|
-
metaStackClass,
|
|
31
33
|
pillAlertClass,
|
|
32
34
|
pillClass,
|
|
33
35
|
pillSoftClass,
|
|
34
|
-
|
|
35
|
-
threadDetailColumnClass,
|
|
36
|
-
threadDetailHeaderClass,
|
|
37
|
-
threadHeaderClass,
|
|
38
|
-
threadShellClass,
|
|
39
|
-
threadSubtitleClass,
|
|
40
|
-
threadTitleClass,
|
|
36
|
+
primaryButtonClass,
|
|
41
37
|
timestampClass,
|
|
42
38
|
} from "#/lib/ui";
|
|
43
39
|
import { AvatarChip } from "./AvatarChip";
|
|
44
40
|
|
|
45
41
|
function MessageBubble({ message }: { message: DmMessageItem }) {
|
|
42
|
+
const outbound = message.direction === "outbound";
|
|
46
43
|
return (
|
|
47
44
|
<div
|
|
48
|
-
className={cx(
|
|
49
|
-
messageRowClass,
|
|
50
|
-
message.direction === "outbound" && messageRowOutboundClass,
|
|
51
|
-
)}
|
|
45
|
+
className={cx(dmMessageRowClass, outbound && dmMessageRowOutboundClass)}
|
|
52
46
|
>
|
|
53
|
-
<div className={messageMetaClass}>
|
|
54
|
-
<span>{message.sender.displayName}</span>
|
|
55
|
-
<span>{formatShortTimestamp(message.createdAt)}</span>
|
|
56
|
-
</div>
|
|
57
47
|
<div
|
|
58
48
|
className={cx(
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
dmMessageBubbleClass,
|
|
50
|
+
outbound && dmMessageBubbleOutboundClass,
|
|
61
51
|
)}
|
|
62
52
|
>
|
|
63
53
|
{message.text}
|
|
64
54
|
</div>
|
|
55
|
+
<div className={dmMessageMetaClass}>
|
|
56
|
+
<span>{message.sender.displayName}</span>
|
|
57
|
+
<span>·</span>
|
|
58
|
+
<span>{formatShortTimestamp(message.createdAt)}</span>
|
|
59
|
+
</div>
|
|
65
60
|
</div>
|
|
66
61
|
);
|
|
67
62
|
}
|
|
@@ -89,8 +84,11 @@ export function DmWorkspace({
|
|
|
89
84
|
: "No conversation selected";
|
|
90
85
|
|
|
91
86
|
return (
|
|
92
|
-
<section className={
|
|
87
|
+
<section className={dmShellClass}>
|
|
93
88
|
<aside className={dmListClass}>
|
|
89
|
+
{conversations.length === 0 ? (
|
|
90
|
+
<div className={emptyStateClass}>No conversations.</div>
|
|
91
|
+
) : null}
|
|
94
92
|
{conversations.map((conversation) => {
|
|
95
93
|
const active = conversation.id === selectedConversation?.id;
|
|
96
94
|
return (
|
|
@@ -106,70 +104,77 @@ export function DmWorkspace({
|
|
|
106
104
|
name={conversation.participant.displayName}
|
|
107
105
|
profileId={conversation.participant.id}
|
|
108
106
|
/>
|
|
109
|
-
<div className={
|
|
110
|
-
<div className={
|
|
111
|
-
<
|
|
112
|
-
|
|
107
|
+
<div className={dmListBodyClass}>
|
|
108
|
+
<div className={dmListHeaderClass}>
|
|
109
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
110
|
+
<span className={dmListNameClass}>
|
|
111
|
+
{conversation.participant.displayName}
|
|
112
|
+
</span>
|
|
113
|
+
<span className={dmListHandleClass}>
|
|
114
|
+
@{conversation.participant.handle}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
<span className={dmListTimestampClass}>
|
|
118
|
+
{formatShortTimestamp(conversation.lastMessageAt)}
|
|
119
|
+
</span>
|
|
113
120
|
</div>
|
|
114
|
-
<p className={
|
|
121
|
+
<p className={dmListPreviewClass}>
|
|
115
122
|
{conversation.lastMessagePreview}
|
|
116
123
|
</p>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
{formatShortTimestamp(conversation.lastMessageAt)}
|
|
132
|
-
</span>
|
|
124
|
+
<div className="mt-1 flex items-center gap-1.5">
|
|
125
|
+
<span
|
|
126
|
+
className={cx(
|
|
127
|
+
pillClass,
|
|
128
|
+
conversation.needsReply ? pillAlertClass : pillSoftClass,
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
{conversation.needsReply ? "needs reply" : "clear"}
|
|
132
|
+
</span>
|
|
133
|
+
<span className={cx(pillClass, pillSoftClass)}>
|
|
134
|
+
{conversation.influenceScore} ·{" "}
|
|
135
|
+
{conversation.influenceLabel}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
133
138
|
</div>
|
|
134
139
|
</button>
|
|
135
140
|
);
|
|
136
141
|
})}
|
|
137
142
|
</aside>
|
|
138
143
|
|
|
139
|
-
<div className={
|
|
144
|
+
<div className={dmThreadClass}>
|
|
140
145
|
{selectedConversation ? (
|
|
141
146
|
<>
|
|
142
|
-
<header className={
|
|
143
|
-
<div>
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<div className={threadDetailColumnClass}>
|
|
154
|
-
<div className={threadDetailHeaderClass}>
|
|
155
|
-
<AvatarChip
|
|
156
|
-
avatarUrl={participant?.avatarUrl}
|
|
157
|
-
hue={participant?.avatarHue ?? 18}
|
|
158
|
-
name={participant?.displayName ?? "Unknown"}
|
|
159
|
-
profileId={participant?.id ?? undefined}
|
|
160
|
-
size="large"
|
|
161
|
-
/>
|
|
162
|
-
<div>
|
|
163
|
-
<strong>{participant?.displayName}</strong>
|
|
164
|
-
<p className={cx("context-handle", contextHandleClass)}>
|
|
165
|
-
@{participant?.handle}
|
|
166
|
-
</p>
|
|
147
|
+
<header className={dmThreadHeaderClass}>
|
|
148
|
+
<div className={dmThreadTitleClass}>
|
|
149
|
+
<AvatarChip
|
|
150
|
+
avatarUrl={participant?.avatarUrl}
|
|
151
|
+
hue={participant?.avatarHue ?? 18}
|
|
152
|
+
name={participant?.displayName ?? "Unknown"}
|
|
153
|
+
profileId={participant?.id ?? undefined}
|
|
154
|
+
/>
|
|
155
|
+
<div className="min-w-0">
|
|
156
|
+
<div className={dmThreadNameClass}>
|
|
157
|
+
{selectedConversation.participant.displayName}
|
|
167
158
|
</div>
|
|
159
|
+
<div className={dmThreadSubtitleClass}>{subtitle}</div>
|
|
168
160
|
</div>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
161
|
+
</div>
|
|
162
|
+
<button
|
|
163
|
+
className={primaryButtonClass}
|
|
164
|
+
onClick={() => onReplySend(selectedConversation.id)}
|
|
165
|
+
type="button"
|
|
166
|
+
>
|
|
167
|
+
Reply
|
|
168
|
+
</button>
|
|
169
|
+
</header>
|
|
170
|
+
{participant?.bio || participant?.followersCount ? (
|
|
171
|
+
<div className="border-b border-[var(--line)] px-4 py-3">
|
|
172
|
+
{participant?.bio ? (
|
|
173
|
+
<p className="text-[14px] leading-[1.4] text-[var(--ink)]">
|
|
174
|
+
{participant.bio}
|
|
175
|
+
</p>
|
|
176
|
+
) : null}
|
|
177
|
+
<dl className="mt-2 grid grid-cols-2 gap-1 text-[13px]">
|
|
173
178
|
<div className={contextStatRowClass}>
|
|
174
179
|
<dt className={contextStatTermClass}>Followers</dt>
|
|
175
180
|
<dd className={contextStatValueClass}>
|
|
@@ -198,26 +203,19 @@ export function DmWorkspace({
|
|
|
198
203
|
</dd>
|
|
199
204
|
</div>
|
|
200
205
|
</dl>
|
|
201
|
-
<button
|
|
202
|
-
className={actionButtonClass}
|
|
203
|
-
onClick={() => onReplySend(selectedConversation.id)}
|
|
204
|
-
type="button"
|
|
205
|
-
>
|
|
206
|
-
Reply
|
|
207
|
-
</button>
|
|
208
206
|
</div>
|
|
209
|
-
|
|
210
|
-
<div className={
|
|
207
|
+
) : null}
|
|
208
|
+
<div className={dmMessagesClass}>
|
|
211
209
|
{selectedMessages.map((message) => (
|
|
212
210
|
<MessageBubble key={message.id} message={message} />
|
|
213
211
|
))}
|
|
214
212
|
</div>
|
|
215
|
-
<div className={
|
|
213
|
+
<div className={dmComposerShellClass}>
|
|
216
214
|
<textarea
|
|
217
215
|
className={composerInputClass}
|
|
218
216
|
onChange={(event) => onReplyDraftChange(event.target.value)}
|
|
219
217
|
placeholder={`Reply to @${selectedConversation.participant.handle}`}
|
|
220
|
-
rows={
|
|
218
|
+
rows={3}
|
|
221
219
|
value={replyDraft}
|
|
222
220
|
/>
|
|
223
221
|
<div className={composerBarClass}>
|
|
@@ -227,7 +225,7 @@ export function DmWorkspace({
|
|
|
227
225
|
: "Thread clear"}
|
|
228
226
|
</span>
|
|
229
227
|
<button
|
|
230
|
-
className={
|
|
228
|
+
className={primaryButtonClass}
|
|
231
229
|
disabled={!replyDraft.trim()}
|
|
232
230
|
onClick={() => onReplySend(selectedConversation.id)}
|
|
233
231
|
type="button"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { formatShortTimestamp } from "#/lib/present";
|
|
2
2
|
import type { EmbeddedTweet } from "#/lib/types";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
embeddedCardBodyClass,
|
|
5
|
+
embeddedCardCopyClass,
|
|
6
|
+
embeddedCardHandleClass,
|
|
7
|
+
embeddedCardHeaderClass,
|
|
8
|
+
embeddedCardLabelClass,
|
|
9
|
+
embeddedCardNameClass,
|
|
10
|
+
feedRowTimestampClass,
|
|
10
11
|
} from "#/lib/ui";
|
|
11
12
|
import { ProfilePreview } from "./ProfilePreview";
|
|
12
13
|
import { TweetMediaGrid } from "./TweetMediaGrid";
|
|
@@ -20,21 +21,26 @@ export function EmbeddedTweetCard({
|
|
|
20
21
|
label: string;
|
|
21
22
|
}) {
|
|
22
23
|
return (
|
|
23
|
-
<section className={
|
|
24
|
-
<p className={
|
|
25
|
-
<header className={
|
|
24
|
+
<section className={embeddedCardBodyClass}>
|
|
25
|
+
<p className={embeddedCardLabelClass}>{label}</p>
|
|
26
|
+
<header className={embeddedCardHeaderClass}>
|
|
26
27
|
<ProfilePreview profile={item.author}>
|
|
27
|
-
<span className=
|
|
28
|
-
<
|
|
29
|
-
|
|
28
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
29
|
+
<span className={embeddedCardNameClass}>
|
|
30
|
+
{item.author.displayName}
|
|
31
|
+
</span>
|
|
32
|
+
<span className={embeddedCardHandleClass}>
|
|
33
|
+
@{item.author.handle}
|
|
34
|
+
</span>
|
|
30
35
|
</span>
|
|
31
36
|
</ProfilePreview>
|
|
32
|
-
<span className=
|
|
37
|
+
<span className="text-[var(--ink-soft)]">·</span>
|
|
38
|
+
<span className={feedRowTimestampClass}>
|
|
33
39
|
{formatShortTimestamp(item.createdAt)}
|
|
34
40
|
</span>
|
|
35
41
|
</header>
|
|
36
42
|
<TweetRichText
|
|
37
|
-
className={
|
|
43
|
+
className={embeddedCardCopyClass}
|
|
38
44
|
entities={item.entities}
|
|
39
45
|
text={item.text}
|
|
40
46
|
/>
|