+14
-14
embed_component.js
+14
-14
embed_component.js
···
10
this.embed = embed;
11
}
12
13
-
/** @returns {AnyElement} */
14
15
buildElement() {
16
if (this.embed instanceof RawRecordEmbed) {
···
54
}
55
}
56
57
-
/** @returns {AnyElement} */
58
59
quotedPostPlaceholder() {
60
return $tag('div.quote-embed', {
···
62
});
63
}
64
65
-
/** @param {InlineRecordEmbed | InlineRecordWithMediaEmbed} embed, @returns {AnyElement} */
66
67
buildQuotedPostElement(embed) {
68
let div = $tag('div.quote-embed');
···
88
return div;
89
}
90
91
-
/** @params {RawLinkEmbed | InlineLinkEmbed} embed, @returns {AnyElement} */
92
93
buildLinkComponent(embed) {
94
let hostname;
···
128
return a;
129
}
130
131
-
/** @param {FeedGeneratorRecord} feedgen, @returns {AnyElement} */
132
133
buildFeedGeneratorView(feedgen) {
134
let link = this.linkToFeedGenerator(feedgen);
···
137
let box = $tag('div');
138
139
if (feedgen.avatar) {
140
-
let avatar = $tag('img.avatar');
141
avatar.src = feedgen.avatar;
142
box.append(avatar);
143
}
···
167
return `https://bsky.app/profile/${repo}/feed/${rkey}`;
168
}
169
170
-
/** @param {UserListRecord} list, @returns {AnyElement} */
171
172
buildUserListView(list) {
173
let link = this.linkToUserList(list);
···
176
let box = $tag('div');
177
178
if (list.avatar) {
179
-
let avatar = $tag('img.avatar');
180
avatar.src = list.avatar;
181
box.append(avatar);
182
}
···
207
return a;
208
}
209
210
-
/** @param {StarterPackRecord} pack, @returns {AnyElement} */
211
212
buildStarterPackView(pack) {
213
let { repo, rkey } = atURI(pack.uri);
···
236
return `https://bsky.app/profile/${repo}/lists/${rkey}`;
237
}
238
239
-
/** @params {RawImageEmbed | InlineImageEmbed} embed, @returns {AnyElement} */
240
241
buildImagesComponent(embed) {
242
let wrapper = $tag('div');
···
246
p.append('[');
247
248
// TODO: load image
249
-
let a = $tag('a', { text: "Image" });
250
251
if (image.fullsize) {
252
a.href = image.fullsize;
···
272
return wrapper;
273
}
274
275
-
/** @params {RawVideoEmbed | InlineVideoEmbed} embed, @returns {AnyElement} */
276
277
buildVideoComponent(embed) {
278
let wrapper = $tag('div');
279
280
// TODO: load thumbnail
281
-
let a = $tag('a', { text: "Video" });
282
283
if (embed.playlistURL) {
284
a.href = embed.playlistURL;
···
303
return wrapper;
304
}
305
306
-
/** @param {string} uri, @param {AnyElement} div, @returns Promise<void> */
307
308
async loadQuotedPost(uri, div) {
309
let record = await api.loadPostIfExists(uri);
···
10
this.embed = embed;
11
}
12
13
+
/** @returns {HTMLElement} */
14
15
buildElement() {
16
if (this.embed instanceof RawRecordEmbed) {
···
54
}
55
}
56
57
+
/** @returns {HTMLElement} */
58
59
quotedPostPlaceholder() {
60
return $tag('div.quote-embed', {
···
62
});
63
}
64
65
+
/** @param {InlineRecordEmbed | InlineRecordWithMediaEmbed} embed, @returns {HTMLElement} */
66
67
buildQuotedPostElement(embed) {
68
let div = $tag('div.quote-embed');
···
88
return div;
89
}
90
91
+
/** @params {RawLinkEmbed | InlineLinkEmbed} embed, @returns {HTMLElement} */
92
93
buildLinkComponent(embed) {
94
let hostname;
···
128
return a;
129
}
130
131
+
/** @param {FeedGeneratorRecord} feedgen, @returns {HTMLElement} */
132
133
buildFeedGeneratorView(feedgen) {
134
let link = this.linkToFeedGenerator(feedgen);
···
137
let box = $tag('div');
138
139
if (feedgen.avatar) {
140
+
let avatar = $tag('img.avatar', HTMLImageElement);
141
avatar.src = feedgen.avatar;
142
box.append(avatar);
143
}
···
167
return `https://bsky.app/profile/${repo}/feed/${rkey}`;
168
}
169
170
+
/** @param {UserListRecord} list, @returns {HTMLElement} */
171
172
buildUserListView(list) {
173
let link = this.linkToUserList(list);
···
176
let box = $tag('div');
177
178
if (list.avatar) {
179
+
let avatar = $tag('img.avatar', HTMLImageElement);
180
avatar.src = list.avatar;
181
box.append(avatar);
182
}
···
207
return a;
208
}
209
210
+
/** @param {StarterPackRecord} pack, @returns {HTMLElement} */
211
212
buildStarterPackView(pack) {
213
let { repo, rkey } = atURI(pack.uri);
···
236
return `https://bsky.app/profile/${repo}/lists/${rkey}`;
237
}
238
239
+
/** @params {RawImageEmbed | InlineImageEmbed} embed, @returns {HTMLElement} */
240
241
buildImagesComponent(embed) {
242
let wrapper = $tag('div');
···
246
p.append('[');
247
248
// TODO: load image
249
+
let a = $tag('a', { text: "Image" }, HTMLLinkElement);
250
251
if (image.fullsize) {
252
a.href = image.fullsize;
···
272
return wrapper;
273
}
274
275
+
/** @params {RawVideoEmbed | InlineVideoEmbed} embed, @returns {HTMLElement} */
276
277
buildVideoComponent(embed) {
278
let wrapper = $tag('div');
279
280
// TODO: load thumbnail
281
+
let a = $tag('a', { text: "Video" }, HTMLLinkElement);
282
283
if (embed.playlistURL) {
284
a.href = embed.playlistURL;
···
303
return wrapper;
304
}
305
306
+
/** @param {string} uri, @param {HTMLElement} div, @returns Promise<void> */
307
308
async loadQuotedPost(uri, div) {
309
let record = await api.loadPostIfExists(uri);
+31
-27
post_component.js
+31
-27
post_component.js
···
5
class PostComponent {
6
/**
7
* Post component's root HTML element, if built.
8
-
* @type {AnyElement | undefined}
9
*/
10
_rootElement;
11
···
26
}
27
28
/**
29
-
* @returns {AnyElement}
30
*/
31
get rootElement() {
32
if (!this._rootElement) {
···
91
}
92
}
93
94
-
/** @param {AnyElement} nodeToUpdate */
95
installIntoElement(nodeToUpdate) {
96
let view = this.buildElement();
97
98
-
nodeToUpdate.querySelector('.content').replaceWith(view.querySelector('.content'));
99
this._rootElement = nodeToUpdate;
100
}
101
102
-
/** @returns {AnyElement} */
103
buildElement() {
104
if (this._rootElement) {
105
return this._rootElement;
···
196
return div;
197
}
198
199
-
/** @returns {AnyElement} */
200
201
buildPostHeader() {
202
let timeFormat = this.timeFormatForTimestamp;
···
260
/** @param {string} url, @returns {HTMLImageElement} */
261
262
buildUserAvatar(url) {
263
-
let avatar = $tag('img.avatar', { loading: 'lazy' }); // needs to be set before src!
264
avatar.src = url;
265
window.avatarPreloader.observe(avatar);
266
return avatar;
267
}
268
269
-
/** @returns {AnyElement} */
270
271
buildPostBody() {
272
if (this.post.originalFediContent) {
···
297
return p;
298
}
299
300
-
/** @param {string[]} tags, @returns {AnyElement} */
301
302
buildTagsRow(tags) {
303
let p = $tag('p.tags');
···
313
return p;
314
}
315
316
-
/** @returns {AnyElement} */
317
318
buildStatsFooter() {
319
let stats = $tag('p.stats');
···
347
return stats;
348
}
349
350
-
/** @param {number} count, @param {boolean} expanded, @returns {AnyElement} */
351
352
buildQuotesIconLink(count, expanded) {
353
let q = new URL(getLocation());
···
369
/** @param {number} quoteCount, @param {boolean} expanded */
370
371
appendQuotesIconLink(quoteCount, expanded) {
372
-
let stats = this.rootElement.querySelector(':scope > .content > p.stats');
373
let quotesLink = this.buildQuotesIconLink(quoteCount, expanded);
374
stats.append(quotesLink);
375
}
376
377
-
/** @returns {AnyElement} */
378
379
buildLoadMoreLink() {
380
let loadMore = $tag('p');
···
394
return loadMore;
395
}
396
397
-
/** @returns {AnyElement} */
398
399
buildHiddenRepliesLink() {
400
let loadMore = $tag('p.hidden-replies');
···
419
return loadMore;
420
}
421
422
-
/** @param {HTMLLinkElement} loadMoreButton */
423
424
loadHiddenReplies(loadMoreButton) {
425
loadMoreButton.innerHTML = `<img class="loader" src="icons/sunny.png">`;
···
443
});
444
}
445
446
-
/** @param {AnyElement} div, @returns {AnyElement} */
447
448
buildBlockedPostElement(div) {
449
let p = $tag('p.blocked-header');
···
458
let blockStatus = this.post.blockedByUser ? 'has blocked you' : this.post.blocksUser ? "you've blocked them" : '';
459
blockStatus = blockStatus ? `, ${blockStatus}` : '';
460
461
-
let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' });
462
p.append(' (', authorLink, blockStatus, ') ');
463
div.appendChild(p);
464
···
479
return div;
480
}
481
482
-
/** @param {AnyElement} div, @returns {AnyElement} */
483
484
buildDetachedQuoteElement(div) {
485
let p = $tag('p.blocked-header');
···
491
return p;
492
}
493
494
-
let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' });
495
p.append(' (', authorLink, ') ');
496
div.appendChild(p);
497
···
512
return div;
513
}
514
515
-
/** @param {AnyElement} div, @returns {AnyElement} */
516
517
buildMissingPostElement(div) {
518
let p = $tag('p.blocked-header');
519
p.innerHTML = `<i class="fa-solid fa-ban"></i> <span>Deleted post</span>`;
520
521
-
let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' });
522
p.append(' (', authorLink, ') ');
523
524
this.loadReferencedPostAuthor(authorLink);
···
528
return div;
529
}
530
531
-
/** @param {string} uri, @param {AnyElement} div, @returns Promise<void> */
532
533
async loadBlockedPost(uri, div) {
534
let record = await appView.loadPostIfExists(this.post.uri);
···
554
html: `<i class="fa-solid fa-arrows-split-up-and-left fa-rotate-180"></i>`
555
});
556
557
-
let header = div.querySelector('p.blocked-header');
558
let separator = $tag('span.separator', { html: '•' });
559
header.append(separator, ' ', a);
560
}
561
562
-
div.querySelector('p.load-post').remove();
563
564
if (this.isRoot && this.post.parentReference) {
565
let { repo, rkey } = atURI(this.post.parentReference.uri);
···
590
}
591
592
toggleSectionFold() {
593
-
let plus = this.rootElement.querySelector(':scope > .margin .plus');
594
595
if (this.isCollapsed()) {
596
this.rootElement.classList.remove('collapsed');
···
601
}
602
}
603
604
-
/** @param {AnyElement} heart */
605
606
onHeartClick(heart) {
607
if (!this.post.hasViewerInfo) {
···
627
return;
628
}
629
630
-
let count = heart.nextElementSibling;
631
632
if (!heart.classList.contains('liked')) {
633
accountAPI.likePost(this.post).then((like) => {
···
5
class PostComponent {
6
/**
7
* Post component's root HTML element, if built.
8
+
* @type {HTMLElement | undefined}
9
*/
10
_rootElement;
11
···
26
}
27
28
/**
29
+
* @returns {HTMLElement}
30
*/
31
get rootElement() {
32
if (!this._rootElement) {
···
91
}
92
}
93
94
+
/** @param {HTMLElement} nodeToUpdate */
95
installIntoElement(nodeToUpdate) {
96
let view = this.buildElement();
97
98
+
let oldContent = $(nodeToUpdate.querySelector('.content'));
99
+
let newContent = $(view.querySelector('.content'));
100
+
oldContent.replaceWith(newContent);
101
+
102
this._rootElement = nodeToUpdate;
103
}
104
105
+
/** @returns {HTMLElement} */
106
buildElement() {
107
if (this._rootElement) {
108
return this._rootElement;
···
199
return div;
200
}
201
202
+
/** @returns {HTMLElement} */
203
204
buildPostHeader() {
205
let timeFormat = this.timeFormatForTimestamp;
···
263
/** @param {string} url, @returns {HTMLImageElement} */
264
265
buildUserAvatar(url) {
266
+
let avatar = $tag('img.avatar', { loading: 'lazy' }, HTMLImageElement); // needs to be set before src!
267
avatar.src = url;
268
window.avatarPreloader.observe(avatar);
269
return avatar;
270
}
271
272
+
/** @returns {HTMLElement} */
273
274
buildPostBody() {
275
if (this.post.originalFediContent) {
···
300
return p;
301
}
302
303
+
/** @param {string[]} tags, @returns {HTMLElement} */
304
305
buildTagsRow(tags) {
306
let p = $tag('p.tags');
···
316
return p;
317
}
318
319
+
/** @returns {HTMLElement} */
320
321
buildStatsFooter() {
322
let stats = $tag('p.stats');
···
350
return stats;
351
}
352
353
+
/** @param {number} count, @param {boolean} expanded, @returns {HTMLElement} */
354
355
buildQuotesIconLink(count, expanded) {
356
let q = new URL(getLocation());
···
372
/** @param {number} quoteCount, @param {boolean} expanded */
373
374
appendQuotesIconLink(quoteCount, expanded) {
375
+
let stats = $(this.rootElement.querySelector(':scope > .content > p.stats'));
376
let quotesLink = this.buildQuotesIconLink(quoteCount, expanded);
377
stats.append(quotesLink);
378
}
379
380
+
/** @returns {HTMLElement} */
381
382
buildLoadMoreLink() {
383
let loadMore = $tag('p');
···
397
return loadMore;
398
}
399
400
+
/** @returns {HTMLElement} */
401
402
buildHiddenRepliesLink() {
403
let loadMore = $tag('p.hidden-replies');
···
422
return loadMore;
423
}
424
425
+
/** @param {HTMLElement} loadMoreButton */
426
427
loadHiddenReplies(loadMoreButton) {
428
loadMoreButton.innerHTML = `<img class="loader" src="icons/sunny.png">`;
···
446
});
447
}
448
449
+
/** @param {HTMLElement} div, @returns {HTMLElement} */
450
451
buildBlockedPostElement(div) {
452
let p = $tag('p.blocked-header');
···
461
let blockStatus = this.post.blockedByUser ? 'has blocked you' : this.post.blocksUser ? "you've blocked them" : '';
462
blockStatus = blockStatus ? `, ${blockStatus}` : '';
463
464
+
let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' }, HTMLLinkElement);
465
p.append(' (', authorLink, blockStatus, ') ');
466
div.appendChild(p);
467
···
482
return div;
483
}
484
485
+
/** @param {HTMLElement} div, @returns {HTMLElement} */
486
487
buildDetachedQuoteElement(div) {
488
let p = $tag('p.blocked-header');
···
494
return p;
495
}
496
497
+
let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' }, HTMLLinkElement);
498
p.append(' (', authorLink, ') ');
499
div.appendChild(p);
500
···
515
return div;
516
}
517
518
+
/** @param {HTMLElement} div, @returns {HTMLElement} */
519
520
buildMissingPostElement(div) {
521
let p = $tag('p.blocked-header');
522
p.innerHTML = `<i class="fa-solid fa-ban"></i> <span>Deleted post</span>`;
523
524
+
let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' }, HTMLLinkElement);
525
p.append(' (', authorLink, ') ');
526
527
this.loadReferencedPostAuthor(authorLink);
···
531
return div;
532
}
533
534
+
/** @param {string} uri, @param {HTMLElement} div, @returns Promise<void> */
535
536
async loadBlockedPost(uri, div) {
537
let record = await appView.loadPostIfExists(this.post.uri);
···
557
html: `<i class="fa-solid fa-arrows-split-up-and-left fa-rotate-180"></i>`
558
});
559
560
+
let header = $(div.querySelector('p.blocked-header'));
561
let separator = $tag('span.separator', { html: '•' });
562
header.append(separator, ' ', a);
563
}
564
565
+
let loadPost = $(div.querySelector('p.load-post'));
566
+
loadPost.remove();
567
568
if (this.isRoot && this.post.parentReference) {
569
let { repo, rkey } = atURI(this.post.parentReference.uri);
···
594
}
595
596
toggleSectionFold() {
597
+
let plus = $(this.rootElement.querySelector(':scope > .margin .plus'), HTMLImageElement);
598
599
if (this.isCollapsed()) {
600
this.rootElement.classList.remove('collapsed');
···
605
}
606
}
607
608
+
/** @param {HTMLElement} heart */
609
610
onHeartClick(heart) {
611
if (!this.post.hasViewerInfo) {
···
631
return;
632
}
633
634
+
let count = $(heart.nextElementSibling);
635
636
if (!heart.classList.contains('liked')) {
637
accountAPI.likePost(this.post).then((like) => {
+45
-37
skythread.js
+45
-37
skythread.js
···
1
function init() {
2
-
let document = /** @type {AnyElement} */ (/** @type {unknown} */ (window.document));
3
-
let html = /** @type {AnyElement} */ (/** @type {unknown} */ (window.document.body.parentNode));
4
5
window.dateLocale = localStorage.getItem('locale') || undefined;
6
window.isIncognito = !!localStorage.getItem('incognito');
7
window.biohazardEnabled = JSON.parse(localStorage.getItem('biohazard') ?? 'null');
8
9
-
window.loginDialog = document.querySelector('#login');
10
-
window.accountMenu = document.querySelector('#account_menu');
11
12
window.avatarPreloader = buildAvatarPreloader();
13
···
15
$id('account_menu').style.visibility = 'hidden';
16
});
17
18
-
document.querySelector('#search form').addEventListener('submit', (e) => {
19
e.preventDefault();
20
submitSearch();
21
});
···
34
});
35
}
36
37
-
document.querySelector('#login .info a').addEventListener('click', (e) => {
38
e.preventDefault();
39
toggleLoginInfo();
40
});
41
42
-
document.querySelector('#login form').addEventListener('submit', (e) => {
43
e.preventDefault();
44
submitLogin();
45
});
46
47
-
document.querySelector('#biohazard_show').addEventListener('click', (e) => {
48
e.preventDefault();
49
50
window.biohazardEnabled = true;
···
55
window.loadInfohazard = undefined;
56
}
57
58
-
let target = /** @type {AnyElement} */ (/** @type {unknown} */ (e.target));
59
60
hideDialog(target.closest('.dialog'));
61
});
62
63
-
document.querySelector('#biohazard_hide').addEventListener('click', (e) => {
64
e.preventDefault();
65
66
window.biohazardEnabled = false;
···
68
toggleMenuButton('biohazard', false);
69
70
for (let p of document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post')) {
71
-
p.style.display = 'none';
72
}
73
74
-
let target = /** @type {AnyElement} */ (/** @type {unknown} */ (e.target));
75
76
hideDialog(target.closest('.dialog'));
77
});
78
79
-
document.querySelector('#account').addEventListener('click', (e) => {
80
toggleAccountMenu();
81
e.stopPropagation();
82
});
···
85
e.stopPropagation();
86
});
87
88
-
accountMenu.querySelector('a[data-action=biohazard]').addEventListener('click', (e) => {
89
e.preventDefault();
90
91
let hazards = document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post');
···
94
window.biohazardEnabled = true;
95
localStorage.setItem('biohazard', 'true');
96
toggleMenuButton('biohazard', true);
97
-
Array.from(hazards).forEach(p => { p.style.display = 'block' });
98
} else {
99
window.biohazardEnabled = false;
100
localStorage.setItem('biohazard', 'false');
101
toggleMenuButton('biohazard', false);
102
-
Array.from(hazards).forEach(p => { p.style.display = 'none' });
103
}
104
});
105
106
-
accountMenu.querySelector('a[data-action=incognito]').addEventListener('click', (e) => {
107
e.preventDefault();
108
109
if (isIncognito) {
···
115
location.reload();
116
});
117
118
-
accountMenu.querySelector('a[data-action=login]').addEventListener('click', (e) => {
119
e.preventDefault();
120
toggleDialog(loginDialog);
121
$id('account_menu').style.visibility = 'hidden';
122
});
123
124
-
accountMenu.querySelector('a[data-action=logout]').addEventListener('click', (e) => {
125
e.preventDefault();
126
logOut();
127
});
···
181
}
182
}
183
184
-
/** @param {AnyPost} post, @returns {AnyElement} */
185
186
function buildParentLink(post) {
187
let p = $tag('p.back');
···
189
if (post instanceof BlockedPost) {
190
let element = new PostComponent(post, 'parent').buildElement();
191
element.className = 'back';
192
-
element.querySelector('p.blocked-header span').innerText = 'Parent post blocked';
193
return element;
194
} else if (post instanceof MissingPost) {
195
p.innerHTML = `<i class="fa-solid fa-ban"></i> parent post has been deleted`;
···
226
}
227
228
function showSearch() {
229
-
$id('search').style.visibility = 'visible';
230
-
$id('search').querySelector('input[type=text]').focus();
231
}
232
233
function hideSearch() {
···
271
/** @param {string} buttonName */
272
273
function showMenuButton(buttonName) {
274
-
let button = accountMenu.querySelector(`a[data-action=${buttonName}]`);
275
-
button.parentNode.style.display = 'list-item';
276
}
277
278
/** @param {string} buttonName */
279
280
function hideMenuButton(buttonName) {
281
-
let button = accountMenu.querySelector(`a[data-action=${buttonName}]`);
282
-
button.parentNode.style.display = 'none';
283
}
284
285
/** @param {string} buttonName, @param {boolean} state */
286
287
function toggleMenuButton(buttonName, state) {
288
-
let button = accountMenu.querySelector(`a[data-action=${buttonName}]`);
289
-
button.querySelector('.check').style.display = (state) ? 'inline' : 'none';
290
}
291
292
/** @param {boolean | 'incognito'} loggedIn, @param {string | undefined | null} [avatar] */
···
295
let account = $id('account');
296
297
if (loggedIn === true && avatar) {
298
-
let button = account.querySelector('i');
299
300
let img = $tag('img.avatar', { src: avatar });
301
img.style.display = 'none';
···
318
}
319
320
function submitLogin() {
321
-
let handle = $id('login_handle');
322
-
let password = $id('login_password');
323
let submit = $id('login_submit');
324
let cloudy = $id('cloudy');
325
···
401
}
402
403
function submitSearch() {
404
-
let url = $id('search').querySelector('input[name=q]').value.trim();
405
406
if (!url) { return }
407
···
702
});
703
}
704
705
-
/** @param {Post} post, @param {AnyElement} nodeToUpdate */
706
707
function loadSubtree(post, nodeToUpdate) {
708
api.loadThreadByAtURI(post.uri).then(json => {
···
715
}).catch(showError);
716
}
717
718
-
/** @param {Post} post, @param {AnyElement} nodeToUpdate */
719
720
function loadHiddenSubtree(post, nodeToUpdate) {
721
-
let content = nodeToUpdate.querySelector('.content');
722
-
let hiddenRepliesDiv = content.querySelector(':scope > .hidden-replies');
723
724
blueAPI.getReplies(post.uri).then(replies => {
725
let missingReplies = replies.filter(r => !post.replies.some(x => x.uri === r));
···
1
function init() {
2
+
let html = $(document.body.parentNode);
3
4
window.dateLocale = localStorage.getItem('locale') || undefined;
5
window.isIncognito = !!localStorage.getItem('incognito');
6
window.biohazardEnabled = JSON.parse(localStorage.getItem('biohazard') ?? 'null');
7
8
+
window.loginDialog = $(document.querySelector('#login'));
9
+
window.accountMenu = $(document.querySelector('#account_menu'));
10
11
window.avatarPreloader = buildAvatarPreloader();
12
···
14
$id('account_menu').style.visibility = 'hidden';
15
});
16
17
+
$(document.querySelector('#search form')).addEventListener('submit', (e) => {
18
e.preventDefault();
19
submitSearch();
20
});
···
33
});
34
}
35
36
+
$(document.querySelector('#login .info a')).addEventListener('click', (e) => {
37
e.preventDefault();
38
toggleLoginInfo();
39
});
40
41
+
$(document.querySelector('#login form')).addEventListener('submit', (e) => {
42
e.preventDefault();
43
submitLogin();
44
});
45
46
+
$(document.querySelector('#biohazard_show')).addEventListener('click', (e) => {
47
e.preventDefault();
48
49
window.biohazardEnabled = true;
···
54
window.loadInfohazard = undefined;
55
}
56
57
+
let target = $(e.target);
58
59
hideDialog(target.closest('.dialog'));
60
});
61
62
+
$(document.querySelector('#biohazard_hide')).addEventListener('click', (e) => {
63
e.preventDefault();
64
65
window.biohazardEnabled = false;
···
67
toggleMenuButton('biohazard', false);
68
69
for (let p of document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post')) {
70
+
$(p).style.display = 'none';
71
}
72
73
+
let target = $(e.target);
74
75
hideDialog(target.closest('.dialog'));
76
});
77
78
+
$(document.querySelector('#account')).addEventListener('click', (e) => {
79
toggleAccountMenu();
80
e.stopPropagation();
81
});
···
84
e.stopPropagation();
85
});
86
87
+
$(accountMenu.querySelector('a[data-action=biohazard]')).addEventListener('click', (e) => {
88
e.preventDefault();
89
90
let hazards = document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post');
···
93
window.biohazardEnabled = true;
94
localStorage.setItem('biohazard', 'true');
95
toggleMenuButton('biohazard', true);
96
+
Array.from(hazards).forEach(p => { $(p).style.display = 'block' });
97
} else {
98
window.biohazardEnabled = false;
99
localStorage.setItem('biohazard', 'false');
100
toggleMenuButton('biohazard', false);
101
+
Array.from(hazards).forEach(p => { $(p).style.display = 'none' });
102
}
103
});
104
105
+
$(accountMenu.querySelector('a[data-action=incognito]')).addEventListener('click', (e) => {
106
e.preventDefault();
107
108
if (isIncognito) {
···
114
location.reload();
115
});
116
117
+
$(accountMenu.querySelector('a[data-action=login]')).addEventListener('click', (e) => {
118
e.preventDefault();
119
toggleDialog(loginDialog);
120
$id('account_menu').style.visibility = 'hidden';
121
});
122
123
+
$(accountMenu.querySelector('a[data-action=logout]')).addEventListener('click', (e) => {
124
e.preventDefault();
125
logOut();
126
});
···
180
}
181
}
182
183
+
/** @param {AnyPost} post, @returns {HTMLElement} */
184
185
function buildParentLink(post) {
186
let p = $tag('p.back');
···
188
if (post instanceof BlockedPost) {
189
let element = new PostComponent(post, 'parent').buildElement();
190
element.className = 'back';
191
+
let span = $(element.querySelector('p.blocked-header span'));
192
+
span.innerText = 'Parent post blocked';
193
return element;
194
} else if (post instanceof MissingPost) {
195
p.innerHTML = `<i class="fa-solid fa-ban"></i> parent post has been deleted`;
···
226
}
227
228
function showSearch() {
229
+
let search = $id('search');
230
+
let searchField = $(search.querySelector('input[type=text]'));
231
+
232
+
search.style.visibility = 'visible';
233
+
searchField.focus();
234
}
235
236
function hideSearch() {
···
274
/** @param {string} buttonName */
275
276
function showMenuButton(buttonName) {
277
+
let button = $(accountMenu.querySelector(`a[data-action=${buttonName}]`));
278
+
let item = $(button.parentNode);
279
+
item.style.display = 'list-item';
280
}
281
282
/** @param {string} buttonName */
283
284
function hideMenuButton(buttonName) {
285
+
let button = $(accountMenu.querySelector(`a[data-action=${buttonName}]`));
286
+
let item = $(button.parentNode);
287
+
item.style.display = 'none';
288
}
289
290
/** @param {string} buttonName, @param {boolean} state */
291
292
function toggleMenuButton(buttonName, state) {
293
+
let button = $(accountMenu.querySelector(`a[data-action=${buttonName}]`));
294
+
let check = $(button.querySelector('.check'));
295
+
check.style.display = (state) ? 'inline' : 'none';
296
}
297
298
/** @param {boolean | 'incognito'} loggedIn, @param {string | undefined | null} [avatar] */
···
301
let account = $id('account');
302
303
if (loggedIn === true && avatar) {
304
+
let button = $(account.querySelector('i'));
305
306
let img = $tag('img.avatar', { src: avatar });
307
img.style.display = 'none';
···
324
}
325
326
function submitLogin() {
327
+
let handle = $id('login_handle', HTMLInputElement);
328
+
let password = $id('login_password', HTMLInputElement);
329
let submit = $id('login_submit');
330
let cloudy = $id('cloudy');
331
···
407
}
408
409
function submitSearch() {
410
+
let search = $id('search');
411
+
let searchField = $(search.querySelector('input[name=q]'), HTMLInputElement);
412
+
let url = searchField.value.trim();
413
414
if (!url) { return }
415
···
710
});
711
}
712
713
+
/** @param {Post} post, @param {HTMLElement} nodeToUpdate */
714
715
function loadSubtree(post, nodeToUpdate) {
716
api.loadThreadByAtURI(post.uri).then(json => {
···
723
}).catch(showError);
724
}
725
726
+
/** @param {Post} post, @param {HTMLElement} nodeToUpdate */
727
728
function loadHiddenSubtree(post, nodeToUpdate) {
729
+
let content = $(nodeToUpdate.querySelector('.content'));
730
+
let hiddenRepliesDiv = $(content.querySelector(':scope > .hidden-replies'));
731
732
blueAPI.getReplies(post.uri).then(replies => {
733
let missingReplies = replies.filter(r => !post.replies.some(x => x.uri === r));
+10
-25
types.d.ts
+10
-25
types.d.ts
···
11
declare var api: BlueskyAPI;
12
declare var isIncognito: boolean;
13
declare var biohazardEnabled: boolean;
14
-
declare var loginDialog: AnyElement;
15
-
declare var accountMenu: AnyElement;
16
declare var avatarPreloader: IntersectionObserver;
17
18
-
type SomeElement = Element | HTMLElement | AnyElement;
19
type json = Record<string, any>;
20
21
-
interface AnyElement {
22
-
classList: CSSClassList;
23
-
className: string;
24
-
innerText: string;
25
-
innerHTML: string;
26
-
nextElementSibling: AnyElement;
27
-
parentNode: AnyElement;
28
-
src: string;
29
-
style: CSSStyleDeclaration;
30
31
-
addEventListener<K extends keyof DocumentEventMap>(
32
-
type: K, listener: EventListenerOrEventListenerObject
33
-
): void;
34
35
-
append(...e: Array<string | SomeElement>): void;
36
-
appendChild(e: SomeElement): void;
37
-
closest(q: string): AnyElement;
38
-
querySelector(q: string): AnyElement;
39
-
querySelectorAll(q: string): AnyElement[];
40
-
prepend(...e: Array<string | SomeElement>): void;
41
-
remove(): void;
42
-
replaceChildren(e: SomeElement): void;
43
-
replaceWith(e: SomeElement): void;
44
-
}
···
11
declare var api: BlueskyAPI;
12
declare var isIncognito: boolean;
13
declare var biohazardEnabled: boolean;
14
+
declare var loginDialog: HTMLElement;
15
+
declare var accountMenu: HTMLElement;
16
declare var avatarPreloader: IntersectionObserver;
17
18
type json = Record<string, any>;
19
20
+
function $tag(tag: string): HTMLElement;
21
+
function $tag<T>(tag: string, type: new (...args: any[]) => T): T;
22
+
function $tag(tag: string, params: string | object): HTMLElement;
23
+
function $tag<T>(tag: string, params: string | object, type: new (...args: any[]) => T): T;
24
25
+
function $id(id: string): HTMLElement;
26
+
function $id<T>(id: string, type: new (...args: any[]) => T): T;
27
28
+
function $(element: Node | EventTarget | null): HTMLElement;
29
+
function $<T>(element: Node | EventTarget | null, type: new (...args: any[]) => T): T;
+21
-6
utils.js
+21
-6
utils.js
···
17
}
18
}
19
20
-
/** @param {string} tag, @param {string | object} [params], @returns {any} */
21
22
-
function $tag(tag, params) {
23
let element;
24
let parts = tag.split('.');
25
···
45
}
46
}
47
48
-
return element;
49
}
50
51
-
/** @param {string} name, @returns {any} */
52
53
-
function $id(name) {
54
-
return document.getElementById(name);
55
}
56
57
/** @param {string} uri, @returns {AtURI} */
···
17
}
18
}
19
20
+
/**
21
+
* @template T
22
+
* @param {string} tag
23
+
* @param {string | object} params
24
+
* @param {new (...args: any[]) => T} type
25
+
* @returns {T}
26
+
*/
27
28
+
function $tag(tag, params, type) {
29
let element;
30
let parts = tag.split('.');
31
···
51
}
52
}
53
54
+
return /** @type {T} */ (element);
55
+
}
56
+
57
+
function $id(name, type) {
58
+
return (document.getElementById(name));
59
}
60
61
+
/**
62
+
* @template T
63
+
* @param {Node | EventTarget | null} element
64
+
* @param {new (...args: any[]) => T} type
65
+
* @returns {T}
66
+
*/
67
68
+
function $(element, type) {
69
+
return /** @type {T} */ (element);
70
}
71
72
/** @param {string} uri, @returns {AtURI} */