@youmind-openlab/rettiwt-api 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/dist/browser/adapters/DomAdapter.d.ts +3 -2
- package/dist/browser/adapters/DomAdapter.js +10 -4
- package/dist/browser/adapters/DomAdapter.js.map +1 -1
- package/dist/browser/browser/adapters/DomAdapter.d.ts +3 -2
- package/dist/browser/browser/adapters/DomAdapter.js +10 -4
- package/dist/browser/browser/adapters/DomAdapter.js.map +1 -1
- package/examples/browser-extension/manifest.json +4 -0
- package/examples/browser-extension/src/background.ts +151 -0
- package/examples/browser-extension/src/popup.ts +95 -33
- package/examples/browser-extension/webpack.config.js +1 -0
- package/package.json +1 -1
- package/src/browser/adapters/DomAdapter.ts +11 -4
- package/tsconfig.browser.json +2 -1
- package/tsconfig.json +2 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* DOM parsing adapter for browser environments.
|
|
3
|
+
* Uses linkedom for compatibility with both popup and service worker (background) contexts.
|
|
4
|
+
* DOMParser is not available in service workers, so we use linkedom which works everywhere.
|
|
4
5
|
*
|
|
5
6
|
* @internal
|
|
6
7
|
*/
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DomAdapter = void 0;
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
5
|
+
// @ts-ignore - linkedom types are incompatible with DOM types but work at runtime
|
|
6
|
+
const linkedom_1 = require("linkedom");
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
8
|
+
* DOM parsing adapter for browser environments.
|
|
9
|
+
* Uses linkedom for compatibility with both popup and service worker (background) contexts.
|
|
10
|
+
* DOMParser is not available in service workers, so we use linkedom which works everywhere.
|
|
7
11
|
*
|
|
8
12
|
* @internal
|
|
9
13
|
*/
|
|
@@ -16,8 +20,10 @@ class DomAdapter {
|
|
|
16
20
|
* @returns The parsed Document
|
|
17
21
|
*/
|
|
18
22
|
static parseHTML(html) {
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
// Use linkedom for service worker compatibility
|
|
24
|
+
// linkedom works in all JS environments (popup, content script, service worker)
|
|
25
|
+
const { document } = (0, linkedom_1.parseHTML)(html);
|
|
26
|
+
return document;
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
exports.DomAdapter = DomAdapter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DomAdapter.js","sourceRoot":"","sources":["../../../src/browser/adapters/DomAdapter.ts"],"names":[],"mappings":";;;AAAA
|
|
1
|
+
{"version":3,"file":"DomAdapter.js","sourceRoot":"","sources":["../../../src/browser/adapters/DomAdapter.ts"],"names":[],"mappings":";;;AAAA,6DAA6D;AAC7D,kFAAkF;AAClF,uCAA0D;AAE1D;;;;;;GAMG;AACH,MAAa,UAAU;IACtB;;;;;;OAMG;IACH,MAAM,CAAC,SAAS,CAAC,IAAY;QAC5B,gDAAgD;QAChD,gFAAgF;QAChF,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAA,oBAAiB,EAAC,IAAI,CAAC,CAAC;QAC7C,OAAO,QAA+B,CAAC;IACxC,CAAC;CACD;AAdD,gCAcC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* DOM parsing adapter for browser environments.
|
|
3
|
+
* Uses linkedom for compatibility with both popup and service worker (background) contexts.
|
|
4
|
+
* DOMParser is not available in service workers, so we use linkedom which works everywhere.
|
|
4
5
|
*
|
|
5
6
|
* @internal
|
|
6
7
|
*/
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
|
+
// @ts-ignore - linkedom types are incompatible with DOM types but work at runtime
|
|
3
|
+
import { parseHTML as linkedomParseHTML } from 'linkedom';
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
5
|
+
* DOM parsing adapter for browser environments.
|
|
6
|
+
* Uses linkedom for compatibility with both popup and service worker (background) contexts.
|
|
7
|
+
* DOMParser is not available in service workers, so we use linkedom which works everywhere.
|
|
4
8
|
*
|
|
5
9
|
* @internal
|
|
6
10
|
*/
|
|
@@ -13,8 +17,10 @@ export class DomAdapter {
|
|
|
13
17
|
* @returns The parsed Document
|
|
14
18
|
*/
|
|
15
19
|
static parseHTML(html) {
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
// Use linkedom for service worker compatibility
|
|
21
|
+
// linkedom works in all JS environments (popup, content script, service worker)
|
|
22
|
+
const { document } = linkedomParseHTML(html);
|
|
23
|
+
return document;
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
26
|
//# sourceMappingURL=DomAdapter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DomAdapter.js","sourceRoot":"","sources":["../../../../src/browser/adapters/DomAdapter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"DomAdapter.js","sourceRoot":"","sources":["../../../../src/browser/adapters/DomAdapter.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,kFAAkF;AAClF,OAAO,EAAE,SAAS,IAAI,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,OAAO,UAAU;IACtB;;;;;;OAMG;IACH,MAAM,CAAC,SAAS,CAAC,IAAY;QAC5B,gDAAgD;QAChD,gFAAgF;QAChF,MAAM,EAAE,QAAQ,EAAE,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC7C,OAAO,QAA+B,CAAC;IACxC,CAAC;CACD"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background service worker for Rettiwt Browser Extension
|
|
3
|
+
* Handles all API calls and communicates with popup via chrome.runtime messages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { RettiwtBrowser } from '../../../src/browser';
|
|
7
|
+
import { Tweet } from '../../../src/models/data/Tweet';
|
|
8
|
+
import { User } from '../../../src/models/data/User';
|
|
9
|
+
|
|
10
|
+
// Global state
|
|
11
|
+
let rettiwt: RettiwtBrowser | null = null;
|
|
12
|
+
let currentUser: User | null = null;
|
|
13
|
+
|
|
14
|
+
// Message types
|
|
15
|
+
export type MessageType =
|
|
16
|
+
| { type: 'CHECK_LOGIN' }
|
|
17
|
+
| { type: 'INITIALIZE' }
|
|
18
|
+
| { type: 'GET_USER' }
|
|
19
|
+
| { type: 'GET_BOOKMARKS'; count?: number; cursor?: string }
|
|
20
|
+
| { type: 'SEARCH_TWEETS'; query: string; count?: number; cursor?: string };
|
|
21
|
+
|
|
22
|
+
export type ResponseType =
|
|
23
|
+
| { success: true; data: unknown }
|
|
24
|
+
| { success: false; error: string };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize RettiwtBrowser instance
|
|
28
|
+
*/
|
|
29
|
+
async function ensureRettiwt(): Promise<RettiwtBrowser> {
|
|
30
|
+
if (!rettiwt) {
|
|
31
|
+
rettiwt = new RettiwtBrowser({ logging: true });
|
|
32
|
+
}
|
|
33
|
+
return rettiwt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handle incoming messages from popup
|
|
38
|
+
*/
|
|
39
|
+
chrome.runtime.onMessage.addListener(
|
|
40
|
+
(message: MessageType, _sender, sendResponse: (response: ResponseType) => void) => {
|
|
41
|
+
handleMessage(message)
|
|
42
|
+
.then((data) => sendResponse({ success: true, data }))
|
|
43
|
+
.catch((error) => {
|
|
44
|
+
console.error('Background error:', error);
|
|
45
|
+
sendResponse({
|
|
46
|
+
success: false,
|
|
47
|
+
error: error instanceof Error ? error.message : String(error),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Return true to indicate we'll respond asynchronously
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Process messages and return results
|
|
58
|
+
*/
|
|
59
|
+
async function handleMessage(message: MessageType): Promise<unknown> {
|
|
60
|
+
const api = await ensureRettiwt();
|
|
61
|
+
|
|
62
|
+
switch (message.type) {
|
|
63
|
+
case 'CHECK_LOGIN': {
|
|
64
|
+
const isLoggedIn = await api.isLoggedIn();
|
|
65
|
+
return { isLoggedIn };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case 'INITIALIZE': {
|
|
69
|
+
currentUser = await api.initialize();
|
|
70
|
+
return {
|
|
71
|
+
user: {
|
|
72
|
+
id: currentUser.id,
|
|
73
|
+
userName: currentUser.userName,
|
|
74
|
+
fullName: currentUser.fullName,
|
|
75
|
+
profileImage: currentUser.profileImage,
|
|
76
|
+
followersCount: currentUser.followersCount,
|
|
77
|
+
followingsCount: currentUser.followingsCount,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'GET_USER': {
|
|
83
|
+
if (!currentUser) {
|
|
84
|
+
throw new Error('Not initialized');
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
user: {
|
|
88
|
+
id: currentUser.id,
|
|
89
|
+
userName: currentUser.userName,
|
|
90
|
+
fullName: currentUser.fullName,
|
|
91
|
+
profileImage: currentUser.profileImage,
|
|
92
|
+
followersCount: currentUser.followersCount,
|
|
93
|
+
followingsCount: currentUser.followingsCount,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case 'GET_BOOKMARKS': {
|
|
99
|
+
if (!api.isInitialized) {
|
|
100
|
+
throw new Error('Not initialized');
|
|
101
|
+
}
|
|
102
|
+
const bookmarks = await api.user.bookmarks(message.count ?? 20, message.cursor);
|
|
103
|
+
return {
|
|
104
|
+
list: bookmarks.list.map(serializeTweet),
|
|
105
|
+
next: bookmarks.next,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'SEARCH_TWEETS': {
|
|
110
|
+
if (!api.isInitialized) {
|
|
111
|
+
throw new Error('Not initialized');
|
|
112
|
+
}
|
|
113
|
+
const results = await api.tweet.search(
|
|
114
|
+
{ includeWords: [message.query] },
|
|
115
|
+
message.count ?? 20,
|
|
116
|
+
message.cursor,
|
|
117
|
+
);
|
|
118
|
+
return {
|
|
119
|
+
list: results.list.map(serializeTweet),
|
|
120
|
+
next: results.next,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
default:
|
|
125
|
+
throw new Error(`Unknown message type: ${(message as { type: string }).type}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Serialize a Tweet object to a plain object for message passing
|
|
131
|
+
*/
|
|
132
|
+
function serializeTweet(tweet: Tweet): Record<string, unknown> {
|
|
133
|
+
return {
|
|
134
|
+
id: tweet.id,
|
|
135
|
+
fullText: tweet.fullText,
|
|
136
|
+
createdAt: tweet.createdAt,
|
|
137
|
+
likeCount: tweet.likeCount,
|
|
138
|
+
retweetCount: tweet.retweetCount,
|
|
139
|
+
replyCount: tweet.replyCount,
|
|
140
|
+
tweetBy: tweet.tweetBy
|
|
141
|
+
? {
|
|
142
|
+
id: tweet.tweetBy.id,
|
|
143
|
+
userName: tweet.tweetBy.userName,
|
|
144
|
+
fullName: tweet.tweetBy.fullName,
|
|
145
|
+
profileImage: tweet.tweetBy.profileImage,
|
|
146
|
+
}
|
|
147
|
+
: null,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('Rettiwt background service worker loaded');
|
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Popup script for Rettiwt Browser Extension Demo
|
|
3
|
+
* Communicates with background service worker via chrome.runtime messages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MessageType, ResponseType } from './background';
|
|
7
|
+
|
|
8
|
+
// Types for serialized data from background
|
|
9
|
+
interface SerializedUser {
|
|
10
|
+
id: string;
|
|
11
|
+
userName: string;
|
|
12
|
+
fullName: string;
|
|
13
|
+
profileImage: string;
|
|
14
|
+
followersCount: number;
|
|
15
|
+
followingsCount: number;
|
|
16
|
+
statusesCount?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SerializedTweet {
|
|
20
|
+
id: string;
|
|
21
|
+
fullText: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
likeCount: number;
|
|
24
|
+
retweetCount: number;
|
|
25
|
+
replyCount: number;
|
|
26
|
+
tweetBy: {
|
|
27
|
+
id: string;
|
|
28
|
+
userName: string;
|
|
29
|
+
fullName: string;
|
|
30
|
+
profileImage: string;
|
|
31
|
+
} | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CursoredResponse<T> {
|
|
35
|
+
list: T[];
|
|
36
|
+
next: string;
|
|
37
|
+
}
|
|
2
38
|
|
|
3
39
|
// DOM Elements
|
|
4
40
|
const statusEl = document.getElementById('status') as HTMLDivElement;
|
|
@@ -28,10 +64,10 @@ const contentListEl = document.getElementById('content-list') as HTMLDivElement;
|
|
|
28
64
|
const loadMoreEl = document.getElementById('load-more') as HTMLDivElement;
|
|
29
65
|
|
|
30
66
|
// Global state
|
|
31
|
-
let
|
|
67
|
+
let currentUser: SerializedUser | null = null;
|
|
32
68
|
let currentTab: 'bookmarks' | 'search' = 'bookmarks';
|
|
33
|
-
let bookmarks:
|
|
34
|
-
let searchResults:
|
|
69
|
+
let bookmarks: SerializedTweet[] = [];
|
|
70
|
+
let searchResults: SerializedTweet[] = [];
|
|
35
71
|
let bookmarksCursor: string | undefined;
|
|
36
72
|
let searchCursor: string | undefined;
|
|
37
73
|
let isLoading = false;
|
|
@@ -39,6 +75,25 @@ let hasMoreBookmarks = true;
|
|
|
39
75
|
let hasMoreSearch = true;
|
|
40
76
|
let currentSearchQuery = '';
|
|
41
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Send message to background service worker
|
|
80
|
+
*/
|
|
81
|
+
async function sendMessage<T>(message: MessageType): Promise<T> {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
chrome.runtime.sendMessage(message, (response: ResponseType) => {
|
|
84
|
+
if (chrome.runtime.lastError) {
|
|
85
|
+
reject(new Error(chrome.runtime.lastError.message));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (response.success) {
|
|
89
|
+
resolve(response.data as T);
|
|
90
|
+
} else {
|
|
91
|
+
reject(new Error(response.error));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
42
97
|
/**
|
|
43
98
|
* Update status display
|
|
44
99
|
*/
|
|
@@ -67,7 +122,7 @@ function formatNumber(num: number): string {
|
|
|
67
122
|
/**
|
|
68
123
|
* Display user profile
|
|
69
124
|
*/
|
|
70
|
-
function displayProfile(user:
|
|
125
|
+
function displayProfile(user: SerializedUser) {
|
|
71
126
|
profileAvatarEl.src = user.profileImage || '';
|
|
72
127
|
profileNameEl.textContent = user.fullName || 'Unknown';
|
|
73
128
|
profileUsernameEl.textContent = `@${user.userName || 'unknown'}`;
|
|
@@ -88,7 +143,7 @@ function escapeHtml(text: string): string {
|
|
|
88
143
|
/**
|
|
89
144
|
* Render a single tweet card
|
|
90
145
|
*/
|
|
91
|
-
function renderTweetCard(tweet:
|
|
146
|
+
function renderTweetCard(tweet: SerializedTweet): string {
|
|
92
147
|
const author = tweet.tweetBy;
|
|
93
148
|
const avatarUrl = author?.profileImage || '';
|
|
94
149
|
const authorName = author?.fullName || 'Unknown';
|
|
@@ -135,34 +190,37 @@ function updateContentList() {
|
|
|
135
190
|
itemCountEl.textContent = items.length > 0 ? `${items.length} items` : '';
|
|
136
191
|
|
|
137
192
|
// Only hide load-more when there's definitely no more content
|
|
138
|
-
// The loading indicator is managed by the load functions
|
|
139
193
|
if (!hasMore && !isLoading) {
|
|
140
194
|
loadMoreEl.classList.add('hidden');
|
|
141
195
|
}
|
|
142
196
|
}
|
|
143
197
|
|
|
144
198
|
/**
|
|
145
|
-
* Load more bookmarks
|
|
199
|
+
* Load more bookmarks via background script
|
|
146
200
|
*/
|
|
147
201
|
async function loadMoreBookmarks() {
|
|
148
|
-
if (
|
|
202
|
+
if (isLoading || !hasMoreBookmarks) return;
|
|
149
203
|
|
|
150
204
|
isLoading = true;
|
|
151
205
|
loadMoreEl.classList.remove('hidden');
|
|
152
206
|
|
|
153
207
|
try {
|
|
154
|
-
const result = await
|
|
208
|
+
const result = await sendMessage<CursoredResponse<SerializedTweet>>({
|
|
209
|
+
type: 'GET_BOOKMARKS',
|
|
210
|
+
count: 20,
|
|
211
|
+
cursor: bookmarksCursor,
|
|
212
|
+
});
|
|
155
213
|
|
|
156
214
|
if (!result.list || result.list.length === 0) {
|
|
157
215
|
hasMoreBookmarks = false;
|
|
158
216
|
} else {
|
|
159
217
|
bookmarks.push(...result.list);
|
|
160
|
-
bookmarksCursor = result.next;
|
|
218
|
+
bookmarksCursor = result.next || undefined;
|
|
161
219
|
hasMoreBookmarks = !!bookmarksCursor;
|
|
162
220
|
}
|
|
163
221
|
} catch (error) {
|
|
164
222
|
console.error('Error loading bookmarks:', error);
|
|
165
|
-
hasMoreBookmarks = false;
|
|
223
|
+
hasMoreBookmarks = false;
|
|
166
224
|
} finally {
|
|
167
225
|
isLoading = false;
|
|
168
226
|
loadMoreEl.classList.add('hidden');
|
|
@@ -171,37 +229,37 @@ async function loadMoreBookmarks() {
|
|
|
171
229
|
}
|
|
172
230
|
|
|
173
231
|
/**
|
|
174
|
-
* Load more search results
|
|
232
|
+
* Load more search results via background script
|
|
175
233
|
*/
|
|
176
234
|
async function loadMoreSearchResults() {
|
|
177
|
-
if (
|
|
235
|
+
if (isLoading || !hasMoreSearch || !currentSearchQuery) return;
|
|
178
236
|
|
|
179
237
|
isLoading = true;
|
|
180
238
|
loadMoreEl.classList.remove('hidden');
|
|
181
239
|
|
|
182
240
|
try {
|
|
183
241
|
console.log('Searching for:', currentSearchQuery, 'cursor:', searchCursor);
|
|
184
|
-
const result = await
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
242
|
+
const result = await sendMessage<CursoredResponse<SerializedTweet>>({
|
|
243
|
+
type: 'SEARCH_TWEETS',
|
|
244
|
+
query: currentSearchQuery,
|
|
245
|
+
count: 20,
|
|
246
|
+
cursor: searchCursor,
|
|
247
|
+
});
|
|
189
248
|
console.log('Search result:', result);
|
|
190
249
|
|
|
191
250
|
if (!result.list || result.list.length === 0) {
|
|
192
251
|
hasMoreSearch = false;
|
|
193
252
|
} else {
|
|
194
253
|
searchResults.push(...result.list);
|
|
195
|
-
searchCursor = result.next;
|
|
254
|
+
searchCursor = result.next || undefined;
|
|
196
255
|
hasMoreSearch = !!searchCursor;
|
|
197
256
|
}
|
|
198
257
|
} catch (error) {
|
|
199
258
|
console.error('Error loading search results:', error);
|
|
200
|
-
// Show error to user
|
|
201
259
|
if (searchResults.length === 0) {
|
|
202
260
|
contentListEl.innerHTML = `<div class="empty-state">Search failed: ${error instanceof Error ? error.message : 'Unknown error'}</div>`;
|
|
203
261
|
}
|
|
204
|
-
hasMoreSearch = false;
|
|
262
|
+
hasMoreSearch = false;
|
|
205
263
|
} finally {
|
|
206
264
|
isLoading = false;
|
|
207
265
|
loadMoreEl.classList.add('hidden');
|
|
@@ -213,8 +271,6 @@ async function loadMoreSearchResults() {
|
|
|
213
271
|
* Perform search
|
|
214
272
|
*/
|
|
215
273
|
async function performSearch() {
|
|
216
|
-
if (!rettiwt) return;
|
|
217
|
-
|
|
218
274
|
const query = searchInputEl.value.trim();
|
|
219
275
|
if (!query) return;
|
|
220
276
|
|
|
@@ -270,7 +326,14 @@ function handleScroll() {
|
|
|
270
326
|
|
|
271
327
|
// Load more when scrolled near bottom (within 150px)
|
|
272
328
|
if (scrollTop + clientHeight >= scrollHeight - 150) {
|
|
273
|
-
console.log('Near bottom detected', {
|
|
329
|
+
console.log('Near bottom detected', {
|
|
330
|
+
scrollTop,
|
|
331
|
+
scrollHeight,
|
|
332
|
+
clientHeight,
|
|
333
|
+
isLoading,
|
|
334
|
+
hasMoreBookmarks,
|
|
335
|
+
hasMoreSearch,
|
|
336
|
+
});
|
|
274
337
|
if (currentTab === 'bookmarks' && !isLoading && hasMoreBookmarks) {
|
|
275
338
|
loadMoreBookmarks();
|
|
276
339
|
} else if (currentTab === 'search' && currentSearchQuery && !isLoading && hasMoreSearch) {
|
|
@@ -288,12 +351,10 @@ async function initialize() {
|
|
|
288
351
|
mainContentEl.classList.add('hidden');
|
|
289
352
|
|
|
290
353
|
try {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// Check if logged in (just checks cookies, no API call)
|
|
294
|
-
const isLoggedIn = await rettiwt.isLoggedIn();
|
|
354
|
+
// Check if logged in via background script
|
|
355
|
+
const loginResult = await sendMessage<{ isLoggedIn: boolean }>({ type: 'CHECK_LOGIN' });
|
|
295
356
|
|
|
296
|
-
if (!isLoggedIn) {
|
|
357
|
+
if (!loginResult.isLoggedIn) {
|
|
297
358
|
setStatus('Not logged in to X.com', 'error');
|
|
298
359
|
loginPromptEl.classList.remove('hidden');
|
|
299
360
|
return;
|
|
@@ -301,12 +362,13 @@ async function initialize() {
|
|
|
301
362
|
|
|
302
363
|
setStatus('Verifying credentials...', 'loading');
|
|
303
364
|
|
|
304
|
-
// Initialize and verify
|
|
305
|
-
const
|
|
365
|
+
// Initialize and verify via background script
|
|
366
|
+
const initResult = await sendMessage<{ user: SerializedUser }>({ type: 'INITIALIZE' });
|
|
367
|
+
currentUser = initResult.user;
|
|
306
368
|
|
|
307
369
|
setStatus('Successfully connected!', 'success');
|
|
308
370
|
mainContentEl.classList.remove('hidden');
|
|
309
|
-
displayProfile(
|
|
371
|
+
displayProfile(currentUser);
|
|
310
372
|
|
|
311
373
|
// Auto-load bookmarks
|
|
312
374
|
await loadMoreBookmarks();
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
|
+
// @ts-ignore - linkedom types are incompatible with DOM types but work at runtime
|
|
3
|
+
import { parseHTML as linkedomParseHTML } from 'linkedom';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
6
|
+
* DOM parsing adapter for browser environments.
|
|
7
|
+
* Uses linkedom for compatibility with both popup and service worker (background) contexts.
|
|
8
|
+
* DOMParser is not available in service workers, so we use linkedom which works everywhere.
|
|
4
9
|
*
|
|
5
10
|
* @internal
|
|
6
11
|
*/
|
|
@@ -13,7 +18,9 @@ export class DomAdapter {
|
|
|
13
18
|
* @returns The parsed Document
|
|
14
19
|
*/
|
|
15
20
|
static parseHTML(html: string): Document {
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
// Use linkedom for service worker compatibility
|
|
22
|
+
// linkedom works in all JS environments (popup, content script, service worker)
|
|
23
|
+
const { document } = linkedomParseHTML(html);
|
|
24
|
+
return document as unknown as Document;
|
|
18
25
|
}
|
|
19
26
|
}
|
package/tsconfig.browser.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
|
70
70
|
// "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
71
71
|
/* Type Checking */
|
|
72
|
-
"strict": true /* Enable all strict type-checking options. */
|
|
72
|
+
"strict": true, /* Enable all strict type-checking options. */
|
|
73
|
+
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
|
73
74
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
|
74
75
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
|
75
76
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|