Thread viewer for Bluesky
at master 4.5 kB view raw
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, '&amp;') 131 .replace(/</g, '&lt;') 132 .replace(/>/g,'&gt;'); 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}