Skip to content

Commit d47f43a

Browse files
committed
Ring joining/inviting works????????
1 parent 63db87d commit d47f43a

File tree

18 files changed

+429
-150
lines changed

18 files changed

+429
-150
lines changed

env.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/// <reference types="vite/client" />
2+
/// <reference types="unplugin-vue-router/client" />

eslint.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const rules = [
1010
rules: {
1111
'@typescript-eslint/no-unused-vars': 'warn',
1212
'@typescript-eslint/no-explicit-any': 'off',
13+
'vue/multi-word-component-names': 'off',
1314
'semi': ['warn', 'always']
1415
},
1516
},

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@atproto/syntax": "^0.3.1",
2929
"@mdx-js/mdx": "^3.1.0",
3030
"@noble/hashes": "^1.6.1",
31+
"@vue/compiler-sfc": "^3.5.13",
3132
"@vue/reactivity": "3.5.13",
3233
"@vueuse/core": "^12.0.0",
3334
"bluesky-post-embed": "^1.0.5",
@@ -62,16 +63,18 @@
6263
"@vitejs/plugin-vue-jsx": "^4.1.1",
6364
"@vue/eslint-config-prettier": "^10.1.0",
6465
"@vue/eslint-config-typescript": "^14.1.4",
66+
"@vue/runtime-dom": "^3.5.13",
6567
"@vue/tsconfig": "^0.7.0",
6668
"eslint": "^9.17.0",
6769
"eslint-plugin-vue": "^9.32.0",
6870
"material-design-icons-iconfont": "^6.7.0",
6971
"monaco-editor": "^0.52.2",
70-
"npm-run-all2": "^7.0.1",
72+
"npm-run-all2": "^7.0.2",
7173
"prettier": "^3.4.2",
7274
"sass": "^1.83.0",
7375
"sass-embedded": "^1.83.0",
7476
"typescript": "~5.7.2",
77+
"unplugin-vue-router": "^0.10.9",
7578
"vite": "^6.0.3",
7679
"vite-plugin-node-polyfills": "^0.22.0",
7780
"vite-plugin-vue-devtools": "^7.6.8",

pnpm-lock.yaml

+139-30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/atproto/atweb-client.ts

+34-4
Original file line numberDiff line numberDiff line change
@@ -101,27 +101,57 @@ export class AtwebClient {
101101
return { ...result, uri: new AtUri(result.uri) };
102102
}
103103

104-
async updateRing(
104+
async updateRingMembers(
105105
rkey: string,
106-
ring: IoGithubAtwebRing.Record,
106+
memberships: (string | AtUri | { membership: AtUri; })[],
107107
swapRecord?: string,
108108
) {
109+
const { value } = await this.agent.get({
110+
collection: 'io.github.atweb.ring',
111+
repo: this.user.did,
112+
rkey,
113+
cid: swapRecord,
114+
});
115+
109116
await this.agent.put({
110117
collection: 'io.github.atweb.ring',
111118
repo: this.user.did,
112119
rkey,
113-
record: ring,
120+
record: {
121+
...value,
122+
members: memberships.map(e => ({
123+
membership: typeof e !== 'string' && 'membership' in e
124+
? e.membership.toString()
125+
: e.toString()
126+
}))
127+
},
114128
swapRecord
115129
});
116130
}
117131

118132
async deleteRing(
119-
rkey: string
133+
rkey: string,
134+
swapRecord?: string,
120135
) {
121136
await this.agent.delete({
122137
collection: 'io.github.atweb.ring',
123138
repo: this.user.did,
124139
rkey,
140+
swapRecord,
141+
});
142+
}
143+
144+
async acceptInvite(inviterDid: string, rkey: string, mainPage: string) {
145+
await this.agent.put({
146+
collection: 'io.github.atweb.ringMembership',
147+
repo: this.user.did,
148+
rkey: rkey,
149+
record: {
150+
$type: 'io.github.atweb.ringMembership',
151+
createdAt: new Date().toISOString(),
152+
ring: AtUri.make(inviterDid, 'io.github.atweb.ring', rkey).toString(),
153+
mainPage: AtUri.make(this.user.did, 'io.github.atweb.page', filepathToRkey(mainPage)).toString(),
154+
}
125155
});
126156
}
127157
}

src/lib/atproto/atweb-unauthed.ts

+52-32
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { parseAtUri } from "../utils";
55
import { getDidAndPds } from "./pds-helpers";
66
import type { Awaitable } from "@vueuse/core";
77
import { AtUri } from "@atproto/syntax";
8+
import { resolveHandleAnonymously } from "./handles/resolve";
89

910
const unauthedAgent = new KittyAgent({ handler: simpleFetchHandler({ service: 'https://bsky.social' }) });
1011

@@ -186,56 +187,50 @@ export async function downloadFile(did: At.DID, rkey: string): Promise<Page> {
186187
};
187188
}
188189

