A custom element for kibun.social statuses. Built atop https://tangled.org/strings/did:plc:2sqok7oqqrhtmmmb5sulkrw2/3m6cyhxo2pw22
kibun-status.js
173 lines 5.1 kB view raw
1const urls = { 2 identityResolveMiniDoc: (username) => `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${username}`, 3 repoGetRecord: (userDidDoc) => `${userDidDoc.pds}/xrpc/com.atproto.repo.getRecord?repo=${userDidDoc.did}&collection=app.bsky.actor.profile&rkey=self`, 4 repoListRecords: (userDidDoc) => `${userDidDoc.pds}/xrpc/com.atproto.repo.listRecords?repo=${userDidDoc.did}&collection=social.kibun.status&limit=1`, 5} 6 7const defaultStyles = ` 8:host { 9 display: block; 10} 11#container { 12 border: 1px #7dd3fc solid; 13 box-shadow: 4px 4px 0 #7dd3fc; 14 padding: 20px; 15 max-width: 400px; 16 background-color: #FFFFFF; 17 font-family: 'Inter', 'San Francisco', 'Lucida Grande', Arial, sans-serif; 18 font-size: 14px; 19 position: relative; 20} 21#header { 22 display: flex; 23 gap: 10px; 24 align-items: center; 25 flex-wrap: wrap; 26} 27#displayname { 28 color: black; 29 font-weight: bold; 30 text-decoration: none; 31} 32#handle { 33 color: #666666; 34 font-size: .8em; 35 text-decoration: none; 36} 37#datetime { 38 color: #666666; 39 font-size: .8em; 40} 41#datetime:before { 42 content: """; 43 margin-right: 10px; 44} 45#status { 46 margin-top: 10px; 47} 48#link { 49 position: absolute; 50 bottom: 5px; 51 right: 5px; 52 font-size: .6em; 53 color: #666666; 54} 55`; 56 57class KibunStatus extends HTMLElement { 58 constructor() { 59 super(); 60 this.attachShadow({ mode: 'open' }); 61 } 62 63 connectedCallback() { 64 this.render(); 65 } 66 67 async render() { 68 const username = this.getAttribute('username'); 69 const hideKibun = this.hasAttribute('hide-kibun'); 70 const noStyles = this.hasAttribute('no-styles'); 71 72 const details = await this._retrieveStatus(username).catch(err => this._dispatchError(err)); 73 if (!details) { 74 return; 75 } 76 77 const { displayName, emoji, statusText, timeAgoText } = details 78 79 this.shadowRoot.innerHTML = ` 80 ${noStyles ? '' : `<style>${defaultStyles}</style>`} 81 <div id="container"> 82 <div id="header"> 83 <a id="displayname" href="https://www.kibun.social/users/${username}" target="_blank" rel="external">${displayName}</a> 84 <span id="emoji">${emoji}</span> 85 <a id="handle" href="https://www.kibun.social/users/${username}" target="_blank" rel="external">@${username}</a> 86 <span id="datetime">${timeAgoText}</span> 87 </div> 88 <div id="status">${statusText}</div> 89 ${hideKibun ? '' : '<a id="link" href="https://www.kibun.social/" target="_blank" rel="external">kibun.social</a>'} 90 </div> 91 `; 92 } 93 94 _dispatchError(error) { 95 this.dispatchEvent(new CustomEvent('error', { 96 detail: { error, message: 'Unable to retrieve Kibun status information'}, 97 bubbles: true, 98 composed: true, 99 })); 100 console.error(error); 101 } 102 103 async _retrieveStatus(username) { 104 if (!username || !/\./.test(username)) { 105 throw new Error('Please include at least a Kibun username: eg. <kibun-status username="kibun.social">'); 106 } 107 108 let userDidDoc; 109 try { 110 userDidDoc = await fetch(urls.identityResolveMiniDoc(username)).then(res => res.json()); 111 } catch(error) { 112 throw new Error('Unable to retrieve ATProto user data from Slingshot', error); 113 } 114 115 let userInfoData; 116 try { 117 userInfoData = await fetch(urls.repoGetRecord(userDidDoc)).then(res => res.json()); 118 } catch(error) { 119 throw new Error('Unable to retrieve user profile data from their PDS', error); 120 } 121 122 let statuses; 123 try { 124 statuses = await fetch(urls.repoListRecords(userDidDoc)).then(res => res.json()); 125 } catch (error) { 126 throw new Error('Unable to retrieve kibun records from user PDS', error); 127 } 128 129 if (statuses.records.length === 0) { 130 throw new Error(`'${username}' doesn't seem to use Kibun!`); 131 } 132 133 const status = statuses.records[0]; 134 return { 135 displayName: userInfoData.value.displayName, 136 emoji: status.value.emoji, 137 statusText: status.value.text, 138 timeAgoText: this._timeAgo(status.value.createdAt), 139 } 140 } 141 142 143 _timeAgo (dateString) { 144 const date = Date.parse(dateString); 145 const curDate = new Date(date); 146 const now = Date.now(); 147 const yest = new Date(Date.parse(dateString)); 148 const today = new Date(date); 149 yest.setDate(today - 1); 150 const diff = (now - date) / 1000; // difference in seconds 151 if (diff < 5) { 152 return "just now"; 153 } else if (diff < 60) { 154 return `${diff} seconds ago`; 155 } else if (diff < 60*60) { 156 const min = Math.floor(diff / 60); 157 return `${min} minute${min > 1 ? 's' : ''} ago`; 158 } else if (diff < 60*60*24) { 159 const hr = Math.floor(diff / (60*60)); 160 return `${hr} hour${hr > 1 ? 's' : ''} ago`; 161 } else if (date.getDate() === yest.getDate() && date.getMonth() === yest.getMonth() && date.getYear() === yest.getYear()) { 162 return "yesterday"; 163 } 164 return `${curDate.toLocaleDateString(undefined, { 165 weekday: 'short', 166 year: 'numeric', 167 month: 'short', 168 day: 'numeric' 169 }).toLowerCase()}`; 170 } 171} 172 173customElements.define('kibun-status', KibunStatus);