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