xfeed 0.1.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/LICENSE +23 -0
- package/README.md +5 -0
- package/package.json +43 -0
- package/src/api/actions.ts +16 -0
- package/src/api/client.test.ts +3370 -0
- package/src/api/client.ts +4319 -0
- package/src/api/query-ids.json +11 -0
- package/src/api/query-ids.test.ts +118 -0
- package/src/api/query-ids.ts +59 -0
- package/src/api/runtime-query-ids.test.ts +926 -0
- package/src/api/runtime-query-ids.ts +389 -0
- package/src/api/types.ts +581 -0
- package/src/app.tsx +664 -0
- package/src/auth/browser-detect.ts +150 -0
- package/src/auth/browser-picker.ts +118 -0
- package/src/auth/check.test.preload.ts +94 -0
- package/src/auth/check.test.ts +388 -0
- package/src/auth/check.ts +220 -0
- package/src/auth/cookies.test.ts +529 -0
- package/src/auth/cookies.ts +299 -0
- package/src/auth/manual-entry.ts +88 -0
- package/src/auth/session.ts +30 -0
- package/src/components/ErrorBanner.tsx +172 -0
- package/src/components/Footer.tsx +90 -0
- package/src/components/Header.tsx +57 -0
- package/src/components/NotificationItem.test.ts +252 -0
- package/src/components/NotificationItem.tsx +80 -0
- package/src/components/NotificationList.test.ts +328 -0
- package/src/components/NotificationList.tsx +157 -0
- package/src/components/PostCard.tsx +186 -0
- package/src/components/PostList.tsx +232 -0
- package/src/components/QuotedPostCard.tsx +55 -0
- package/src/components/ReplyPreviewCard.tsx +80 -0
- package/src/components/ThreadView.prototype.tsx +533 -0
- package/src/components/Toast.tsx +28 -0
- package/src/config/loader.ts +69 -0
- package/src/config/types.ts +27 -0
- package/src/contexts/ModalContext.tsx +227 -0
- package/src/experiments/TimelineScreenExperimental.tsx +202 -0
- package/src/experiments/index.tsx +43 -0
- package/src/experiments/query-client.ts +132 -0
- package/src/experiments/use-bookmark-mutation.ts +342 -0
- package/src/experiments/use-bookmarks-query.ts +166 -0
- package/src/experiments/use-notifications-query.ts +368 -0
- package/src/experiments/use-post-detail-query.ts +187 -0
- package/src/experiments/use-profile-query.ts +162 -0
- package/src/experiments/use-timeline-query.ts +201 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/useActions.ts +354 -0
- package/src/hooks/useBookmarkFolders.ts +70 -0
- package/src/hooks/useBookmarks.ts +111 -0
- package/src/hooks/useCountdown.ts +75 -0
- package/src/hooks/useListNavigation.test.ts +273 -0
- package/src/hooks/useListNavigation.ts +118 -0
- package/src/hooks/useNavigation.test.ts +340 -0
- package/src/hooks/useNavigation.ts +103 -0
- package/src/hooks/useNotifications.test.ts +377 -0
- package/src/hooks/useNotifications.ts +117 -0
- package/src/hooks/usePaginatedData.ts +217 -0
- package/src/hooks/usePostDetail.ts +137 -0
- package/src/hooks/useThread.prototype.ts +314 -0
- package/src/hooks/useTimeline.ts +136 -0
- package/src/hooks/useUserProfile.ts +142 -0
- package/src/index.tsx +304 -0
- package/src/lib/colors.ts +41 -0
- package/src/lib/format.ts +69 -0
- package/src/lib/media.ts +464 -0
- package/src/lib/result.ts +6 -0
- package/src/lib/text.tsx +76 -0
- package/src/modals/BookmarkFolderSelector.tsx +260 -0
- package/src/modals/ExitConfirmationModal.tsx +131 -0
- package/src/modals/FolderPicker.tsx +281 -0
- package/src/modals/README.md +171 -0
- package/src/modals/SessionExpiredModal.tsx +47 -0
- package/src/modals/index.ts +4 -0
- package/src/screens/.gitkeep +0 -0
- package/src/screens/BookmarksScreen.tsx +168 -0
- package/src/screens/NotificationsScreen.tsx +172 -0
- package/src/screens/PostDetailScreen.tsx +976 -0
- package/src/screens/ProfileScreen.tsx +528 -0
- package/src/screens/SplashScreen.tsx +72 -0
- package/src/screens/ThreadScreen.tsx +81 -0
- package/src/screens/TimelineScreen.tsx +188 -0
- package/vendor/sweet-cookie/LICENSE +22 -0
- package/vendor/sweet-cookie/README.md +29 -0
- package/vendor/sweet-cookie/dist/index.d.ts +3 -0
- package/vendor/sweet-cookie/dist/index.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/index.js +2 -0
- package/vendor/sweet-cookie/dist/index.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chrome.d.ts +10 -0
- package/vendor/sweet-cookie/dist/providers/chrome.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chrome.js +27 -0
- package/vendor/sweet-cookie/dist/providers/chrome.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts +11 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.js +100 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts +25 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js +104 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts +10 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.js +293 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts +10 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js +26 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts +7 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.js +51 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.d.ts +10 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.js +118 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts +7 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.js +38 -0
- package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts +5 -0
- package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.js +33 -0
- package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts +24 -0
- package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.js +30 -0
- package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/paths.d.ts +11 -0
- package/vendor/sweet-cookie/dist/providers/chromium/paths.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/paths.js +43 -0
- package/vendor/sweet-cookie/dist/providers/chromium/paths.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.js +41 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.js +53 -0
- package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edge.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/edge.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edge.js +27 -0
- package/vendor/sweet-cookie/dist/providers/edge.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts +7 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.js +53 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.js +60 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts +7 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.js +38 -0
- package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/firefoxSqlite.d.ts +6 -0
- package/vendor/sweet-cookie/dist/providers/firefoxSqlite.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/firefoxSqlite.js +257 -0
- package/vendor/sweet-cookie/dist/providers/firefoxSqlite.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/inline.d.ts +8 -0
- package/vendor/sweet-cookie/dist/providers/inline.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/inline.js +71 -0
- package/vendor/sweet-cookie/dist/providers/inline.js.map +1 -0
- package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.d.ts +6 -0
- package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.js +173 -0
- package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.js.map +1 -0
- package/vendor/sweet-cookie/dist/public.d.ts +26 -0
- package/vendor/sweet-cookie/dist/public.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/public.js +197 -0
- package/vendor/sweet-cookie/dist/public.js.map +1 -0
- package/vendor/sweet-cookie/dist/types.d.ts +127 -0
- package/vendor/sweet-cookie/dist/types.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/types.js +2 -0
- package/vendor/sweet-cookie/dist/types.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/base64.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/base64.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/base64.js +18 -0
- package/vendor/sweet-cookie/dist/util/base64.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/exec.d.ts +8 -0
- package/vendor/sweet-cookie/dist/util/exec.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/exec.js +110 -0
- package/vendor/sweet-cookie/dist/util/exec.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/expire.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/expire.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/expire.js +32 -0
- package/vendor/sweet-cookie/dist/util/expire.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/fs.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/fs.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/fs.js +13 -0
- package/vendor/sweet-cookie/dist/util/fs.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/hostMatch.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/hostMatch.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/hostMatch.js +7 -0
- package/vendor/sweet-cookie/dist/util/hostMatch.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/nodeSqlite.d.ts +5 -0
- package/vendor/sweet-cookie/dist/util/nodeSqlite.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/nodeSqlite.js +58 -0
- package/vendor/sweet-cookie/dist/util/nodeSqlite.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/origins.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/origins.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/origins.js +27 -0
- package/vendor/sweet-cookie/dist/util/origins.js.map +1 -0
- package/vendor/sweet-cookie/dist/util/runtime.d.ts +2 -0
- package/vendor/sweet-cookie/dist/util/runtime.d.ts.map +1 -0
- package/vendor/sweet-cookie/dist/util/runtime.js +8 -0
- package/vendor/sweet-cookie/dist/util/runtime.js.map +1 -0
- package/vendor/sweet-cookie/package.json +40 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProfileScreen - User profile view with bio and recent tweets
|
|
3
|
+
* Supports collapsible header when scrolling through tweets
|
|
4
|
+
* When viewing own profile (isSelf), shows tabs for Tweets/Likes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useKeyboard } from "@opentui/react";
|
|
8
|
+
import { useState, useCallback, useEffect } from "react";
|
|
9
|
+
|
|
10
|
+
import type { XClient } from "@/api/client";
|
|
11
|
+
import type { TweetData, UserData } from "@/api/types";
|
|
12
|
+
import type { TweetActionState } from "@/hooks/useActions";
|
|
13
|
+
|
|
14
|
+
import { Footer, type Keybinding } from "@/components/Footer";
|
|
15
|
+
import { PostList } from "@/components/PostList";
|
|
16
|
+
import { useUserProfile } from "@/hooks/useUserProfile";
|
|
17
|
+
import { colors } from "@/lib/colors";
|
|
18
|
+
import { formatCount } from "@/lib/format";
|
|
19
|
+
import { openInBrowser, previewImageUrl } from "@/lib/media";
|
|
20
|
+
import { extractMentions, renderTextWithMentions } from "@/lib/text";
|
|
21
|
+
|
|
22
|
+
type ProfileTab = "tweets" | "likes";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Format X's created_at date to "Joined Month Year"
|
|
26
|
+
*/
|
|
27
|
+
function formatJoinDate(createdAt: string | undefined): string | undefined {
|
|
28
|
+
if (!createdAt) return undefined;
|
|
29
|
+
try {
|
|
30
|
+
const date = new Date(createdAt);
|
|
31
|
+
if (Number.isNaN(date.getTime())) return undefined;
|
|
32
|
+
const month = date.toLocaleDateString("en-US", { month: "long" });
|
|
33
|
+
const year = date.getFullYear();
|
|
34
|
+
return `Joined ${month} ${year}`;
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract display domain from a URL
|
|
42
|
+
*/
|
|
43
|
+
function extractDomain(url: string | undefined): string | undefined {
|
|
44
|
+
if (!url) return undefined;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = new URL(url);
|
|
47
|
+
return parsed.hostname.replace(/^www\./, "");
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ProfileScreenProps {
|
|
54
|
+
client: XClient;
|
|
55
|
+
username: string;
|
|
56
|
+
/** Current logged-in user (used to detect self-view) */
|
|
57
|
+
currentUser?: UserData;
|
|
58
|
+
focused?: boolean;
|
|
59
|
+
onBack?: () => void;
|
|
60
|
+
onPostSelect?: (post: TweetData) => void;
|
|
61
|
+
/** Called when user navigates to a mentioned profile */
|
|
62
|
+
onProfileOpen?: (username: string) => void;
|
|
63
|
+
/** Called when user presses 'l' to toggle like */
|
|
64
|
+
onLike?: (post: TweetData) => void;
|
|
65
|
+
/** Called when user presses 'b' to toggle bookmark */
|
|
66
|
+
onBookmark?: (post: TweetData) => void;
|
|
67
|
+
/** Get current action state for a tweet */
|
|
68
|
+
getActionState?: (tweetId: string) => TweetActionState;
|
|
69
|
+
/** Initialize action state from API data */
|
|
70
|
+
initActionState?: (
|
|
71
|
+
tweetId: string,
|
|
72
|
+
liked: boolean,
|
|
73
|
+
bookmarked: boolean
|
|
74
|
+
) => void;
|
|
75
|
+
/** Whether to show the footer */
|
|
76
|
+
showFooter?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function ProfileScreen({
|
|
80
|
+
client,
|
|
81
|
+
username,
|
|
82
|
+
currentUser,
|
|
83
|
+
focused = false,
|
|
84
|
+
onBack,
|
|
85
|
+
onPostSelect,
|
|
86
|
+
onProfileOpen,
|
|
87
|
+
onLike,
|
|
88
|
+
onBookmark,
|
|
89
|
+
getActionState,
|
|
90
|
+
initActionState,
|
|
91
|
+
showFooter = true,
|
|
92
|
+
}: ProfileScreenProps) {
|
|
93
|
+
// Detect if viewing own profile
|
|
94
|
+
const isSelf = Boolean(
|
|
95
|
+
currentUser && username.toLowerCase() === currentUser.username.toLowerCase()
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const {
|
|
99
|
+
user,
|
|
100
|
+
tweets,
|
|
101
|
+
loading,
|
|
102
|
+
error,
|
|
103
|
+
refresh,
|
|
104
|
+
likedTweets,
|
|
105
|
+
likesLoading,
|
|
106
|
+
likesError,
|
|
107
|
+
fetchLikes,
|
|
108
|
+
likesFetched,
|
|
109
|
+
} = useUserProfile({
|
|
110
|
+
client,
|
|
111
|
+
username,
|
|
112
|
+
isSelf,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Tab state (only used when isSelf)
|
|
116
|
+
const [activeTab, setActiveTab] = useState<ProfileTab>("tweets");
|
|
117
|
+
|
|
118
|
+
// Track if header should be collapsed (when scrolled past first tweet)
|
|
119
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
120
|
+
|
|
121
|
+
// Mentions navigation state
|
|
122
|
+
const [mentionsMode, setMentionsMode] = useState(false);
|
|
123
|
+
const [mentionIndex, setMentionIndex] = useState(0);
|
|
124
|
+
|
|
125
|
+
// Fetch likes when switching to likes tab for the first time
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (isSelf && activeTab === "likes" && !likesFetched && !likesLoading) {
|
|
128
|
+
fetchLikes();
|
|
129
|
+
}
|
|
130
|
+
}, [isSelf, activeTab, likesFetched, likesLoading, fetchLikes]);
|
|
131
|
+
|
|
132
|
+
// Extract mentions from bio
|
|
133
|
+
const bioMentions = user?.description
|
|
134
|
+
? extractMentions(user.description)
|
|
135
|
+
: [];
|
|
136
|
+
const hasMentions = bioMentions.length > 0;
|
|
137
|
+
const mentionCount = bioMentions.length;
|
|
138
|
+
const currentMention = hasMentions ? bioMentions[mentionIndex] : undefined;
|
|
139
|
+
|
|
140
|
+
const handleSelectedIndexChange = useCallback((index: number) => {
|
|
141
|
+
setIsCollapsed(index > 0);
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
// Handle keyboard shortcuts
|
|
145
|
+
useKeyboard((key) => {
|
|
146
|
+
if (!focused) return;
|
|
147
|
+
|
|
148
|
+
// Handle mentions mode navigation
|
|
149
|
+
if (mentionsMode && hasMentions) {
|
|
150
|
+
// Exit mentions mode with escape or h
|
|
151
|
+
if (key.name === "escape" || key.name === "h") {
|
|
152
|
+
setMentionsMode(false);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Navigate mentions with j/k
|
|
156
|
+
if (key.name === "j" || key.name === "down") {
|
|
157
|
+
if (mentionIndex < mentionCount - 1) {
|
|
158
|
+
setMentionIndex((prev) => prev + 1);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (key.name === "k" || key.name === "up") {
|
|
163
|
+
if (mentionIndex > 0) {
|
|
164
|
+
setMentionIndex((prev) => prev - 1);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Open mention profile with Enter
|
|
169
|
+
if (key.name === "return") {
|
|
170
|
+
if (currentMention) {
|
|
171
|
+
onProfileOpen?.(currentMention);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Other keys exit mentions mode and proceed
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
switch (key.name) {
|
|
179
|
+
case "escape":
|
|
180
|
+
case "backspace":
|
|
181
|
+
case "h":
|
|
182
|
+
onBack?.();
|
|
183
|
+
break;
|
|
184
|
+
case "r":
|
|
185
|
+
refresh();
|
|
186
|
+
break;
|
|
187
|
+
case "a":
|
|
188
|
+
// Open avatar/profile photo in Quick Look
|
|
189
|
+
if (user?.profileImageUrl) {
|
|
190
|
+
previewImageUrl(user.profileImageUrl, `profile_${user.username}`);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case "v":
|
|
194
|
+
// View banner image in Quick Look
|
|
195
|
+
if (user?.bannerImageUrl) {
|
|
196
|
+
previewImageUrl(user.bannerImageUrl, `banner_${user.username}`);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
case "w":
|
|
200
|
+
// Open website in browser
|
|
201
|
+
if (user?.websiteUrl) {
|
|
202
|
+
openInBrowser(user.websiteUrl);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
case "x":
|
|
206
|
+
// Open profile on x.com
|
|
207
|
+
if (user?.username) {
|
|
208
|
+
openInBrowser(`https://x.com/${user.username}`);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
case "m":
|
|
212
|
+
// Handle mentions: single = direct profile, multiple = enter mode
|
|
213
|
+
if (hasMentions && !mentionsMode) {
|
|
214
|
+
if (mentionCount === 1) {
|
|
215
|
+
// Single mention - open profile directly
|
|
216
|
+
const mention = bioMentions[0];
|
|
217
|
+
if (mention) {
|
|
218
|
+
onProfileOpen?.(mention);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
// Multiple mentions - enter navigation mode
|
|
222
|
+
setMentionsMode(true);
|
|
223
|
+
setMentionIndex(0);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case "1":
|
|
228
|
+
// Switch to tweets tab (only on own profile)
|
|
229
|
+
if (isSelf && activeTab !== "tweets") {
|
|
230
|
+
setActiveTab("tweets");
|
|
231
|
+
setIsCollapsed(false);
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
case "2":
|
|
235
|
+
// Switch to likes tab (only on own profile)
|
|
236
|
+
if (isSelf && activeTab !== "likes") {
|
|
237
|
+
setActiveTab("likes");
|
|
238
|
+
setIsCollapsed(false);
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Compact header for collapsed state - just name and handle
|
|
245
|
+
const compactHeader = user && (
|
|
246
|
+
<box
|
|
247
|
+
style={{
|
|
248
|
+
flexShrink: 0,
|
|
249
|
+
paddingLeft: 1,
|
|
250
|
+
paddingRight: 1,
|
|
251
|
+
flexDirection: "row",
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
<text fg={colors.dim}>{mentionsMode ? "<- Back (Esc) | " : "<- "}</text>
|
|
255
|
+
{mentionsMode && <text fg={colors.primary}>Navigating mentions </text>}
|
|
256
|
+
{isSelf && <text fg={colors.primary}>My Profile </text>}
|
|
257
|
+
<text fg="#ffffff">
|
|
258
|
+
<b>{user.name}</b>
|
|
259
|
+
</text>
|
|
260
|
+
{user.isBlueVerified && <text fg={colors.primary}> {"\u2713"}</text>}
|
|
261
|
+
<text fg={colors.muted}> @{user.username}</text>
|
|
262
|
+
</box>
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Full profile header with back hint, bio, and stats
|
|
266
|
+
const joinDate = formatJoinDate(user?.createdAt);
|
|
267
|
+
const websiteDomain = extractDomain(user?.websiteUrl);
|
|
268
|
+
|
|
269
|
+
const fullHeader = user && (
|
|
270
|
+
<box
|
|
271
|
+
style={{
|
|
272
|
+
flexShrink: 0,
|
|
273
|
+
flexDirection: "column",
|
|
274
|
+
justifyContent: "flex-start",
|
|
275
|
+
marginBottom: 0,
|
|
276
|
+
paddingBottom: 0,
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{/* Back hint + Name + Handle on same line */}
|
|
280
|
+
<box style={{ paddingLeft: 1, paddingRight: 1, flexDirection: "row" }}>
|
|
281
|
+
<text fg={colors.dim}>{mentionsMode ? "<- Back (Esc) | " : "<- "}</text>
|
|
282
|
+
{mentionsMode && <text fg={colors.primary}>Navigating mentions </text>}
|
|
283
|
+
{isSelf && <text fg={colors.primary}>My Profile </text>}
|
|
284
|
+
<text fg="#ffffff">
|
|
285
|
+
<b>{user.name}</b>
|
|
286
|
+
</text>
|
|
287
|
+
{user.isBlueVerified && <text fg={colors.primary}> {"\u2713"}</text>}
|
|
288
|
+
<text fg={colors.muted}> @{user.username}</text>
|
|
289
|
+
</box>
|
|
290
|
+
|
|
291
|
+
{/* Bio - highlight @mentions in blue */}
|
|
292
|
+
{user.description && (
|
|
293
|
+
<box style={{ paddingLeft: 1, paddingRight: 1 }}>
|
|
294
|
+
{hasMentions ? (
|
|
295
|
+
renderTextWithMentions(
|
|
296
|
+
user.description.trim(),
|
|
297
|
+
colors.primary,
|
|
298
|
+
"#cccccc"
|
|
299
|
+
)
|
|
300
|
+
) : (
|
|
301
|
+
<text fg="#cccccc">{user.description.trim()}</text>
|
|
302
|
+
)}
|
|
303
|
+
</box>
|
|
304
|
+
)}
|
|
305
|
+
|
|
306
|
+
{/* Location, Website, Join Date row */}
|
|
307
|
+
{(user.location || websiteDomain || joinDate) && (
|
|
308
|
+
<box style={{ paddingLeft: 1, paddingRight: 1, flexDirection: "row" }}>
|
|
309
|
+
{user.location && (
|
|
310
|
+
<>
|
|
311
|
+
<text fg={colors.muted}>{"\u{1F4CD}"} </text>
|
|
312
|
+
<text fg="#aaaaaa">{user.location}</text>
|
|
313
|
+
</>
|
|
314
|
+
)}
|
|
315
|
+
{user.location && websiteDomain && (
|
|
316
|
+
<text fg={colors.dim}> {"\u00B7"} </text>
|
|
317
|
+
)}
|
|
318
|
+
{websiteDomain && (
|
|
319
|
+
<>
|
|
320
|
+
<text fg={colors.muted}>{"\u{1F517}"} </text>
|
|
321
|
+
<text fg={colors.primary}>{websiteDomain}</text>
|
|
322
|
+
</>
|
|
323
|
+
)}
|
|
324
|
+
{(user.location || websiteDomain) && joinDate && (
|
|
325
|
+
<text fg={colors.dim}> {"\u00B7"} </text>
|
|
326
|
+
)}
|
|
327
|
+
{joinDate && (
|
|
328
|
+
<>
|
|
329
|
+
<text fg={colors.muted}>{"\u{1F4C5}"} </text>
|
|
330
|
+
<text fg="#aaaaaa">{joinDate}</text>
|
|
331
|
+
</>
|
|
332
|
+
)}
|
|
333
|
+
</box>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
{/* Stats */}
|
|
337
|
+
<box style={{ paddingLeft: 1, paddingRight: 1, flexDirection: "row" }}>
|
|
338
|
+
<text fg="#ffffff">{formatCount(user.followersCount)}</text>
|
|
339
|
+
<text fg={colors.muted}> Followers </text>
|
|
340
|
+
<text fg={colors.dim}>{"\u00B7"} </text>
|
|
341
|
+
<text fg="#ffffff">{formatCount(user.followingCount)}</text>
|
|
342
|
+
<text fg={colors.muted}> Following</text>
|
|
343
|
+
</box>
|
|
344
|
+
|
|
345
|
+
{/* Mentions section - simplified UI based on count */}
|
|
346
|
+
{hasMentions && (
|
|
347
|
+
<box
|
|
348
|
+
style={{ paddingLeft: 1, paddingRight: 1, flexDirection: "column" }}
|
|
349
|
+
>
|
|
350
|
+
{mentionCount === 1 ? (
|
|
351
|
+
// Single mention - show directly with profile shortcut
|
|
352
|
+
<box style={{ flexDirection: "row" }}>
|
|
353
|
+
<text fg={colors.dim}>Mentions: </text>
|
|
354
|
+
<text fg={colors.primary}>@{bioMentions[0]}</text>
|
|
355
|
+
<text fg={colors.dim}> (</text>
|
|
356
|
+
<text fg={colors.primary}>m</text>
|
|
357
|
+
<text fg={colors.dim}> profile)</text>
|
|
358
|
+
</box>
|
|
359
|
+
) : mentionsMode ? (
|
|
360
|
+
// Multiple mentions - navigation mode active
|
|
361
|
+
<>
|
|
362
|
+
<box style={{ flexDirection: "row" }}>
|
|
363
|
+
<text fg={colors.dim}>Mentions ({mentionCount}): </text>
|
|
364
|
+
<text fg={colors.primary}>j/k</text>
|
|
365
|
+
<text fg={colors.dim}> navigate </text>
|
|
366
|
+
<text fg={colors.primary}>Enter</text>
|
|
367
|
+
<text fg={colors.dim}> profile</text>
|
|
368
|
+
</box>
|
|
369
|
+
{bioMentions.map((mention, idx) => {
|
|
370
|
+
const isSelected = idx === mentionIndex;
|
|
371
|
+
return (
|
|
372
|
+
<box key={mention} style={{ flexDirection: "row" }}>
|
|
373
|
+
<text fg={isSelected ? colors.primary : colors.muted}>
|
|
374
|
+
{isSelected ? ">" : " "} @{mention}
|
|
375
|
+
</text>
|
|
376
|
+
</box>
|
|
377
|
+
);
|
|
378
|
+
})}
|
|
379
|
+
</>
|
|
380
|
+
) : (
|
|
381
|
+
// Multiple mentions - collapsed view
|
|
382
|
+
<box style={{ flexDirection: "row" }}>
|
|
383
|
+
<text fg={colors.dim}>Mentions: </text>
|
|
384
|
+
<text fg={colors.primary}>@{bioMentions[0]}</text>
|
|
385
|
+
<text fg={colors.dim}> +{mentionCount - 1} more (</text>
|
|
386
|
+
<text fg={colors.primary}>m</text>
|
|
387
|
+
<text fg={colors.dim}> to navigate)</text>
|
|
388
|
+
</box>
|
|
389
|
+
)}
|
|
390
|
+
</box>
|
|
391
|
+
)}
|
|
392
|
+
</box>
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Tab bar for own profile (Tweets | Likes)
|
|
396
|
+
const tabBar = isSelf && (
|
|
397
|
+
<box
|
|
398
|
+
style={{
|
|
399
|
+
paddingLeft: 1,
|
|
400
|
+
paddingRight: 1,
|
|
401
|
+
flexShrink: 0,
|
|
402
|
+
flexDirection: "row",
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
<text fg={activeTab === "tweets" ? colors.primary : colors.dim}>
|
|
406
|
+
{activeTab === "tweets" ? <b>[1] Tweets</b> : " 1 Tweets"}
|
|
407
|
+
</text>
|
|
408
|
+
<text fg={colors.dim}> | </text>
|
|
409
|
+
<text fg={activeTab === "likes" ? colors.primary : colors.dim}>
|
|
410
|
+
{activeTab === "likes" ? <b>[2] Likes</b> : " 2 Likes"}
|
|
411
|
+
</text>
|
|
412
|
+
{activeTab === "likes" && likesLoading && (
|
|
413
|
+
<text fg={colors.muted}> (loading...)</text>
|
|
414
|
+
)}
|
|
415
|
+
</box>
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Separator
|
|
419
|
+
const separator = (
|
|
420
|
+
<box
|
|
421
|
+
style={{
|
|
422
|
+
paddingLeft: 1,
|
|
423
|
+
paddingRight: 1,
|
|
424
|
+
flexShrink: 0,
|
|
425
|
+
marginTop: 0,
|
|
426
|
+
marginBottom: 0,
|
|
427
|
+
paddingTop: 0,
|
|
428
|
+
paddingBottom: 0,
|
|
429
|
+
}}
|
|
430
|
+
>
|
|
431
|
+
<text fg="#444444">{"─".repeat(50)}</text>
|
|
432
|
+
</box>
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// Determine which posts to show based on active tab
|
|
436
|
+
const displayPosts = isSelf && activeTab === "likes" ? likedTweets : tweets;
|
|
437
|
+
const displayError = isSelf && activeTab === "likes" ? likesError : null;
|
|
438
|
+
|
|
439
|
+
// Footer keybindings - show available actions based on what data exists
|
|
440
|
+
// Tab keybindings (1/2) are shown in the tab bar itself, not in footer
|
|
441
|
+
const footerBindings: Keybinding[] = [
|
|
442
|
+
{ key: "h/Esc", label: "back" },
|
|
443
|
+
{ key: "j/k", label: "nav" },
|
|
444
|
+
{ key: "l", label: "like" },
|
|
445
|
+
{ key: "b", label: "bkmk" },
|
|
446
|
+
{
|
|
447
|
+
key: "m",
|
|
448
|
+
label: mentionCount === 1 ? "@profile" : "mentions",
|
|
449
|
+
show: hasMentions && !mentionsMode,
|
|
450
|
+
},
|
|
451
|
+
{ key: "a", label: "avatar", show: !!user?.profileImageUrl },
|
|
452
|
+
{ key: "v", label: "banner", show: !!user?.bannerImageUrl },
|
|
453
|
+
{ key: "w", label: "web", show: !!user?.websiteUrl },
|
|
454
|
+
{ key: "x", label: "x.com" },
|
|
455
|
+
{ key: "r", label: "refresh" },
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
// Loading state
|
|
459
|
+
if (loading) {
|
|
460
|
+
return (
|
|
461
|
+
<box style={{ flexDirection: "column", height: "100%" }}>
|
|
462
|
+
<box style={{ padding: 1, flexDirection: "row" }}>
|
|
463
|
+
<text fg={colors.dim}>{"<- "}</text>
|
|
464
|
+
<text fg={colors.muted}>Loading profile...</text>
|
|
465
|
+
</box>
|
|
466
|
+
</box>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Error state
|
|
471
|
+
if (error) {
|
|
472
|
+
return (
|
|
473
|
+
<box style={{ flexDirection: "column", height: "100%" }}>
|
|
474
|
+
<box style={{ padding: 1, flexDirection: "row" }}>
|
|
475
|
+
<text fg={colors.dim}>{"<- "}</text>
|
|
476
|
+
<text fg="#ff6666">Error: {error}</text>
|
|
477
|
+
</box>
|
|
478
|
+
</box>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// User not found
|
|
483
|
+
if (!user) {
|
|
484
|
+
return (
|
|
485
|
+
<box style={{ flexDirection: "column", height: "100%" }}>
|
|
486
|
+
<box style={{ padding: 1, flexDirection: "row" }}>
|
|
487
|
+
<text fg={colors.dim}>{"<- "}</text>
|
|
488
|
+
<text fg={colors.muted}>User not found</text>
|
|
489
|
+
</box>
|
|
490
|
+
</box>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Empty state message based on active tab
|
|
495
|
+
const emptyMessage =
|
|
496
|
+
isSelf && activeTab === "likes"
|
|
497
|
+
? "No liked tweets"
|
|
498
|
+
: "No tweets to display";
|
|
499
|
+
|
|
500
|
+
return (
|
|
501
|
+
<box style={{ flexDirection: "column", height: "100%" }}>
|
|
502
|
+
{isCollapsed ? compactHeader : fullHeader}
|
|
503
|
+
{tabBar}
|
|
504
|
+
{separator}
|
|
505
|
+
{displayError ? (
|
|
506
|
+
<box style={{ padding: 1, flexGrow: 1 }}>
|
|
507
|
+
<text fg="#ff6666">Error: {displayError}</text>
|
|
508
|
+
</box>
|
|
509
|
+
) : displayPosts.length > 0 ? (
|
|
510
|
+
<PostList
|
|
511
|
+
posts={displayPosts}
|
|
512
|
+
focused={focused}
|
|
513
|
+
onPostSelect={onPostSelect}
|
|
514
|
+
onSelectedIndexChange={handleSelectedIndexChange}
|
|
515
|
+
onLike={onLike}
|
|
516
|
+
onBookmark={onBookmark}
|
|
517
|
+
getActionState={getActionState}
|
|
518
|
+
initActionState={initActionState}
|
|
519
|
+
/>
|
|
520
|
+
) : (
|
|
521
|
+
<box style={{ padding: 1, flexGrow: 1 }}>
|
|
522
|
+
<text fg={colors.muted}>{emptyMessage}</text>
|
|
523
|
+
</box>
|
|
524
|
+
)}
|
|
525
|
+
<Footer bindings={footerBindings} visible={showFooter} />
|
|
526
|
+
</box>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SplashScreen - Displays during initial app loading
|
|
3
|
+
* Shows xfeed branding with animated spinner
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
|
|
8
|
+
import { colors } from "@/lib/colors";
|
|
9
|
+
|
|
10
|
+
// ASCII art logo
|
|
11
|
+
const LOGO = [
|
|
12
|
+
" ██╗ ██╗███████╗███████╗███████╗██████╗ ",
|
|
13
|
+
" ╚██╗██╔╝██╔════╝██╔════╝██╔════╝██╔══██╗",
|
|
14
|
+
" ╚███╔╝ █████╗ █████╗ █████╗ ██║ ██║",
|
|
15
|
+
" ██╔██╗ ██╔══╝ ██╔══╝ ██╔══╝ ██║ ██║",
|
|
16
|
+
" ██╔╝ ██╗██║ ███████╗███████╗██████╔╝",
|
|
17
|
+
" ╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝╚═════╝ ",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const SPINNER_FRAMES = ["◢", "◣", "◤", "◥"];
|
|
21
|
+
const SPINNER_INTERVAL_MS = 100;
|
|
22
|
+
|
|
23
|
+
export function SplashScreen() {
|
|
24
|
+
const [spinnerIndex, setSpinnerIndex] = useState(0);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const interval = setInterval(() => {
|
|
28
|
+
setSpinnerIndex((prev) => (prev + 1) % SPINNER_FRAMES.length);
|
|
29
|
+
}, SPINNER_INTERVAL_MS);
|
|
30
|
+
|
|
31
|
+
return () => clearInterval(interval);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<box
|
|
36
|
+
style={{
|
|
37
|
+
flexDirection: "column",
|
|
38
|
+
height: "100%",
|
|
39
|
+
justifyContent: "center",
|
|
40
|
+
alignItems: "center",
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{/* Top border */}
|
|
44
|
+
<text fg={colors.dim}>{"─".repeat(52)}</text>
|
|
45
|
+
|
|
46
|
+
<box style={{ marginTop: 1 }} />
|
|
47
|
+
|
|
48
|
+
{/* Logo */}
|
|
49
|
+
{LOGO.map((line, i) => (
|
|
50
|
+
<text key={i} fg="#ffffff">
|
|
51
|
+
{line}
|
|
52
|
+
</text>
|
|
53
|
+
))}
|
|
54
|
+
|
|
55
|
+
<box style={{ marginTop: 1 }} />
|
|
56
|
+
|
|
57
|
+
{/* Tagline */}
|
|
58
|
+
<text fg={colors.dim}>{"[terminal client for X, everything app]"}</text>
|
|
59
|
+
|
|
60
|
+
<box style={{ marginTop: 1 }} />
|
|
61
|
+
|
|
62
|
+
{/* Bottom border */}
|
|
63
|
+
<text fg={colors.dim}>{"─".repeat(52)}</text>
|
|
64
|
+
|
|
65
|
+
{/* Spinner and loading text */}
|
|
66
|
+
<box style={{ marginTop: 2, flexDirection: "row" }}>
|
|
67
|
+
<text fg="#ffffff">{SPINNER_FRAMES[spinnerIndex]}</text>
|
|
68
|
+
<text fg={colors.dim}> initializing...</text>
|
|
69
|
+
</box>
|
|
70
|
+
</box>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThreadScreen - Full thread view with visual hierarchy
|
|
3
|
+
*
|
|
4
|
+
* Experimental screen for Issue #80.
|
|
5
|
+
* Shows ancestor chain, focused tweet, and reply tree with collapse/expand.
|
|
6
|
+
*
|
|
7
|
+
* @experimental
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { XClient } from "@/api/client";
|
|
11
|
+
import type { TweetData } from "@/api/types";
|
|
12
|
+
|
|
13
|
+
import { ThreadViewPrototype } from "@/components/ThreadView.prototype";
|
|
14
|
+
import { useThread } from "@/hooks/useThread.prototype";
|
|
15
|
+
import { colors } from "@/lib/colors";
|
|
16
|
+
|
|
17
|
+
interface ThreadScreenProps {
|
|
18
|
+
client: XClient;
|
|
19
|
+
tweet: TweetData;
|
|
20
|
+
focused?: boolean;
|
|
21
|
+
onBack?: () => void;
|
|
22
|
+
onSelectTweet?: (tweet: TweetData) => void;
|
|
23
|
+
showFooter?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ThreadScreen({
|
|
27
|
+
client,
|
|
28
|
+
tweet,
|
|
29
|
+
focused = false,
|
|
30
|
+
onBack,
|
|
31
|
+
onSelectTweet,
|
|
32
|
+
showFooter = true,
|
|
33
|
+
}: ThreadScreenProps) {
|
|
34
|
+
const { ancestors, replyTree, loadingAncestors, loadingReplies, error } =
|
|
35
|
+
useThread({
|
|
36
|
+
client,
|
|
37
|
+
tweet,
|
|
38
|
+
maxAncestorDepth: 10,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Show loading state
|
|
42
|
+
if (loadingAncestors || loadingReplies) {
|
|
43
|
+
return (
|
|
44
|
+
<box style={{ flexDirection: "column", height: "100%" }}>
|
|
45
|
+
<box style={{ paddingLeft: 1, paddingTop: 1 }}>
|
|
46
|
+
<text fg={colors.muted}>
|
|
47
|
+
Loading thread...
|
|
48
|
+
{loadingAncestors && " (ancestors)"}
|
|
49
|
+
{loadingReplies && " (replies)"}
|
|
50
|
+
</text>
|
|
51
|
+
</box>
|
|
52
|
+
</box>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Show error state
|
|
57
|
+
if (error) {
|
|
58
|
+
return (
|
|
59
|
+
<box style={{ flexDirection: "column", height: "100%" }}>
|
|
60
|
+
<box style={{ paddingLeft: 1, paddingTop: 1 }}>
|
|
61
|
+
<text fg={colors.error}>Error: {error}</text>
|
|
62
|
+
</box>
|
|
63
|
+
<box style={{ paddingLeft: 1, paddingTop: 1 }}>
|
|
64
|
+
<text fg={colors.dim}>Press h or Esc to go back</text>
|
|
65
|
+
</box>
|
|
66
|
+
</box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<ThreadViewPrototype
|
|
72
|
+
ancestors={ancestors}
|
|
73
|
+
focusedTweet={tweet}
|
|
74
|
+
replyTree={replyTree}
|
|
75
|
+
focused={focused}
|
|
76
|
+
onBack={onBack}
|
|
77
|
+
onSelectTweet={onSelectTweet}
|
|
78
|
+
showFooter={showFooter}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|