+14
-30
.husky/pre-commit
+14
-30
.husky/pre-commit
···
6
6
# SPDX-License-Identifier: AGPL-3.0-only
7
7
#
8
8
9
-
# Collect staged files
10
-
STAGED=$(git diff --cached --name-only)
11
-
12
-
# Track exit status
13
-
FAILED=0
9
+
test() {
10
+
cd backend || return 1
11
+
pnpm run fmt
12
+
pnpm run lint
13
+
cd ../frontend || return 1
14
+
pnpm run fmt
15
+
cd ../lexicons || return 1
16
+
pnpm run generate
17
+
pnpm run prepublish
18
+
}
14
19
15
20
echo "Testing code for any errors before committing..."
16
-
17
-
run_cmds() {
18
-
desc=$1
19
-
shift
20
-
cd "$desc" || FAILED=1 && return
21
-
echo "Running pre-commit checks for $desc..."
22
-
if ! "$@"; then
23
-
echo "ERROR: Pre-commit checks for $desc failed. Aborting commit."
24
-
FAILED=1
25
-
fi
26
-
cd ..
21
+
test
22
+
if [ $? -ne 0 ]; then {
23
+
echo "Linting failed. Commit aborted."
24
+
exit 1
27
25
}
28
-
29
-
echo "$STAGED" | grep -q "^backend/" && run_cmds "backend" pnpm run fmt && run_cmds "backend" pnpm run lint --fix
30
-
git update-index --again
31
-
32
-
echo "$STAGED" | grep -q "^frontend/" && run_cmds "frontend" pnpm run fmt
33
-
git update-index --again
34
-
35
-
echo "$STAGED" | grep -q "^lexicons/" && run_cmds "lexicons" pnpm run generate && run_cmds "lexicons" pnpm run prepublish
36
-
git update-index --again
37
-
38
-
# If any failed, block commit
39
-
if [ $FAILED -ne 0 ]; then
40
-
echo "Commit aborted due to failed checks. Please fix your code before committing."
41
-
exit 1
42
26
fi
43
27
44
28
echo "All relevant checks passed. Proceeding with commit."
+80
-20
backend/src/network/commit.ts
+80
-20
backend/src/network/commit.ts
···
18
18
import { validateClip, validateProfile, validateTag } from "./validator.js";
19
19
import { convertDidToString } from "./converters.js";
20
20
import { hashString } from "../hasher.js";
21
-
import { eq } from "drizzle-orm";
21
+
import { and, eq } from "drizzle-orm";
22
+
import type { TagRef } from "../api/types.js";
22
23
23
24
const db = Database.getInstance().getDb();
24
25
···
30
31
export async function handleClip(
31
32
event: CommitEvent<`social.clippr.${string}`>,
32
33
): Promise<void> {
33
-
if (event.commit.operation !== "create") {
34
-
Logger.warn(
35
-
`Operation '${event.commit.operation}' for ${event.commit.collection} not supported. Ignoring.`,
36
-
);
34
+
if (event.commit.operation === "delete") {
35
+
await db
36
+
.delete(clipsTable)
37
+
.where(
38
+
and(
39
+
eq(clipsTable.did, event.did),
40
+
eq(clipsTable.recordKey, event.commit.rkey),
41
+
),
42
+
);
43
+
Logger.verbose(`Deleted clip: ${event.did}/${event.commit.rkey}`, event);
37
44
return;
38
-
} // We currently do not handle these.
45
+
}
39
46
40
47
if (event.commit.record.$type !== "social.clippr.feed.clip") {
41
48
Logger.verbose(
···
76
83
return;
77
84
}
78
85
79
-
if (!(await validateClip(record))) {
86
+
if (!(await validateClip(record))) return;
87
+
88
+
if (event.commit.operation === "update") {
89
+
await db
90
+
.update(clipsTable)
91
+
.set({
92
+
did: convertDidToString(event.did),
93
+
cid: event.commit.cid,
94
+
timestamp: convertMicroToDate(event.time_us),
95
+
recordKey: event.commit.rkey,
96
+
createdAt: new Date(record.createdAt),
97
+
indexedAt: new Date(),
98
+
url: record.url,
99
+
title: record.title,
100
+
description: record.description,
101
+
tags: record.tags as TagRef[] | undefined,
102
+
notes: record.notes,
103
+
unlisted: record.unlisted,
104
+
unread: record.unread,
105
+
languages: record.languages,
106
+
})
107
+
.where(
108
+
and(
109
+
eq(clipsTable.did, event.did),
110
+
eq(clipsTable.recordKey, event.commit.rkey),
111
+
),
112
+
);
113
+
Logger.verbose(`Updated clip: ${event.did}/${event.commit.rkey}`, event);
80
114
return;
81
115
}
82
116
···
104
138
export async function handleTag(
105
139
event: CommitEvent<`social.clippr.${string}`>,
106
140
): Promise<void> {
107
-
if (event.commit.operation !== "create") {
108
-
Logger.warn(
109
-
`Operation '${event.commit.operation}' for ${event.commit.collection} not supported. Ignoring.`,
110
-
);
141
+
if (event.commit.operation === "delete") {
142
+
await db
143
+
.delete(tagsTable)
144
+
.where(
145
+
and(
146
+
eq(tagsTable.did, event.did),
147
+
eq(tagsTable.recordKey, event.commit.rkey),
148
+
),
149
+
);
150
+
Logger.verbose(`Deleted tag: ${event.did}/${event.commit.rkey}`, event);
111
151
return;
112
-
} // We currently do not handle these.
152
+
}
113
153
114
154
if (event.commit.record.$type !== "social.clippr.feed.tag") {
115
155
Logger.verbose(
···
148
188
return;
149
189
}
150
190
191
+
if (event.commit.operation === "update") {
192
+
await db
193
+
.update(tagsTable)
194
+
.set({
195
+
timestamp: convertMicroToDate(event.time_us),
196
+
did: convertDidToString(event.did),
197
+
cid: event.commit.cid,
198
+
recordKey: event.commit.rkey,
199
+
name: record.name,
200
+
description: record.description,
201
+
color: record.color,
202
+
createdAt: new Date(record.createdAt),
203
+
indexedAt: new Date(),
204
+
})
205
+
.where(
206
+
and(
207
+
eq(tagsTable.did, event.did),
208
+
eq(tagsTable.recordKey, event.commit.rkey),
209
+
),
210
+
);
211
+
Logger.verbose(`Updated tag: ${event.did}/${event.commit.rkey}`, event);
212
+
return;
213
+
}
214
+
151
215
await db.insert(tagsTable).values({
152
216
timestamp: convertMicroToDate(event.time_us),
153
217
did: convertDidToString(event.did),
···
167
231
event: CommitEvent<`social.clippr.${string}`>,
168
232
): Promise<void> {
169
233
if (event.commit.operation === "delete") {
170
-
Logger.warn(
171
-
`Operation '${event.commit.operation}' for ${event.commit.collection} not supported. Ignoring.`,
172
-
);
234
+
await db.delete(usersTable).where(eq(usersTable.did, event.did));
235
+
Logger.verbose(`Deleted profile: ${event.did}`, event);
173
236
return;
174
-
} // We currently do not handle deletes.
237
+
}
175
238
176
239
if (event.commit.record.$type !== "social.clippr.actor.profile") {
177
240
Logger.verbose(
···
257
320
avatar: record.avatar?.ref.$link,
258
321
description: record.description,
259
322
})
260
-
.where(eq(usersTable.did, convertDidToString(event.did)))
261
-
.execute();
262
-
323
+
.where(eq(usersTable.did, convertDidToString(event.did)));
263
324
Logger.verbose(`Updated profile: ${convertDidToString(event.did)}`, event);
264
-
265
325
return;
266
326
}
267
327
+9
-7
frontend/README.md
+9
-7
frontend/README.md
···
4
4
5
5
## development
6
6
7
-
If you are testing the frontend in conjunction with the AppView, you might want to change the following:
7
+
If you are testing the frontend in conjunction with the AppView, you might want to change the
8
+
following:
8
9
9
-
* OAuth automatically adapts to whether the frontend is built or in dev mode.
10
-
* ``VITE_CLIPPR_APPVIEW`` is set to the defaults for both production and development, however, if you are hosting the
11
-
appview from another location, you will need to change this.
10
+
- OAuth automatically adapts to whether the frontend is built or in dev mode.
11
+
- `VITE_CLIPPR_APPVIEW` is set to the defaults for both production and development, however, if you
12
+
are hosting the appview from another location, you will need to change this.
12
13
13
14
```shell
14
15
pnpm install
···
17
18
18
19
## deployment
19
20
20
-
If you plan to deploy the frontend and use another AppView or to add/remove OAuth scopes, you will have to modify
21
-
``public/oauth/client-metadata.json`` and the ``VITE_CLIPPR_APPVIEW`` environment variable. There are plans to add a way
22
-
to change what AppView DID the frontend proxies its requests to inside the frontend, but not before launch.
21
+
If you plan to deploy the frontend and use another AppView or to add/remove OAuth scopes, you will
22
+
have to modify `public/oauth/client-metadata.json` and the `VITE_CLIPPR_APPVIEW` environment
23
+
variable. There are plans to add a way to change what AppView DID the frontend proxies its requests
24
+
to inside the frontend, but not before launch.
23
25
24
26
```shell
25
27
pnpm run build
+6
-1
frontend/src/components/profileEditor.tsx
+6
-1
frontend/src/components/profileEditor.tsx
···
146
146
accept=".jpg,.jpeg,.png,image/jpeg,image/png"
147
147
onChange={() => uploadBlob()}
148
148
/>
149
-
<img class="profile-picture" src={avatarPreview()} alt="The user's uploaded avatar." hidden={avatarPreview() === ""} />
149
+
<img
150
+
class="profile-picture"
151
+
src={avatarPreview()}
152
+
alt="The user's uploaded avatar."
153
+
hidden={avatarPreview() === ""}
154
+
/>
150
155
<label for="displayName">display name</label>
151
156
<input
152
157
type="text"
+1
-8
frontend/src/components/profileWidget.tsx
+1
-8
frontend/src/components/profileWidget.tsx
···
4
4
* SPDX-License-Identifier: AGPL-3.0-only
5
5
*/
6
6
7
-
import {
8
-
createResource,
9
-
Match,
10
-
Show,
11
-
splitProps,
12
-
Switch,
13
-
} from "solid-js";
7
+
import { createResource, Match, Show, splitProps, Switch } from "solid-js";
14
8
import { agent } from "./loginForm.tsx";
15
9
import { fetchProfile } from "../utils/profile.ts";
16
10
···
21
15
const ProfileWidget = (props: ProfileProps) => {
22
16
const [local] = splitProps(props, ["actor"]);
23
17
const actor = () => local.actor ?? agent.session.info.sub;
24
-
25
18
26
19
const [profile] = createResource(actor, fetchProfile);
27
20
+6
-6
frontend/src/styles/index.css
+6
-6
frontend/src/styles/index.css
···
13
13
:root {
14
14
--bg: #222 !important;
15
15
--fg: #fff !important;
16
-
--controls-bg: #2B2A33 !important;
17
-
--controls-bg-hover: #52525E !important;
18
-
--controls-border: #8F8F9D !important;
16
+
--controls-bg: #2b2a33 !important;
17
+
--controls-bg-hover: #52525e !important;
18
+
--controls-border: #8f8f9d !important;
19
19
}
20
20
}
21
21
···
23
23
:root {
24
24
--bg: #fff !important;
25
25
--fg: #222 !important;
26
-
--controls-bg: #E9E9ED !important;
27
-
--controls-bg-hover: #D0D0D7 !important;
28
-
--controls-border: #8F8F9D !important;
26
+
--controls-bg: #e9e9ed !important;
27
+
--controls-bg-hover: #d0d0d7 !important;
28
+
--controls-border: #8f8f9d !important;
29
29
}
30
30
}
31
31
+1
-1
frontend/src/types.ts
+1
-1
frontend/src/types.ts
+19
frontend/src/utils/client.ts
+19
frontend/src/utils/client.ts
···
1
+
/*
2
+
* clippr: a social bookmarking service for the AT Protocol
3
+
* Copyright (c) 2025 clippr contributors.
4
+
* SPDX-License-Identifier: AGPL-3.0-only
5
+
*/
6
+
7
+
import { ServiceProxyOptions } from "@atcute/client";
8
+
9
+
// Converts the AppView environment variable into options for the client's server proxy options.
10
+
export const createServiceProxy = (): ServiceProxyOptions | undefined => {
11
+
const appviewUrl = import.meta.env.VITE_CLIPPR_APPVIEW;
12
+
if (appviewUrl.includes("localhost:")) return undefined; // TODO: You can't do PDS proxying if you're testing locally!!!
13
+
let sanitizedUrl = appviewUrl.replace(/^(https?:\/\/)/, "did:web:");
14
+
15
+
return {
16
+
did: sanitizedUrl as `did:${string}:${string}`,
17
+
serviceId: "#clippr_appview",
18
+
};
19
+
};
-6
lexicons/lib/lexicons/index.ts
-6
lexicons/lib/lexicons/index.ts
···
1
-
/*
2
-
* clippr: a social bookmarking service for the AT Protocol
3
-
* Copyright (c) 2025 clippr contributors.
4
-
* SPDX-License-Identifier: AGPL-3.0-only
5
-
*/
6
-
7
1
export * as SocialClipprActorDefs from "./types/social/clippr/actor/defs.js";
8
2
export * as SocialClipprActorGetPreferences from "./types/social/clippr/actor/getPreferences.js";
9
3
export * as SocialClipprActorGetProfile from "./types/social/clippr/actor/getProfile.js";