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 ? '' : ``}
`;
}
_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);