Thread viewer for Bluesky
1class AtURI {
2 /** @param {string} uri */
3 constructor(uri) {
4 if (!uri.startsWith('at://')) {
5 throw new URLError(`Not an at:// URI: ${uri}`);
6 }
7
8 let parts = uri.split('/');
9
10 if (parts.length != 5) {
11 throw new URLError(`Invalid at:// URI: ${uri}`);
12 }
13
14 this.repo = parts[2];
15 this.collection = parts[3];
16 this.rkey = parts[4];
17 }
18}
19
20/**
21 * @typedef {object} PaginatorType
22 * @property {(callback: (boolean) => void) => void} loadInPages
23 * @property {(() => void)=} scrollHandler
24 * @property {ResizeObserver=} resizeObserver
25 */
26
27window.Paginator = {
28 loadInPages(callback) {
29 if (this.scrollHandler) {
30 document.removeEventListener('scroll', this.scrollHandler);
31 }
32
33 if (this.resizeObserver) {
34 this.resizeObserver.disconnect();
35 }
36
37 let loadIfNeeded = () => {
38 if (window.pageYOffset + window.innerHeight > document.body.offsetHeight - 500) {
39 callback(loadIfNeeded);
40 }
41 };
42
43 callback(loadIfNeeded);
44
45 document.addEventListener('scroll', loadIfNeeded);
46 const resizeObserver = new ResizeObserver(loadIfNeeded);
47 resizeObserver.observe(document.body);
48
49 this.scrollHandler = loadIfNeeded;
50 this.resizeObserver = resizeObserver;
51 }
52};
53
54/**
55 * @template T
56 * @param {string} tag
57 * @param {string | object} params
58 * @param {new (...args: any[]) => T} type
59 * @returns {T}
60 */
61
62function $tag(tag, params, type) {
63 let element;
64 let parts = tag.split('.');
65
66 if (parts.length > 1) {
67 let tagName = parts[0];
68 element = document.createElement(tagName);
69 element.className = parts.slice(1).join(' ');
70 } else {
71 element = document.createElement(tag);
72 }
73
74 if (typeof params === 'string') {
75 element.className = element.className + ' ' + params;
76 } else if (params) {
77 for (let key in params) {
78 if (key == 'text') {
79 element.innerText = params[key];
80 } else if (key == 'html') {
81 element.innerHTML = params[key];
82 } else {
83 element[key] = params[key];
84 }
85 }
86 }
87
88 return /** @type {T} */ (element);
89}
90
91/**
92 * @template {HTMLElement} T
93 * @param {string} name
94 * @param {new (...args: any[]) => T} [type]
95 * @returns {T}
96 */
97
98function $id(name, type) {
99 return /** @type {T} */ (document.getElementById(name));
100}
101
102/**
103 * @template {HTMLElement} T
104 * @param {Node | EventTarget | null} element
105 * @param {new (...args: any[]) => T} [type]
106 * @returns {T}
107 */
108
109function $(element, type) {
110 return /** @type {T} */ (element);
111}
112
113/** @param {string} uri, @returns {AtURI} */
114
115function atURI(uri) {
116 return new AtURI(uri);
117}
118
119function castToInt(value) {
120 if (value === undefined || value === null || typeof value == "number") {
121 return value;
122 } else {
123 return parseInt(value, 10);
124 }
125}
126
127/** @param {string} html, @returns {string} */
128
129function escapeHTML(html) {
130 return html.replace(/&/g, '&')
131 .replace(/</g, '<')
132 .replace(/>/g,'>');
133}
134
135/** @param {json} feedPost, @returns {number} */
136
137function feedPostTime(feedPost) {
138 let timestamp = feedPost.reason ? feedPost.reason.indexedAt : feedPost.post.record.createdAt;
139 return Date.parse(timestamp);
140}
141
142/** @param {string} html, @returns {string} */
143
144function sanitizeHTML(html) {
145 return DOMPurify.sanitize(html, {
146 ALLOWED_TAGS: [
147 'a', 'b', 'blockquote', 'br', 'code', 'dd', 'del', 'div', 'dl', 'dt', 'em', 'font',
148 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'li', 'ol', 'p', 'q', 'pre', 's', 'span', 'strong',
149 'sub', 'sup', 'u', 'wbr', '#text'
150 ],
151 ALLOWED_ATTR: [
152 'align', 'alt', 'class', 'clear', 'color', 'dir', 'href', 'lang', 'rel', 'title', 'translate'
153 ]
154 });
155}
156
157/** @returns {string} */
158
159function getLocation() {
160 return location.origin + location.pathname;
161}
162
163/** @param {object} error */
164
165function showError(error) {
166 console.log(error);
167 alert(error);
168}
169
170/** @param {Date} date1, @param {Date} date2, @returns {boolean} */
171
172function sameDay(date1, date2) {
173 return (
174 date1.getDate() == date2.getDate() &&
175 date1.getMonth() == date2.getMonth() &&
176 date1.getFullYear() == date2.getFullYear()
177 );
178}
179
180/** @param {Post} post, @returns {string} */
181
182function linkToPostThread(post) {
183 return linkToPostById(post.author.handle, post.rkey);
184}
185
186/** @param {string} handle, @param {string} postId, @returns {string} */
187
188function linkToPostById(handle, postId) {
189 let url = new URL(getLocation());
190 url.searchParams.set('author', handle);
191 url.searchParams.set('post', postId);
192 return url.toString();
193}