···
1
1
+
{
2
2
+
"arrowParens": "avoid",
3
3
+
"printWidth": 100
4
4
+
}
···
9
9
import { EditorView } from "prosemirror-view";
10
10
import { exampleSetup } from "prosemirror-example-setup";
11
11
import { configure, client, resolve, scope } from "./oauth.js";
12
12
-
import { YPdsProvider, newDocUri } from "./y-pds.js";
12
12
+
import { YPdsProvider } from "./y-pds.js";
13
13
import "actor-typeahead";
14
14
15
15
configure();
16
16
17
17
export class App extends Component {
18
18
-
did = signal(undefined);
19
19
-
atUri = signal(null);
18
18
+
loading = signal(false);
19
19
+
did = signal("");
20
20
+
atUri = signal("");
20
21
21
22
async componentDidMount() {
22
23
try {
23
23
-
const storedDid = localStorage.getItem("ypds:did");
24
24
-
const session = await getSession(storedDid, { allowStale: false });
24
24
+
const did = localStorage.getItem("ypds:did");
25
25
+
if (!did) return;
26
26
+
27
27
+
this.loading.value = true;
28
28
+
29
29
+
const session = await getSession(did, { allowStale: false });
25
30
this.did.value = session.info.sub;
31
31
+
26
32
const params = new URLSearchParams(location.search);
27
27
-
if (!params.has("id")) {
28
28
-
params.set("id", newDocUri(this.did.value));
33
33
+
let uri = params.get("id") ?? "";
34
34
+
if (!uri) {
35
35
+
uri = `at://${this.did.value}/${DOC_COLLECTION}/${crypto.randomUUID()}`;
36
36
+
params.set("id", uri);
29
37
history.replaceState(null, "", "?" + params);
30
38
}
31
31
-
this.atUri.value = params.get("id");
32
32
-
} catch {
33
33
-
this.did.value = null;
39
39
+
40
40
+
this.atUri.value = uri;
41
41
+
} catch (e) {
42
42
+
console.error(e);
43
43
+
this.did.value = "";
44
44
+
} finally {
45
45
+
this.loading.value = false;
34
46
}
35
47
}
36
48
37
49
render() {
38
38
-
if (this.did.value === undefined) return html`<p>loading…</p>`;
50
50
+
if (this.loading.value) return html`<p>loading…</p>`;
39
51
if (!this.did.value) return html`<${Login} />`;
40
40
-
if (!this.atUri.value) return null;
41
52
return html`<${Editor} did=${this.did.value} atUri=${this.atUri.value} />`;
42
53
}
43
54
}
···
63
74
<input name="handle" />
64
75
</actor-typeahead>
65
76
<button>log in</button>
77
77
+
<p>Log in with your <a href="https://internethandle.org">Internet handle</a>.</p>
66
78
</form>
67
79
`;
68
80
}
···
113
125
render() {
114
126
return html`
115
127
<div id="editor" ref=${this.editorRef}></div>
116
116
-
<button id="share" onClick=${() => this.shareDialogRef.current.open(this.provider)}>Share</button>
128
128
+
<button id="share" onClick=${() => this.shareDialogRef.current.open(this.provider)}>
129
129
+
Share
130
130
+
</button>
117
131
<${ShareDialog} ref=${this.shareDialogRef} />
118
132
`;
119
133
}
···
9
9
body {
10
10
height: 100%;
11
11
}
12
12
+
13
13
+
html {
14
14
+
font-family:
15
15
+
system-ui,
16
16
+
-apple-system,
17
17
+
BlinkMacSystemFont,
18
18
+
"Segoe UI",
19
19
+
Roboto,
20
20
+
Oxygen,
21
21
+
Ubuntu,
22
22
+
Cantarell,
23
23
+
"Open Sans",
24
24
+
"Helvetica Neue",
25
25
+
sans-serif;
26
26
+
}
27
27
+
12
28
body {
13
29
display: flex;
14
30
flex-direction: column;
15
31
}
32
32
+
16
33
#app {
17
34
flex: 1;
18
35
display: flex;
19
36
flex-direction: column;
20
37
}
38
38
+
21
39
#editor {
22
40
flex: 1;
23
41
overflow-y: auto;
24
42
padding: 0;
25
25
-
font-family: Georgia, serif;
43
43
+
font-family: ui-serif, Georgia, serif;
26
44
font-size: 1rem;
27
45
line-height: 1.6;
28
46
}
47
47
+
29
48
.ProseMirror {
30
49
min-height: 100%;
31
50
outline: none;
32
32
-
}
33
33
-
.ProseMirror p {
34
34
-
margin-bottom: 1em;
51
51
+
padding: 1rem;
52
52
+
margin-trim: block;
53
53
+
54
54
+
> :first-child,
55
55
+
> :first-child p {
56
56
+
margin-top: 0;
57
57
+
}
58
58
+
59
59
+
> :last-child,
60
60
+
> :last-child p {
61
61
+
margin-bottom: 0;
62
62
+
}
35
63
}
64
64
+
36
65
.ProseMirror-menubar-wrapper {
37
66
height: 100%;
38
67
}
···
7
7
const url = did.startsWith("did:web:")
8
8
? `https://${did.slice("did:web:".length)}/.well-known/did.json`
9
9
: `https://plc.directory/${did}`;
10
10
-
const doc = await fetch(url).then(r => r.json());
11
11
-
const pds = doc.service?.find(s => s.type === "AtprotoPersonalDataServer")?.serviceEndpoint;
10
10
+
const doc = await fetch(url).then((r) => r.json());
11
11
+
const pds = doc.service?.find((s) => s.type === "AtprotoPersonalDataServer")?.serviceEndpoint;
12
12
if (!pds) throw new Error(`no PDS found for ${did}`);
13
13
return new Client({ handler: simpleFetchHandler({ service: pds }) });
14
14
}
···
18
18
const CURSOR_COLLECTION = "com.jakelazaroff.ypds.awareness";
19
19
20
20
const encode = (/** @type {Uint8Array} */ update) => btoa(String.fromCharCode(...update));
21
21
-
const decode = (/** @type {string} */ b64) => Uint8Array.from(atob(b64), c => c.charCodeAt(0));
22
22
-
23
23
-
/** @param {string} did */
24
24
-
export function newDocUri(did) {
25
25
-
return `at://${did}/${DOC_COLLECTION}/${crypto.randomUUID()}`;
26
26
-
}
21
21
+
const decode = (/** @type {string} */ b64) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
27
22
28
23
/** @param {string} did */
29
24
function colorForDid(did) {
···
68
63
const doc = await this.#ensureDocument();
69
64
const repos = [this.#ownerDid, ...doc.members];
70
65
71
71
-
const perRepo = await Promise.all(repos.map(repo => this.#fetchUpdates(repo)));
66
66
+
const perRepo = await Promise.all(repos.map((repo) => this.#fetchUpdates(repo)));
72
67
const updates = perRepo
73
68
.flat()
74
69
.sort((a, b) => a.value.createdAt.localeCompare(b.value.createdAt));
···
111
106
cursor = res.data.cursor;
112
107
} while (cursor);
113
108
114
114
-
return records.filter(r => r.value.docId === this.#rkey);
109
109
+
return records.filter((r) => r.value.docId === this.#rkey);
115
110
}
116
111
117
112
/** @param {Uint8Array} update @param {unknown} origin */
···
209
204
for (const repo of repos) url.searchParams.append("wantedDids", repo);
210
205
211
206
this.#ws = new WebSocket(url);
212
212
-
this.#ws.onmessage = async e => {
207
207
+
this.#ws.onmessage = async (e) => {
213
208
const event = JSON.parse(e.data);
214
209
if (event.kind !== "commit") return;
215
210
if (!repos.has(event.did)) return;