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, '&amp;') 97 .replace(/</g, '&lt;') 98 .replace(/>/g,'&gt;'); 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}