const urls = { identityResolveMiniDoc: (username) => `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${username}`, repoGetRecord: (userDidDoc) => `${userDidDoc.pds}/xrpc/com.atproto.repo.getRecord?repo=${userDidDoc.did}&collection=app.bsky.actor.profile&rkey=self`, repoListRecords: (userDidDoc) => `${userDidDoc.pds}/xrpc/com.atproto.repo.listRecords?repo=${userDidDoc.did}&collection=social.kibun.status&limit=1`, } const defaultStyles = ` :host { display: block; } #container { border: 1px #7dd3fc solid; box-shadow: 4px 4px 0 #7dd3fc; padding: 20px; max-width: 400px; background-color: #FFFFFF; font-family: 'Inter', 'San Francisco', 'Lucida Grande', Arial, sans-serif; font-size: 14px; position: relative; } #header { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } #displayname { color: black; font-weight: bold; text-decoration: none; } #handle { color: #666666; font-size: .8em; text-decoration: none; } #datetime { color: #666666; font-size: .8em; } #datetime:before { content: """; margin-right: 10px; } #status { margin-top: 10px; } #link { position: absolute; bottom: 5px; right: 5px; font-size: .6em; color: #666666; } `; class KibunStatus extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.render(); } async render() { const username = this.getAttribute('username'); const hideKibun = this.hasAttribute('hide-kibun'); const noStyles = this.hasAttribute('no-styles'); const details = await this._retrieveStatus(username).catch(err => this._dispatchError(err)); if (!details) { return; } const { displayName, emoji, statusText, timeAgoText } = details this.shadowRoot.innerHTML = ` ${noStyles ? '' : ``}
${statusText}
${hideKibun ? '' : 'kibun.social'}
`; } _dispatchError(error) { this.dispatchEvent(new CustomEvent('error', { detail: { error, message: 'Unable to retrieve Kibun status information'}, bubbles: true, composed: true, })); console.error(error); } async _retrieveStatus(username) { if (!username || !/\./.test(username)) { throw new Error('Please include at least a Kibun username: eg. '); } let userDidDoc; try { userDidDoc = await fetch(urls.identityResolveMiniDoc(username)).then(res => res.json()); } catch(error) { throw new Error('Unable to retrieve ATProto user data from Slingshot', error); } let userInfoData; try { userInfoData = await fetch(urls.repoGetRecord(userDidDoc)).then(res => res.json()); } catch(error) { throw new Error('Unable to retrieve user profile data from their PDS', error); } let statuses; try { statuses = await fetch(urls.repoListRecords(userDidDoc)).then(res => res.json()); } catch (error) { throw new Error('Unable to retrieve kibun records from user PDS', error); } if (statuses.records.length === 0) { throw new Error(`'${username}' doesn't seem to use Kibun!`); } const status = statuses.records[0]; return { displayName: userInfoData.value.displayName, emoji: status.value.emoji, statusText: status.value.text, timeAgoText: this._timeAgo(status.value.createdAt), } } _timeAgo (dateString) { const date = Date.parse(dateString); const curDate = new Date(date); const now = Date.now(); const yest = new Date(Date.parse(dateString)); const today = new Date(date); yest.setDate(today - 1); const diff = (now - date) / 1000; // difference in seconds if (diff < 5) { return "just now"; } else if (diff < 60) { return `${diff} seconds ago`; } else if (diff < 60*60) { const min = Math.floor(diff / 60); return `${min} minute${min > 1 ? 's' : ''} ago`; } else if (diff < 60*60*24) { const hr = Math.floor(diff / (60*60)); return `${hr} hour${hr > 1 ? 's' : ''} ago`; } else if (date.getDate() === yest.getDate() && date.getMonth() === yest.getMonth() && date.getYear() === yest.getYear()) { return "yesterday"; } return `${curDate.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }).toLowerCase()}`; } } customElements.define('kibun-status', KibunStatus);