a proof of concept realtime collaborative text editor using atproto as a sync server jake.tngl.io/y-pds/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add more detail to dialog

+43 -12
+16 -7
app.js
··· 23 23 ); 24 24 if (res.ok) { 25 25 const p = await res.json(); 26 - return p.displayName ? `${p.displayName} (@${p.handle})` : `@${p.handle}`; 26 + return { 27 + displayName: p.displayName || null, 28 + handle: p.handle || did, 29 + avatar: p.avatar || null, 30 + }; 27 31 } 28 32 } catch {} 29 - return did; 33 + return { displayName: null, handle: did, avatar: null }; 30 34 } 31 35 32 36 /** @param {string} did */ ··· 232 236 this.provider = provider; 233 237 const dids = provider.getMembers(); 234 238 this.editors.value = await Promise.all( 235 - dids.map(async did => ({ did, label: await fetchProfile(did) })), 239 + dids.map(async did => ({ did, profile: await fetchProfile(did) })), 236 240 ); 237 241 this.dialogRef.current.showModal(); 238 242 } 239 243 240 244 async addMember(e) { 245 + const form = e.currentTarget; 241 246 e.preventDefault(); 242 247 const identifier = e.currentTarget.did.value.trim(); 243 248 if (!identifier) return; ··· 248 253 await this.provider.setMembers(updated); 249 254 this.editors.value = [ 250 255 ...this.editors.value, 251 - { did: newDid, label: await fetchProfile(newDid) }, 256 + { did: newDid, profile: await fetchProfile(newDid) }, 252 257 ]; 253 - e.currentTarget.reset(); 258 + form.reset(); 254 259 } 255 260 256 261 async removeMember(did) { ··· 283 288 </header> 284 289 <ul id="editors"> 285 290 ${this.editors.value.map( 286 - ({ did, label }) => html` 291 + ({ did, profile }) => html` 287 292 <li key=${did}> 288 - <span>${label}</span> 293 + ${profile.avatar && html`<img class="avatar" src=${profile.avatar} alt="" />`} 294 + <span class="member-info"> 295 + ${profile.displayName && html`<strong>${profile.displayName}</strong>`} 296 + <small>@${profile.handle}</small> 297 + </span> 289 298 <button type="button" onClick=${() => this.removeMember(did)}>Remove</button> 290 299 </li> 291 300 `,
+27 -5
style.css
··· 160 160 dialog { 161 161 overflow: visible; 162 162 place-self: center; 163 - min-width: 320px; 163 + width: 100%; 164 + max-width: 720px; 164 165 border: none; 165 166 border-radius: 10px; 166 167 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); ··· 214 215 gap: 0.5rem; 215 216 padding: 0.25rem 0; 216 217 217 - > span { 218 + .avatar { 219 + width: 32px; 220 + height: 32px; 221 + border-radius: 50%; 222 + } 223 + 224 + .member-info { 218 225 flex: 1; 219 - font-size: 0.875rem; 220 - font-family: monospace; 226 + display: flex; 227 + flex-direction: column; 228 + min-width: 0; 221 229 overflow: hidden; 222 - text-overflow: ellipsis; 230 + 231 + strong { 232 + font-size: 0.875rem; 233 + text-overflow: ellipsis; 234 + overflow: hidden; 235 + white-space: nowrap; 236 + } 237 + 238 + small { 239 + font-size: 0.75rem; 240 + color: #888; 241 + text-overflow: ellipsis; 242 + overflow: hidden; 243 + white-space: nowrap; 244 + } 223 245 } 224 246 } 225 247 }