kibun-status.js
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);