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 editor warning

+36
+14
app.js
··· 135 135 editorRef = createRef(); 136 136 shareDialogRef = createRef(); 137 137 provider = signal(null); 138 + canEdit = signal(true); 138 139 view = null; 139 140 140 141 async componentDidMount() { ··· 200 201 }); 201 202 202 203 this.view = new EditorView(this.editorRef.current, { state }); 204 + 205 + this.provider.value.onMembersChange = editors => { 206 + console.log("MEMBERS CHANGE", editors); 207 + const canEdit = isOwner || editors.includes(this.props.did); 208 + this.canEdit.value = canEdit; 209 + this.view?.setProps({ editable: () => canEdit }); 210 + }; 211 + 203 212 await this.provider.value.load(); 204 213 205 214 const color = colorForDid(this.props.did); ··· 224 233 render() { 225 234 return html` 226 235 <div id="editor" ref=${this.editorRef}></div> 236 + ${!this.canEdit.value && 237 + html`<div class="readonly-banner"> 238 + You're viewing this document in read-only mode. Ask the owner to click the "Share" button to 239 + add you as an editor. 240 + </div>`} 227 241 <${ShareDialog} 228 242 ref=${this.shareDialogRef} 229 243 atUri=${this.props.atUri}
+17
style.css
··· 282 282 align-items: flex-end; 283 283 } 284 284 285 + .readonly-banner { 286 + position: fixed; 287 + bottom: 1rem; 288 + left: 50%; 289 + transform: translateX(-50%); 290 + background: #f8f8f8; 291 + border: 1px solid #ddd; 292 + border-radius: 8px; 293 + padding: 0.5rem 1rem; 294 + font-size: 0.8rem; 295 + color: #666; 296 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); 297 + z-index: 10; 298 + white-space: nowrap; 299 + animation: 0.3s fade-in; 300 + } 301 + 285 302 .collab-cursor { 286 303 position: relative; 287 304 border-left: 2px solid var(--color);
+5
y-pds.js
··· 115 115 * @param {Awareness} [options.awareness] 116 116 * @param {string} [options.jetstream] 117 117 */ 118 + /** @type {((editors: string[]) => void) | null} */ 119 + onMembersChange = null; 120 + 118 121 constructor({ ydoc, client, atUri, did, awareness, jetstream }) { 119 122 this.#clients.set(did, client); 120 123 this.#ydoc = ydoc; ··· 138 141 rkey: this.#rkey, 139 142 }); 140 143 this.#doc = data.value; 144 + this.onMembersChange?.(this.#doc.editors); 141 145 } catch { 142 146 // if the fetch fails and the document is not in our own repo, 143 147 // throw an error rather than attempting to create it ··· 151 155 rkey: this.#rkey, 152 156 record: this.#doc, 153 157 }); 158 + this.onMembersChange?.(this.#doc.editors); 154 159 } 155 160 156 161 // fetch updates from the owner and all editors