189-
export async function getManagedRings(didOrHandle: string) {
190-
const { records } = await unauthedAgent.list({
191-
collection: 'io.github.atweb.ring',
192-
repo: didOrHandle,
193-
});
194-
195-
return records.map(record => ({
196-
...record.value,
197-
uri: new AtUri(record.uri),
198-
}));
199-
}
200-
201190
export async function getRingsUserIsAMemberOf(didOrHandle: string) {
202191
const { records } = await unauthedAgent.list({
203192
collection: 'io.github.atweb.ringMembership',
204193
repo: didOrHandle,
205194
});
206195

207-
const rings: Array<IoGithubAtwebRing.Record & { uri: AtUri }> = [];
196+
const rings: Array<Awaited<ReturnType<typeof getRing>>> = [];
208197
for (const record of records) {
209198
const uri = new AtUri(record.value.ring);
210-
if (uri.host === didOrHandle) continue;
211199

212-
const entry = await unauthedAgent.tryGet({
213-
collection: 'io.github.atweb.ring',
214-
repo: uri.host,
215-
rkey: uri.rkey,
216-
});
200+
const entry = await tryGetRing(uri.host, uri.rkey);
217201

218-
if (entry.value) {
219-
rings.push({
220-
...entry.value,
221-
uri: new AtUri(entry.uri),
222-
});
202+
if (entry) {
203+
rings.push(entry);
223204
}
224205
}
225206

226207
return rings;
227208
}
228209

229-
export async function getMemberRings(didOrHandle: string) {
230-
const { records } = await unauthedAgent.list({
210+
async function arrayFromAsync<T>(generator: AsyncIterable<T>): Promise<T[]> {
211+
const arr: T[] = [];
212+
for await (const e of generator) {
213+
arr.push(e);
214+
}
215+
return arr;
216+
}
217+
218+
export async function tryGetRing(repo: string, rkey: string) {
219+
const result = await unauthedAgent.tryGet({
231220
collection: 'io.github.atweb.ring',
232-
repo: didOrHandle,
221+
repo,
222+
rkey,
233223
});
234224

235-
return records.map(record => ({
236-
...record.value,
237-
uri: new AtUri(record.uri),
238-
}));
225+
if (!result.value) return undefined;
226+
227+
return {
228+
name: result.value.name,
229+
createdAt: result.value.createdAt,
230+
members: await arrayFromAsync(getMembershipStatuses(repo, result.value.members ?? [])),
231+
cid: result.cid,
232+
uri: new AtUri(result.uri),
233+
};
239234
}
240235

241236
export async function getRing(repo: string, rkey: string) {
@@ -244,9 +239,34 @@ export async function getRing(repo: string, rkey: string) {
244239
repo,
245240
rkey,
246241
});
242+
247243
return {
248-
...result.value,
244+
name: result.value.name,
245+
createdAt: result.value.createdAt,
246+
members: await arrayFromAsync(getMembershipStatuses(repo, result.value.members ?? [])),
249247
cid: result.cid,
250248
uri: new AtUri(result.uri),
251249
};
252250
}
251+
252+
async function *getMembershipStatuses(owner: string, members: IoGithubAtwebRing.Member[]) {
253+
owner = await resolveHandleAnonymously(owner);
254+
255+
for (const member of members) {
256+
const uri = new AtUri(member.membership);
257+
const result = await unauthedAgent.tryGet({
258+
collection: 'io.github.atweb.ringMembership',
259+
repo: uri.host,
260+
rkey: uri.rkey,
261+
});
262+
const isMember = !!result.value;
263+
const isOwner = isMember && uri.host === owner;
264+
265+
yield {
266+
membership: uri,
267+
isMember,
268+
isOwner,
269+
};
270+
}
271+
}
272+

src/main.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import './assets/main.scss';
22

33
import 'bluesky-profile-card-embed';
4-
54
// import './custom-elements.ts';
65

76
import { createApp } from 'vue';
File renamed without changes.
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
import SignInGate from '@/components/SignInGate.vue';
3+
import { getRing } from '@/lib/atproto/atweb-unauthed';
4+
import { getDidAndPds } from '@/lib/atproto/pds-helpers';
5+
import { user } from '@/lib/atproto/signed-in-user';
6+
import { ref } from 'vue';
7+
import { useRoute } from 'vue-router';
8+
9+
const route = useRoute('/invited/[inviterDid]/[ringAndInviteRkey]/');
10+
11+
const { didDocument } = await getDidAndPds(route.params.inviterDid);
12+
const inviterHandle = didDocument.alsoKnownAs?.[0].replace('at://', '') ?? route.params.inviterDid;
13+
14+
const mainPage = ref('');
15+
16+
async function acceptInvite() {
17+
const ring = await getRing(route.params.inviterDid, route.params.ringAndInviteRkey);
18+
if (!ring.members.some(member => member.membership.host === user.value!.did))
19+
return; // invite was rescinded
20+
21+
await user.value!.client.acceptInvite(route.params.inviterDid, route.params.ringAndInviteRkey, mainPage.value);
22+
}
23+
</script>
24+
25+
<template>
26+
<SignInGate :sign-in-text="`Sign in to view invite from @${inviterHandle}`">
27+
<div>
28+
Accept this invite from @{{ inviterHandle }}?
29+
</div>
30+
<div>
31+
<VaInput v-model="mainPage" label="My Member Page" placeholder="index.mdx"/>
32+
<VaButton @click="acceptInvite()" style="vertical-align: text-top; margin-top: -1px">OK</VaButton>
33+
</div>
34+
</SignInGate>
35+
</template>

src/views/PageView.vue src/pages/page/[handle]/[rkey]/index.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { page } from '@/lib/shared-globals';
88
import { watchImmediate } from '@vueuse/core';
99
import { watchImmediateAsync } from '@/lib/vue-utils';
1010
11-
const route = useRoute();
11+
const route = useRoute('/page/[handle]/[rkey]/');
1212
await watchImmediateAsync(
1313
route,
1414
async () => {

0 commit comments

Comments
 (0)