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