···
1
1
import { Component, createRef } from "preact";
2
2
import { html } from "htm/preact";
3
3
import { signal } from "@preact/signals";
4
4
-
import { getSession, logIn, logOut, resolveDID } from "./atsw.js";
4
4
+
import { getSession, listSessions, logIn, logOut, resolveDID } from "./atsw.js";
5
5
import metadata from "./client-metadata.json" with { type: "json" };
6
6
import * as Y from "yjs";
7
7
import { ySyncPlugin, yUndoPlugin, yCursorPlugin } from "y-prosemirror";
···
126
126
127
127
async componentDidMount() {
128
128
try {
129
129
-
const did = localStorage.getItem("ypds:did");
130
130
-
if (!did) return;
129
129
+
let did = sessionStorage.getItem("ypds:did");
130
130
+
if (!did) {
131
131
+
const sessions = await listSessions();
132
132
+
if (!sessions.length) return;
133
133
+
did = sessions[0].did;
134
134
+
sessionStorage.setItem("ypds:did", did);
135
135
+
}
131
136
132
137
this.loading.value = true;
133
138
···
139
144
this.did.value = session.did;
140
145
141
146
const params = new URLSearchParams(location.search);
142
142
-
let uri = params.get("id") ?? localStorage.getItem("ypds:pending-id") ?? "";
143
143
-
localStorage.removeItem("ypds:pending-id");
147
147
+
let uri = params.get("id") ?? sessionStorage.getItem("ypds:pending-id") ?? "";
148
148
+
sessionStorage.removeItem("ypds:pending-id");
144
149
if (!uri) {
145
150
uri = `at://${this.did.value}/${DOC_COLLECTION}/${genTid()}`;
146
151
}
···
178
183
const identifier = data.get("handle");
179
184
if (typeof identifier !== "string") throw new Error("invalid handle");
180
185
const id = new URLSearchParams(location.search).get("id");
181
181
-
if (id) localStorage.setItem("ypds:pending-id", id);
186
186
+
if (id) sessionStorage.setItem("ypds:pending-id", id);
182
187
const did = await resolveDID(identifier);
183
183
-
localStorage.setItem("ypds:did", did);
188
188
+
sessionStorage.setItem("ypds:did", did);
184
189
await logIn(
185
190
{
186
191
clientId: metadata.client_id,
···
219
224
class Editor extends Component {
220
225
editorRef = createRef();
221
226
shareDialogRef = createRef();
227
227
+
accountsDialogRef = createRef();
222
228
provider = signal(null);
223
229
canEdit = signal(true);
224
230
showDevtools = signal(false);
···
269
275
});
270
276
wrap.appendChild(dbBtn);
271
277
272
272
-
const logOutBtn = document.createElement("button");
273
273
-
logOutBtn.textContent = "Log out";
274
274
-
logOutBtn.addEventListener("click", async () => {
275
275
-
await logOut(this.props.did);
276
276
-
localStorage.removeItem("ypds:did");
277
277
-
location.reload();
278
278
+
const accountBtn = document.createElement("button");
279
279
+
accountBtn.textContent = this.props.did;
280
280
+
accountBtn.addEventListener("click", () => {
281
281
+
this.accountsDialogRef.current.open();
278
282
});
279
279
-
wrap.appendChild(logOutBtn);
283
283
+
wrap.appendChild(accountBtn);
284
284
+
this._accountBtn = accountBtn;
280
285
281
286
return wrap;
282
287
},
···
332
337
if (res.ok) {
333
338
const profile = await res.json();
334
339
name = profile.displayName || profile.handle || this.props.did;
340
340
+
if (this._accountBtn && profile.handle) this._accountBtn.textContent = `@${profile.handle}`;
335
341
}
336
342
} catch {}
337
343
this.provider.value.awareness.setLocalState({ user: { name, color } });
···
373
379
You're viewing this document in read-only mode. Ask the owner to click the "Share" button to
374
380
add you as an editor.
375
381
</div>`}
382
382
+
<${AccountsPopover}
383
383
+
ref=${this.accountsDialogRef}
384
384
+
did=${this.props.did}
385
385
+
/>
376
386
<${ShareDialog}
377
387
ref=${this.shareDialogRef}
378
388
atUri=${this.props.atUri}
···
714
724
${tab === "updates" ? this.renderTimeline()
715
725
: tab === "awareness" ? this.renderAwareness()
716
726
: this.renderDoc()}
727
727
+
</div>
728
728
+
</div>
729
729
+
`;
730
730
+
}
731
731
+
}
732
732
+
733
733
+
class AccountsPopover extends Component {
734
734
+
dialogRef = createRef();
735
735
+
sessions = signal([]);
736
736
+
profiles = signal({});
737
737
+
738
738
+
async open() {
739
739
+
const sessions = await listSessions();
740
740
+
this.sessions.value = sessions;
741
741
+
const map = { ...this.profiles.value };
742
742
+
await Promise.all(
743
743
+
sessions.filter(s => !map[s.did]).map(async s => {
744
744
+
map[s.did] = await fetchProfile(s.did);
745
745
+
}),
746
746
+
);
747
747
+
this.profiles.value = map;
748
748
+
this.dialogRef.current.showPopover();
749
749
+
}
750
750
+
751
751
+
handleSwitch(did) {
752
752
+
sessionStorage.setItem("ypds:did", did);
753
753
+
location.reload();
754
754
+
}
755
755
+
756
756
+
async handleLogOut(did) {
757
757
+
await logOut(did);
758
758
+
if (did === this.props.did) {
759
759
+
sessionStorage.removeItem("ypds:did");
760
760
+
location.reload();
761
761
+
return;
762
762
+
}
763
763
+
this.sessions.value = this.sessions.value.filter(s => s.did !== did);
764
764
+
}
765
765
+
766
766
+
async handleAddAccount(e) {
767
767
+
e.preventDefault();
768
768
+
const data = new FormData(e.currentTarget);
769
769
+
const identifier = data.get("handle");
770
770
+
if (typeof identifier !== "string" || !identifier.trim()) return;
771
771
+
const id = new URLSearchParams(location.search).get("id");
772
772
+
if (id) sessionStorage.setItem("ypds:pending-id", id);
773
773
+
await logIn(
774
774
+
{ clientId: metadata.client_id, redirectUri: metadata.redirect_uris[0], scope: metadata.scope },
775
775
+
identifier.trim(),
776
776
+
);
777
777
+
}
778
778
+
779
779
+
render() {
780
780
+
const activeDid = this.props.did;
781
781
+
const sessions = [...this.sessions.value].sort((a, b) => {
782
782
+
if (a.did === activeDid) return -1;
783
783
+
if (b.did === activeDid) return 1;
784
784
+
return 0;
785
785
+
});
786
786
+
const profiles = this.profiles.value;
787
787
+
788
788
+
return html`
789
789
+
<div ref=${this.dialogRef} popover class="accounts-popover">
790
790
+
<header>
791
791
+
<h2>Accounts</h2>
792
792
+
<button
793
793
+
class="icon-button"
794
794
+
aria-label="Close"
795
795
+
onClick=${() => this.dialogRef.current.hidePopover()}
796
796
+
>
797
797
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
798
798
+
<line x1="4" y1="4" x2="12" y2="12" />
799
799
+
<line x1="12" y1="4" x2="4" y2="12" />
800
800
+
</svg>
801
801
+
</button>
802
802
+
</header>
803
803
+
<ul class="accounts-list">
804
804
+
${sessions.map(session => {
805
805
+
const profile = profiles[session.did] ?? {};
806
806
+
const isActive = session.did === activeDid;
807
807
+
return html`
808
808
+
<li key=${session.did} class=${isActive ? "active" : ""}>
809
809
+
<button
810
810
+
class="account-row"
811
811
+
onClick=${() => { if (!isActive) this.handleSwitch(session.did); }}
812
812
+
>
813
813
+
${profile.avatar
814
814
+
? html`<img class="avatar" width="32" height="32" src=${profile.avatar} alt="" />`
815
815
+
: html`<div class="avatar avatar-placeholder"></div>`}
816
816
+
<span class="member-info">
817
817
+
${profile.displayName && html`<strong>${profile.displayName}</strong>`}
818
818
+
<small>@${profile.handle || session.did}</small>
819
819
+
</span>
820
820
+
</button>
821
821
+
<button type="button" onClick=${() => this.handleLogOut(session.did)}>
822
822
+
Log out
823
823
+
</button>
824
824
+
</li>
825
825
+
`;
826
826
+
})}
827
827
+
</ul>
828
828
+
<div class="accounts-footer">
829
829
+
<form onSubmit=${e => this.handleAddAccount(e)}>
830
830
+
<label>
831
831
+
<span>Add another account</span>
832
832
+
<actor-typeahead>
833
833
+
<input name="handle" placeholder="example.bsky.social" autocomplete="off" />
834
834
+
</actor-typeahead>
835
835
+
</label>
836
836
+
<button type="submit">Log in</button>
837
837
+
</form>
717
838
</div>
718
839
</div>
719
840
`;
···
34
34
flex: 1;
35
35
}
36
36
37
37
-
button,
38
38
-
.ProseMirror-menuitem .share-menu-button {
37
37
+
button {
39
38
padding: 0.4rem 0.75rem;
40
39
border: 1px solid #ccc;
41
40
border-radius: 4px;
···
297
296
align-items: flex-end;
298
297
}
299
298
299
299
+
.accounts-popover {
300
300
+
position: fixed;
301
301
+
inset: auto;
302
302
+
top: 2.5rem;
303
303
+
right: 0.5rem;
304
304
+
width: 280px;
305
305
+
padding: 0;
306
306
+
margin: 0;
307
307
+
border: 1px solid #ddd;
308
308
+
border-radius: 10px;
309
309
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
310
310
+
background: #fff;
311
311
+
312
312
+
&:popover-open {
313
313
+
animation: popover-in 0.1s ease;
314
314
+
}
315
315
+
316
316
+
header {
317
317
+
display: flex;
318
318
+
justify-content: space-between;
319
319
+
align-items: center;
320
320
+
padding: 10px 12px;
321
321
+
border-bottom: 1px solid #ddd;
322
322
+
}
323
323
+
324
324
+
h2 {
325
325
+
font-size: 0.875rem;
326
326
+
font-weight: 600;
327
327
+
margin: 0;
328
328
+
}
329
329
+
}
330
330
+
331
331
+
@keyframes popover-in {
332
332
+
from {
333
333
+
opacity: 0;
334
334
+
transform: translateY(-4px);
335
335
+
}
336
336
+
to {
337
337
+
opacity: 1;
338
338
+
transform: translateY(0);
339
339
+
}
340
340
+
}
341
341
+
342
342
+
.accounts-list {
343
343
+
list-style: none;
344
344
+
margin: 0;
345
345
+
padding: 8px 0;
346
346
+
display: flex;
347
347
+
flex-direction: column;
348
348
+
349
349
+
> li {
350
350
+
display: flex;
351
351
+
align-items: center;
352
352
+
gap: 4px;
353
353
+
padding: 0 8px;
354
354
+
355
355
+
&:not(.active):hover {
356
356
+
background: #f0f0f0;
357
357
+
}
358
358
+
359
359
+
&.active {
360
360
+
background: #e8f0fe;
361
361
+
362
362
+
.account-row {
363
363
+
cursor: default;
364
364
+
}
365
365
+
}
366
366
+
}
367
367
+
}
368
368
+
369
369
+
.account-row {
370
370
+
all: unset;
371
371
+
flex: 1;
372
372
+
display: flex;
373
373
+
align-items: center;
374
374
+
gap: 0.5rem;
375
375
+
padding: 0.4rem 0;
376
376
+
cursor: pointer;
377
377
+
min-width: 0;
378
378
+
379
379
+
&:hover {
380
380
+
background: transparent;
381
381
+
}
382
382
+
383
383
+
.avatar {
384
384
+
border-radius: 50%;
385
385
+
flex-shrink: 0;
386
386
+
}
387
387
+
388
388
+
.avatar-placeholder {
389
389
+
width: 32px;
390
390
+
height: 32px;
391
391
+
border-radius: 50%;
392
392
+
background: #e0e0e0;
393
393
+
flex-shrink: 0;
394
394
+
}
395
395
+
396
396
+
.member-info {
397
397
+
flex: 1;
398
398
+
display: flex;
399
399
+
flex-direction: column;
400
400
+
min-width: 0;
401
401
+
overflow: hidden;
402
402
+
403
403
+
strong {
404
404
+
font-size: 0.875rem;
405
405
+
text-overflow: ellipsis;
406
406
+
overflow: hidden;
407
407
+
white-space: nowrap;
408
408
+
}
409
409
+
410
410
+
small {
411
411
+
font-size: 0.75rem;
412
412
+
color: #888;
413
413
+
text-overflow: ellipsis;
414
414
+
overflow: hidden;
415
415
+
white-space: nowrap;
416
416
+
}
417
417
+
}
418
418
+
}
419
419
+
420
420
+
.accounts-footer {
421
421
+
border-top: 1px solid #ddd;
422
422
+
padding: 10px 12px;
423
423
+
424
424
+
form {
425
425
+
display: flex;
426
426
+
gap: 0.5rem;
427
427
+
align-items: flex-end;
428
428
+
}
429
429
+
}
430
430
+
300
431
.readonly-banner {
301
432
position: fixed;
302
433
bottom: 1rem;
···
353
484
flex-direction: column;
354
485
border-top: 1px solid #ddd;
355
486
background: #fff;
356
356
-
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
357
357
-
"DejaVu Sans Mono", monospace;
487
487
+
font-family:
488
488
+
ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;
358
489
font-size: 12px;
359
490
}
360
491
···
443
574
border-bottom: 2px solid transparent;
444
575
white-space: nowrap;
445
576
446
446
-
&:hover { color: #333; }
447
447
-
&.active { color: #1a73e8; border-bottom-color: #1a73e8; }
577
577
+
&:hover {
578
578
+
color: #333;
579
579
+
}
580
580
+
&.active {
581
581
+
color: #1a73e8;
582
582
+
border-bottom-color: #1a73e8;
583
583
+
}
448
584
}
449
585
}
450
586