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