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.
Files changed (210) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +5 -0
  3. package/package.json +43 -0
  4. package/src/api/actions.ts +16 -0
  5. package/src/api/client.test.ts +3370 -0
  6. package/src/api/client.ts +4319 -0
  7. package/src/api/query-ids.json +11 -0
  8. package/src/api/query-ids.test.ts +118 -0
  9. package/src/api/query-ids.ts +59 -0
  10. package/src/api/runtime-query-ids.test.ts +926 -0
  11. package/src/api/runtime-query-ids.ts +389 -0
  12. package/src/api/types.ts +581 -0
  13. package/src/app.tsx +664 -0
  14. package/src/auth/browser-detect.ts +150 -0
  15. package/src/auth/browser-picker.ts +118 -0
  16. package/src/auth/check.test.preload.ts +94 -0
  17. package/src/auth/check.test.ts +388 -0
  18. package/src/auth/check.ts +220 -0
  19. package/src/auth/cookies.test.ts +529 -0
  20. package/src/auth/cookies.ts +299 -0
  21. package/src/auth/manual-entry.ts +88 -0
  22. package/src/auth/session.ts +30 -0
  23. package/src/components/ErrorBanner.tsx +172 -0
  24. package/src/components/Footer.tsx +90 -0
  25. package/src/components/Header.tsx +57 -0
  26. package/src/components/NotificationItem.test.ts +252 -0
  27. package/src/components/NotificationItem.tsx +80 -0
  28. package/src/components/NotificationList.test.ts +328 -0
  29. package/src/components/NotificationList.tsx +157 -0
  30. package/src/components/PostCard.tsx +186 -0
  31. package/src/components/PostList.tsx +232 -0
  32. package/src/components/QuotedPostCard.tsx +55 -0
  33. package/src/components/ReplyPreviewCard.tsx +80 -0
  34. package/src/components/ThreadView.prototype.tsx +533 -0
  35. package/src/components/Toast.tsx +28 -0
  36. package/src/config/loader.ts +69 -0
  37. package/src/config/types.ts +27 -0
  38. package/src/contexts/ModalContext.tsx +227 -0
  39. package/src/experiments/TimelineScreenExperimental.tsx +202 -0
  40. package/src/experiments/index.tsx +43 -0
  41. package/src/experiments/query-client.ts +132 -0
  42. package/src/experiments/use-bookmark-mutation.ts +342 -0
  43. package/src/experiments/use-bookmarks-query.ts +166 -0
  44. package/src/experiments/use-notifications-query.ts +368 -0
  45. package/src/experiments/use-post-detail-query.ts +187 -0
  46. package/src/experiments/use-profile-query.ts +162 -0
  47. package/src/experiments/use-timeline-query.ts +201 -0
  48. package/src/hooks/.gitkeep +0 -0
  49. package/src/hooks/useActions.ts +354 -0
  50. package/src/hooks/useBookmarkFolders.ts +70 -0
  51. package/src/hooks/useBookmarks.ts +111 -0
  52. package/src/hooks/useCountdown.ts +75 -0
  53. package/src/hooks/useListNavigation.test.ts +273 -0
  54. package/src/hooks/useListNavigation.ts +118 -0
  55. package/src/hooks/useNavigation.test.ts +340 -0
  56. package/src/hooks/useNavigation.ts +103 -0
  57. package/src/hooks/useNotifications.test.ts +377 -0
  58. package/src/hooks/useNotifications.ts +117 -0
  59. package/src/hooks/usePaginatedData.ts +217 -0
  60. package/src/hooks/usePostDetail.ts +137 -0
  61. package/src/hooks/useThread.prototype.ts +314 -0
  62. package/src/hooks/useTimeline.ts +136 -0
  63. package/src/hooks/useUserProfile.ts +142 -0
  64. package/src/index.tsx +304 -0
  65. package/src/lib/colors.ts +41 -0
  66. package/src/lib/format.ts +69 -0
  67. package/src/lib/media.ts +464 -0
  68. package/src/lib/result.ts +6 -0
  69. package/src/lib/text.tsx +76 -0
  70. package/src/modals/BookmarkFolderSelector.tsx +260 -0
  71. package/src/modals/ExitConfirmationModal.tsx +131 -0
  72. package/src/modals/FolderPicker.tsx +281 -0
  73. package/src/modals/README.md +171 -0
  74. package/src/modals/SessionExpiredModal.tsx +47 -0
  75. package/src/modals/index.ts +4 -0
  76. package/src/screens/.gitkeep +0 -0
  77. package/src/screens/BookmarksScreen.tsx +168 -0
  78. package/src/screens/NotificationsScreen.tsx +172 -0
  79. package/src/screens/PostDetailScreen.tsx +976 -0
  80. package/src/screens/ProfileScreen.tsx +528 -0
  81. package/src/screens/SplashScreen.tsx +72 -0
  82. package/src/screens/ThreadScreen.tsx +81 -0
  83. package/src/screens/TimelineScreen.tsx +188 -0
  84. package/vendor/sweet-cookie/LICENSE +22 -0
  85. package/vendor/sweet-cookie/README.md +29 -0
  86. package/vendor/sweet-cookie/dist/index.d.ts +3 -0
  87. package/vendor/sweet-cookie/dist/index.d.ts.map +1 -0
  88. package/vendor/sweet-cookie/dist/index.js +2 -0
  89. package/vendor/sweet-cookie/dist/index.js.map +1 -0
  90. package/vendor/sweet-cookie/dist/providers/chrome.d.ts +10 -0
  91. package/vendor/sweet-cookie/dist/providers/chrome.d.ts.map +1 -0
  92. package/vendor/sweet-cookie/dist/providers/chrome.js +27 -0
  93. package/vendor/sweet-cookie/dist/providers/chrome.js.map +1 -0
  94. package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts +11 -0
  95. package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts.map +1 -0
  96. package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.js +100 -0
  97. package/vendor/sweet-cookie/dist/providers/chromeSqlite/crypto.js.map +1 -0
  98. package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts +25 -0
  99. package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts.map +1 -0
  100. package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js +104 -0
  101. package/vendor/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js.map +1 -0
  102. package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts +10 -0
  103. package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts.map +1 -0
  104. package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.js +293 -0
  105. package/vendor/sweet-cookie/dist/providers/chromeSqlite/shared.js.map +1 -0
  106. package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts +10 -0
  107. package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts.map +1 -0
  108. package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js +26 -0
  109. package/vendor/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js.map +1 -0
  110. package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts +7 -0
  111. package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts.map +1 -0
  112. package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.js +51 -0
  113. package/vendor/sweet-cookie/dist/providers/chromeSqliteLinux.js.map +1 -0
  114. package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.d.ts +10 -0
  115. package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.d.ts.map +1 -0
  116. package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.js +118 -0
  117. package/vendor/sweet-cookie/dist/providers/chromeSqliteMac.js.map +1 -0
  118. package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts +7 -0
  119. package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts.map +1 -0
  120. package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.js +38 -0
  121. package/vendor/sweet-cookie/dist/providers/chromeSqliteWindows.js.map +1 -0
  122. package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts +5 -0
  123. package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts.map +1 -0
  124. package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.js +33 -0
  125. package/vendor/sweet-cookie/dist/providers/chromium/linuxPaths.js.map +1 -0
  126. package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts +24 -0
  127. package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts.map +1 -0
  128. package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.js +30 -0
  129. package/vendor/sweet-cookie/dist/providers/chromium/macosKeychain.js.map +1 -0
  130. package/vendor/sweet-cookie/dist/providers/chromium/paths.d.ts +11 -0
  131. package/vendor/sweet-cookie/dist/providers/chromium/paths.d.ts.map +1 -0
  132. package/vendor/sweet-cookie/dist/providers/chromium/paths.js +43 -0
  133. package/vendor/sweet-cookie/dist/providers/chromium/paths.js.map +1 -0
  134. package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts +8 -0
  135. package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts.map +1 -0
  136. package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.js +41 -0
  137. package/vendor/sweet-cookie/dist/providers/chromium/windowsMasterKey.js.map +1 -0
  138. package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts +8 -0
  139. package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts.map +1 -0
  140. package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.js +53 -0
  141. package/vendor/sweet-cookie/dist/providers/chromium/windowsPaths.js.map +1 -0
  142. package/vendor/sweet-cookie/dist/providers/edge.d.ts +8 -0
  143. package/vendor/sweet-cookie/dist/providers/edge.d.ts.map +1 -0
  144. package/vendor/sweet-cookie/dist/providers/edge.js +27 -0
  145. package/vendor/sweet-cookie/dist/providers/edge.js.map +1 -0
  146. package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts +7 -0
  147. package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts.map +1 -0
  148. package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.js +53 -0
  149. package/vendor/sweet-cookie/dist/providers/edgeSqliteLinux.js.map +1 -0
  150. package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.d.ts +8 -0
  151. package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.d.ts.map +1 -0
  152. package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.js +60 -0
  153. package/vendor/sweet-cookie/dist/providers/edgeSqliteMac.js.map +1 -0
  154. package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts +7 -0
  155. package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts.map +1 -0
  156. package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.js +38 -0
  157. package/vendor/sweet-cookie/dist/providers/edgeSqliteWindows.js.map +1 -0
  158. package/vendor/sweet-cookie/dist/providers/firefoxSqlite.d.ts +6 -0
  159. package/vendor/sweet-cookie/dist/providers/firefoxSqlite.d.ts.map +1 -0
  160. package/vendor/sweet-cookie/dist/providers/firefoxSqlite.js +257 -0
  161. package/vendor/sweet-cookie/dist/providers/firefoxSqlite.js.map +1 -0
  162. package/vendor/sweet-cookie/dist/providers/inline.d.ts +8 -0
  163. package/vendor/sweet-cookie/dist/providers/inline.d.ts.map +1 -0
  164. package/vendor/sweet-cookie/dist/providers/inline.js +71 -0
  165. package/vendor/sweet-cookie/dist/providers/inline.js.map +1 -0
  166. package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.d.ts +6 -0
  167. package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.d.ts.map +1 -0
  168. package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.js +173 -0
  169. package/vendor/sweet-cookie/dist/providers/safariBinaryCookies.js.map +1 -0
  170. package/vendor/sweet-cookie/dist/public.d.ts +26 -0
  171. package/vendor/sweet-cookie/dist/public.d.ts.map +1 -0
  172. package/vendor/sweet-cookie/dist/public.js +197 -0
  173. package/vendor/sweet-cookie/dist/public.js.map +1 -0
  174. package/vendor/sweet-cookie/dist/types.d.ts +127 -0
  175. package/vendor/sweet-cookie/dist/types.d.ts.map +1 -0
  176. package/vendor/sweet-cookie/dist/types.js +2 -0
  177. package/vendor/sweet-cookie/dist/types.js.map +1 -0
  178. package/vendor/sweet-cookie/dist/util/base64.d.ts +2 -0
  179. package/vendor/sweet-cookie/dist/util/base64.d.ts.map +1 -0
  180. package/vendor/sweet-cookie/dist/util/base64.js +18 -0
  181. package/vendor/sweet-cookie/dist/util/base64.js.map +1 -0
  182. package/vendor/sweet-cookie/dist/util/exec.d.ts +8 -0
  183. package/vendor/sweet-cookie/dist/util/exec.d.ts.map +1 -0
  184. package/vendor/sweet-cookie/dist/util/exec.js +110 -0
  185. package/vendor/sweet-cookie/dist/util/exec.js.map +1 -0
  186. package/vendor/sweet-cookie/dist/util/expire.d.ts +2 -0
  187. package/vendor/sweet-cookie/dist/util/expire.d.ts.map +1 -0
  188. package/vendor/sweet-cookie/dist/util/expire.js +32 -0
  189. package/vendor/sweet-cookie/dist/util/expire.js.map +1 -0
  190. package/vendor/sweet-cookie/dist/util/fs.d.ts +2 -0
  191. package/vendor/sweet-cookie/dist/util/fs.d.ts.map +1 -0
  192. package/vendor/sweet-cookie/dist/util/fs.js +13 -0
  193. package/vendor/sweet-cookie/dist/util/fs.js.map +1 -0
  194. package/vendor/sweet-cookie/dist/util/hostMatch.d.ts +2 -0
  195. package/vendor/sweet-cookie/dist/util/hostMatch.d.ts.map +1 -0
  196. package/vendor/sweet-cookie/dist/util/hostMatch.js +7 -0
  197. package/vendor/sweet-cookie/dist/util/hostMatch.js.map +1 -0
  198. package/vendor/sweet-cookie/dist/util/nodeSqlite.d.ts +5 -0
  199. package/vendor/sweet-cookie/dist/util/nodeSqlite.d.ts.map +1 -0
  200. package/vendor/sweet-cookie/dist/util/nodeSqlite.js +58 -0
  201. package/vendor/sweet-cookie/dist/util/nodeSqlite.js.map +1 -0
  202. package/vendor/sweet-cookie/dist/util/origins.d.ts +2 -0
  203. package/vendor/sweet-cookie/dist/util/origins.d.ts.map +1 -0
  204. package/vendor/sweet-cookie/dist/util/origins.js +27 -0
  205. package/vendor/sweet-cookie/dist/util/origins.js.map +1 -0
  206. package/vendor/sweet-cookie/dist/util/runtime.d.ts +2 -0
  207. package/vendor/sweet-cookie/dist/util/runtime.d.ts.map +1 -0
  208. package/vendor/sweet-cookie/dist/util/runtime.js +8 -0
  209. package/vendor/sweet-cookie/dist/util/runtime.js.map +1 -0
  210. package/vendor/sweet-cookie/package.json +40 -0
@@ -0,0 +1,581 @@
1
+ /**
2
+ * X API types for xfeed
3
+ * Consolidated from bird reference implementation
4
+ */
5
+
6
+ import type { XCookies } from "@/auth/cookies";
7
+
8
+ /**
9
+ * Result of a tweet creation operation
10
+ */
11
+ export type TweetResult =
12
+ | { success: true; tweetId: string }
13
+ | { success: false; error: string };
14
+
15
+ /**
16
+ * Result of a media upload operation
17
+ */
18
+ export interface UploadMediaResult {
19
+ success: boolean;
20
+ mediaId?: string;
21
+ error?: string;
22
+ }
23
+
24
+ /**
25
+ * Author information for a tweet
26
+ */
27
+ export interface TweetAuthor {
28
+ username: string;
29
+ name: string;
30
+ }
31
+
32
+ /**
33
+ * Media item attached to a tweet (photo, video, or animated GIF)
34
+ */
35
+ export interface MediaItem {
36
+ /** Unique media ID */
37
+ id: string;
38
+ /** Media type: photo, video, or animated_gif */
39
+ type: "photo" | "video" | "animated_gif";
40
+ /** Direct URL to the media (for photos, this is the display URL) */
41
+ url: string;
42
+ /** Width in pixels */
43
+ width?: number;
44
+ /** Height in pixels */
45
+ height?: number;
46
+ /** Video variants (only for video/animated_gif types) */
47
+ videoVariants?: VideoVariant[];
48
+ }
49
+
50
+ /**
51
+ * Video variant with quality/format information
52
+ */
53
+ export interface VideoVariant {
54
+ /** Bitrate in bits per second (higher = better quality) */
55
+ bitrate?: number;
56
+ /** MIME type (e.g., "video/mp4", "application/x-mpegURL") */
57
+ contentType: string;
58
+ /** Direct URL to the video variant */
59
+ url: string;
60
+ }
61
+
62
+ /**
63
+ * URL entity from tweet text
64
+ */
65
+ export interface UrlEntity {
66
+ /** Shortened t.co URL as it appears in tweet text */
67
+ url: string;
68
+ /** Full expanded URL */
69
+ expandedUrl: string;
70
+ /** Display-friendly URL (truncated, no protocol) */
71
+ displayUrl: string;
72
+ /** Character indices in tweet text [start, end] */
73
+ indices: [number, number];
74
+ }
75
+
76
+ /**
77
+ * @mention entity from tweet text
78
+ */
79
+ export interface MentionEntity {
80
+ /** @username as it appears in text */
81
+ username: string;
82
+ /** User ID from the API */
83
+ userId?: string;
84
+ /** Display name of the mentioned user */
85
+ name?: string;
86
+ /** Character indices in tweet text [start, end] */
87
+ indices: [number, number];
88
+ }
89
+
90
+ /**
91
+ * Core tweet data structure
92
+ */
93
+ export interface TweetData {
94
+ id: string;
95
+ text: string;
96
+ author: TweetAuthor;
97
+ authorId?: string;
98
+ createdAt?: string;
99
+ replyCount?: number;
100
+ retweetCount?: number;
101
+ likeCount?: number;
102
+ conversationId?: string;
103
+ inReplyToStatusId?: string;
104
+ /** Quoted tweet data (recursive, depth controlled by quoteDepth option) */
105
+ quotedTweet?: TweetData;
106
+ /** Media attachments (photos, videos, GIFs) */
107
+ media?: MediaItem[];
108
+ /** URL entities parsed from tweet text */
109
+ urls?: UrlEntity[];
110
+ /** @mention entities parsed from tweet text */
111
+ mentions?: MentionEntity[];
112
+ /** Whether the tweet is liked by the current user */
113
+ favorited?: boolean;
114
+ /** Whether the tweet is bookmarked by the current user */
115
+ bookmarked?: boolean;
116
+ /** First nested reply preview (display-only, not navigable) */
117
+ nestedReplyPreview?: TweetData;
118
+ }
119
+
120
+ /**
121
+ * Result of fetching a single tweet
122
+ */
123
+ export interface GetTweetResult {
124
+ success: boolean;
125
+ tweet?: TweetData;
126
+ error?: string;
127
+ }
128
+
129
+ /**
130
+ * Result of a search or timeline fetch operation
131
+ */
132
+ export interface SearchResult {
133
+ success: boolean;
134
+ tweets?: TweetData[];
135
+ error?: string;
136
+ /** Pagination cursor for loading more results (e.g., replies) */
137
+ nextCursor?: string;
138
+ }
139
+
140
+ /**
141
+ * Result of a timeline fetch with pagination support
142
+ */
143
+ export type TimelineResult =
144
+ | { success: true; tweets: TweetData[]; nextCursor?: string }
145
+ | { success: false; error: string };
146
+
147
+ /**
148
+ * User information
149
+ */
150
+ export interface UserData {
151
+ id: string;
152
+ username: string;
153
+ name: string;
154
+ }
155
+
156
+ /**
157
+ * Extended user profile data with bio and stats
158
+ */
159
+ export interface UserProfileData {
160
+ id: string;
161
+ username: string;
162
+ name: string;
163
+ description?: string;
164
+ followersCount?: number;
165
+ followingCount?: number;
166
+ isBlueVerified?: boolean;
167
+ /** Profile photo URL */
168
+ profileImageUrl?: string;
169
+ /** Banner/header image URL */
170
+ bannerImageUrl?: string;
171
+ /** User's location string */
172
+ location?: string;
173
+ /** User's website URL (expanded) */
174
+ websiteUrl?: string;
175
+ /** Account creation date (ISO string) */
176
+ createdAt?: string;
177
+ }
178
+
179
+ /**
180
+ * Result of fetching a user profile
181
+ */
182
+ export interface UserProfileResult {
183
+ success: boolean;
184
+ user?: UserProfileData;
185
+ error?: string;
186
+ }
187
+
188
+ /**
189
+ * Result of fetching user tweets
190
+ */
191
+ export interface UserTweetsResult {
192
+ success: boolean;
193
+ tweets?: TweetData[];
194
+ error?: string;
195
+ }
196
+
197
+ /**
198
+ * Result of fetching current user information
199
+ */
200
+ export interface CurrentUserResult {
201
+ success: boolean;
202
+ user?: UserData;
203
+ error?: string;
204
+ }
205
+
206
+ /**
207
+ * Options for creating an XClient instance
208
+ */
209
+ export interface XClientOptions {
210
+ cookies: XCookies;
211
+ userAgent?: string;
212
+ timeoutMs?: number;
213
+ /** Max depth for quoted tweets (0 disables, default: 1) */
214
+ quoteDepth?: number;
215
+ /** Callback when session expires (401/403 errors) */
216
+ onSessionExpired?: () => void;
217
+ }
218
+
219
+ /**
220
+ * Internal GraphQL tweet result structure from X API responses
221
+ */
222
+ export interface GraphqlTweetResult {
223
+ __typename?: string;
224
+ rest_id?: string;
225
+ legacy?: {
226
+ full_text?: string;
227
+ created_at?: string;
228
+ reply_count?: number;
229
+ retweet_count?: number;
230
+ favorite_count?: number;
231
+ conversation_id_str?: string;
232
+ in_reply_to_status_id_str?: string | null;
233
+ favorited?: boolean;
234
+ bookmarked?: boolean;
235
+ entities?: {
236
+ urls?: Array<{
237
+ url: string;
238
+ expanded_url: string;
239
+ display_url: string;
240
+ indices: [number, number];
241
+ }>;
242
+ media?: Array<{
243
+ url: string;
244
+ indices: [number, number];
245
+ }>;
246
+ user_mentions?: Array<{
247
+ screen_name: string;
248
+ name: string;
249
+ id_str?: string;
250
+ indices: [number, number];
251
+ }>;
252
+ };
253
+ extended_entities?: {
254
+ media?: Array<{
255
+ id_str?: string;
256
+ type: "photo" | "video" | "animated_gif";
257
+ media_url_https: string;
258
+ original_info?: {
259
+ width?: number;
260
+ height?: number;
261
+ };
262
+ video_info?: {
263
+ variants: Array<{
264
+ bitrate?: number;
265
+ content_type: string;
266
+ url: string;
267
+ }>;
268
+ };
269
+ }>;
270
+ };
271
+ };
272
+ core?: {
273
+ user_results?: {
274
+ result?: {
275
+ rest_id?: string;
276
+ id?: string;
277
+ legacy?: {
278
+ screen_name?: string;
279
+ name?: string;
280
+ };
281
+ core?: {
282
+ screen_name?: string;
283
+ name?: string;
284
+ };
285
+ };
286
+ };
287
+ };
288
+ note_tweet?: {
289
+ note_tweet_results?: {
290
+ result?: {
291
+ text?: string;
292
+ richtext?: { text?: string };
293
+ rich_text?: { text?: string };
294
+ content?: {
295
+ text?: string;
296
+ richtext?: { text?: string };
297
+ rich_text?: { text?: string };
298
+ };
299
+ };
300
+ };
301
+ };
302
+ article?: ArticleData;
303
+ /** Quoted tweet data - same structure, recursively nested */
304
+ quoted_status_result?: {
305
+ result?: GraphqlTweetResult;
306
+ };
307
+ /** Visibility wrapper - tweet may be nested here instead of at top level */
308
+ tweet?: GraphqlTweetResult;
309
+ }
310
+
311
+ /**
312
+ * Article content structure (for long-form tweets/articles)
313
+ */
314
+ export interface ArticleData {
315
+ article_results?: {
316
+ result?: ArticleResult;
317
+ };
318
+ title?: string;
319
+ plain_text?: string;
320
+ text?: string;
321
+ richtext?: { text?: string };
322
+ rich_text?: { text?: string };
323
+ body?: TextContent;
324
+ content?: ContentWithItems;
325
+ sections?: Array<{ items?: ContentItem[] }>;
326
+ }
327
+
328
+ interface TextContent {
329
+ text?: string;
330
+ richtext?: { text?: string };
331
+ rich_text?: { text?: string };
332
+ }
333
+
334
+ interface ContentItem {
335
+ text?: string;
336
+ content?: TextContent;
337
+ }
338
+
339
+ interface ContentWithItems extends TextContent {
340
+ items?: ContentItem[];
341
+ }
342
+
343
+ interface ArticleResult {
344
+ title?: string;
345
+ plain_text?: string;
346
+ text?: string;
347
+ richtext?: { text?: string };
348
+ rich_text?: { text?: string };
349
+ body?: TextContent;
350
+ content?: ContentWithItems;
351
+ sections?: Array<{ items?: ContentItem[] }>;
352
+ }
353
+
354
+ /**
355
+ * Internal response structure for tweet creation
356
+ */
357
+ export interface CreateTweetResponse {
358
+ data?: {
359
+ create_tweet?: {
360
+ tweet_results?: {
361
+ result?: {
362
+ rest_id?: string;
363
+ legacy?: {
364
+ full_text?: string;
365
+ };
366
+ };
367
+ };
368
+ };
369
+ };
370
+ errors?: Array<{ message: string; code?: number }>;
371
+ }
372
+
373
+ /**
374
+ * GraphQL operation names that use rotating query IDs
375
+ */
376
+ export type OperationName =
377
+ | "CreateTweet"
378
+ | "CreateRetweet"
379
+ | "FavoriteTweet"
380
+ | "UnfavoriteTweet"
381
+ | "CreateBookmark"
382
+ | "DeleteBookmark"
383
+ | "TweetDetail"
384
+ | "SearchTimeline"
385
+ | "UserArticlesTweets"
386
+ | "Bookmarks"
387
+ | "BookmarkFolderTimeline"
388
+ | "HomeTimeline"
389
+ | "HomeLatestTimeline"
390
+ | "UserByScreenName"
391
+ | "UserTweets"
392
+ | "Likes"
393
+ | "BookmarkFoldersSlice"
394
+ | "bookmarkTweetToFolder"
395
+ | "NotificationsTimeline";
396
+
397
+ /**
398
+ * Result of an action mutation (like, bookmark, etc.)
399
+ */
400
+ export type ActionResult =
401
+ | { success: true }
402
+ | { success: false; error: string };
403
+
404
+ /**
405
+ * Bookmark folder data structure
406
+ */
407
+ export interface BookmarkFolder {
408
+ /** Folder ID (bookmark_collection_id) */
409
+ id: string;
410
+ /** Folder display name */
411
+ name: string;
412
+ }
413
+
414
+ /**
415
+ * Result of fetching bookmark folders
416
+ */
417
+ export type BookmarkFoldersResult =
418
+ | { success: true; folders: BookmarkFolder[] }
419
+ | { success: false; error: string };
420
+
421
+ /**
422
+ * API error types for different failure modes
423
+ */
424
+ export type ApiErrorType =
425
+ | "rate_limit"
426
+ | "auth_expired"
427
+ | "network_error"
428
+ | "not_found"
429
+ | "unavailable"
430
+ | "unknown";
431
+
432
+ /**
433
+ * Structured API error with type discrimination
434
+ */
435
+ export interface ApiError {
436
+ /** Error type for programmatic handling */
437
+ type: ApiErrorType;
438
+ /** Human-readable error message */
439
+ message: string;
440
+ /** HTTP status code if applicable */
441
+ statusCode?: number;
442
+ /** Rate limit reset time (Unix timestamp) for rate_limit errors */
443
+ rateLimitReset?: number;
444
+ /** Retry-After header value in seconds */
445
+ retryAfter?: number;
446
+ }
447
+
448
+ /**
449
+ * Timeline result with typed errors
450
+ */
451
+ export type TimelineResultV2 =
452
+ | { success: true; tweets: TweetData[]; nextCursor?: string }
453
+ | { success: false; error: ApiError };
454
+
455
+ /**
456
+ * Generic fetch result with typed errors
457
+ */
458
+ export type FetchResult<T> =
459
+ | { success: true; data: T }
460
+ | { success: false; error: ApiError };
461
+
462
+ /**
463
+ * User-friendly error messages for each error type
464
+ */
465
+ export const API_ERROR_MESSAGES: Record<ApiErrorType, string> = {
466
+ rate_limit: "Rate limited by X. Please wait before trying again.",
467
+ auth_expired: "Session expired. Please log into x.com and restart xfeed.",
468
+ network_error: "Network error. Check your connection and try again.",
469
+ not_found: "Content not found or has been deleted.",
470
+ unavailable: "This content is temporarily unavailable.",
471
+ unknown: "Something went wrong. Please try again.",
472
+ };
473
+
474
+ /**
475
+ * Check if an error type is retryable
476
+ */
477
+ export function isRetryableError(errorType: ApiErrorType): boolean {
478
+ return errorType === "network_error" || errorType === "rate_limit";
479
+ }
480
+
481
+ /**
482
+ * Check if an error indicates auth issues requiring re-login
483
+ */
484
+ export function isAuthError(errorType: ApiErrorType): boolean {
485
+ return errorType === "auth_expired";
486
+ }
487
+
488
+ /**
489
+ * Notification icon types from X API
490
+ */
491
+ export type NotificationIcon =
492
+ | "heart_icon"
493
+ | "person_icon"
494
+ | "bird_icon"
495
+ | "retweet_icon"
496
+ | "reply_icon";
497
+
498
+ /**
499
+ * Notification data structure
500
+ */
501
+ export interface NotificationData {
502
+ /** Unique notification ID */
503
+ id: string;
504
+ /** Icon type indicating notification category */
505
+ icon: NotificationIcon;
506
+ /** Human-readable notification message */
507
+ message: string;
508
+ /** URL to navigate to when clicked */
509
+ url: string;
510
+ /** ISO timestamp of the notification */
511
+ timestamp: string;
512
+ /** Sort index for ordering and unread calculation */
513
+ sortIndex: string;
514
+ /** Associated tweet (for likes, retweets, replies) */
515
+ targetTweet?: TweetData;
516
+ /** Users who performed the action */
517
+ fromUsers?: UserData[];
518
+ }
519
+
520
+ /**
521
+ * Result of fetching notifications
522
+ */
523
+ export type NotificationsResult =
524
+ | {
525
+ success: true;
526
+ notifications: NotificationData[];
527
+ unreadSortIndex?: string;
528
+ topCursor?: string;
529
+ bottomCursor?: string;
530
+ }
531
+ | { success: false; error: ApiError };
532
+
533
+ /**
534
+ * Internal notification timeline instruction types from X API
535
+ */
536
+ export interface NotificationInstruction {
537
+ type: string;
538
+ sort_index?: string;
539
+ entries?: Array<{
540
+ entryId?: string;
541
+ sortIndex?: string;
542
+ content?: {
543
+ cursorType?: string;
544
+ value?: string;
545
+ itemContent?: {
546
+ itemType?: string;
547
+ id?: string;
548
+ notification_icon?: string;
549
+ rich_message?: {
550
+ text?: string;
551
+ };
552
+ notification_url?: {
553
+ url?: string;
554
+ };
555
+ template?: {
556
+ target_objects?: Array<{
557
+ tweet_results?: {
558
+ result?: GraphqlTweetResult;
559
+ };
560
+ }>;
561
+ from_users?: Array<{
562
+ user_results?: {
563
+ result?: {
564
+ rest_id?: string;
565
+ core?: {
566
+ screen_name?: string;
567
+ name?: string;
568
+ };
569
+ legacy?: {
570
+ screen_name?: string;
571
+ name?: string;
572
+ };
573
+ };
574
+ };
575
+ }>;
576
+ };
577
+ timestamp_ms?: string;
578
+ };
579
+ };
580
+ }>;
581
+ }