diff --git a/app/.npmrc b/app/.npmrc new file mode 100644 index 00000000..41583e36 --- /dev/null +++ b/app/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/app/api/utils/cid.ts b/app/api/utils/cid.ts deleted file mode 100644 index 7e9c1176..00000000 --- a/app/api/utils/cid.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as varint from './varint'; -import * as base32 from './base32'; - -export const CBOR_CODE = 0x71; - -interface Digest { - code: number; - size: number; - digest: Uint8Array; - bytes: Uint8Array; -} - -interface CID { - version: number; - code: number; - digest: Digest; - bytes: Uint8Array; -} - -/** - * Creates a CID according to ATProto's blessed format, a SHA256-hashed v1. - */ -export const createCID = async (code: number, input: Uint8Array): Promise => { - const digest = createDigest(0x12, new Uint8Array(await crypto.subtle.digest('sha-256', input))); - const bytes = encodeCID(1, code, digest.bytes); - - return { - version: 1, - code: code, - digest: digest, - bytes: bytes, - }; -}; - -export const formatCID = (cid: CID) => { - return 'b' + base32.encode(cid.bytes); -}; - -const createDigest = (code: number, digest: Uint8Array): Digest => { - const size = digest.byteLength; - const sizeOffset = varint.encodingLength(code); - const digestOffset = sizeOffset + varint.encodingLength(size); - - const bytes = new Uint8Array(digestOffset + size); - varint.encode(code, bytes, 0); - varint.encode(size, bytes, sizeOffset); - bytes.set(digest, digestOffset); - - return { - code: code, - size: size, - digest: digest, - bytes: bytes, - }; -}; - -const encodeCID = (version: number, code: number, multihash: Uint8Array) => { - const codeOffset = varint.encodingLength(version); - const hashOffset = codeOffset + varint.encodingLength(code); - - const bytes = new Uint8Array(hashOffset + multihash.byteLength); - varint.encode(version, bytes, 0); - varint.encode(code, bytes, codeOffset); - bytes.set(multihash, hashOffset); - - return bytes; -}; diff --git a/app/api/utils/tid.ts b/app/api/utils/tid.ts deleted file mode 100644 index 0f0918ba..00000000 --- a/app/api/utils/tid.ts +++ /dev/null @@ -1,34 +0,0 @@ -let lastTimestamp = 0; - -export const getCurrentTid = () => { - // we need these two aspects, which Date.now() doesn't provide: - // - monotonically increasing time - // - microsecond precision - - // while `performance.timeOrigin + performance.now()` could be used here, they - // seem to have cross-browser differences, not sure on that yet. - - let now = Math.max(Date.now() * 1_000, lastTimestamp); - - if (now === lastTimestamp) { - now += 1; - } - - lastTimestamp = now; - - const id = Math.floor(Math.random() * 1023); - - return s32encode(now) + s32encode(id).padStart(2, '2'); -}; - -const S32_CHAR = '234567abcdefghijklmnopqrstuvwxyz'; - -export const s32encode = (i: number): string => { - let s = ''; - while (i) { - const c = i % 32; - i = Math.floor(i / 32); - s = S32_CHAR.charAt(c) + s; - } - return s; -}; diff --git a/app/api/utils/varint.ts b/app/api/utils/varint.ts deleted file mode 100644 index 73b0a4b6..00000000 --- a/app/api/utils/varint.ts +++ /dev/null @@ -1,56 +0,0 @@ -const MSB = 0x80; -const REST = 0x7f; - -const MSBALL = ~REST; -const INT = Math.pow(2, 31); - -export const encode = (num: number, buf: Uint8Array, offset = 0) => { - if (num > Number.MAX_SAFE_INTEGER) { - throw new RangeError('Could not encode varint'); - } - - while (num >= INT) { - buf[offset++] = (num & 0xff) | MSB; - num /= 128; - } - - while (num & MSBALL) { - buf[offset++] = (num & 0xff) | MSB; - num >>>= 7; - } - - buf[offset] = num | 0; - return buf; -}; - -const N1 = 2 ** 7; -const N2 = 2 ** 14; -const N3 = 2 ** 21; -const N4 = 2 ** 28; -const N5 = 2 ** 35; -const N6 = 2 ** 42; -const N7 = 2 ** 49; -const N8 = 2 ** 56; -const N9 = 2 ** 63; - -export const encodingLength = (value: number) => { - return value < N1 - ? 1 - : value < N2 - ? 2 - : value < N3 - ? 3 - : value < N4 - ? 4 - : value < N5 - ? 5 - : value < N6 - ? 6 - : value < N7 - ? 7 - : value < N8 - ? 8 - : value < N9 - ? 9 - : 10; -}; diff --git a/app/com/components/dialogs/lists/AddProfileInListDialog.desktop.tsx b/app/com/components/dialogs/lists/AddProfileInListDialog.desktop.tsx index 798e0b06..494e2fa0 100644 --- a/app/com/components/dialogs/lists/AddProfileInListDialog.desktop.tsx +++ b/app/com/components/dialogs/lists/AddProfileInListDialog.desktop.tsx @@ -1,5 +1,6 @@ import { For, createEffect, createMemo, createSignal, lazy } from 'solid-js'; +import * as TID from '@mary/atproto-tid'; import { type InfiniteData, createInfiniteQuery, @@ -11,7 +12,6 @@ import { import type { AppBskyGraphListitem, Brand, ComAtprotoRepoApplyWrites } from '~/api/atp-schema'; import { multiagent, renderAccountName } from '~/api/globals/agent'; import { renderListPurpose } from '~/api/display'; -import { getCurrentTid } from '~/api/utils/tid'; import { getCurrentDate, getRecordId } from '~/api/utils/misc'; import type { ListMembersPage } from '~/api/queries/get-list-members'; @@ -137,7 +137,7 @@ const AddProfileInListDialog = (props: AddProfileInListDialogProps) => { creations.push({ $type: 'com.atproto.repo.applyWrites#create', collection: 'app.bsky.graph.listitem', - rkey: getCurrentTid(), + rkey: TID.now(), value: record, }); } diff --git a/app/desktop/components/composer/ComposerPane.tsx b/app/desktop/components/composer/ComposerPane.tsx index 6320b20f..9d894dcd 100644 --- a/app/desktop/components/composer/ComposerPane.tsx +++ b/app/desktop/components/composer/ComposerPane.tsx @@ -1,9 +1,11 @@ import { type JSX, For, Show, batch, createEffect, createMemo, createSignal, untrack, lazy } from 'solid-js'; import { unwrap } from 'solid-js/store'; -import { createQuery, useQueryClient } from '@pkg/solid-query'; import { makeEventListener } from '@solid-primitives/event-listener'; +import * as TID from '@mary/atproto-tid'; +import { createQuery, useQueryClient } from '@pkg/solid-query'; + import type { AppBskyEmbedExternal, AppBskyFeedPost, @@ -15,7 +17,6 @@ import type { } from '~/api/atp-schema'; import { multiagent } from '~/api/globals/agent'; import { extractAppLink } from '~/api/utils/links'; -import { getCurrentTid } from '~/api/utils/tid'; import { formatQueryError, getCollectionId, isDid } from '~/api/utils/misc'; import type { ThreadData } from '~/api/models/threads'; @@ -366,7 +367,7 @@ const ComposerPane = () => { date.setMilliseconds(i); const draft = posts[i]; - const rkey = getCurrentTid(); + const rkey = TID.now(); const uri = `at://${uid}/app.bsky.feed.post/${rkey}`; const rt = RESOLVED_RT.get(getPostRt(draft))!; diff --git a/app/desktop/components/composer/dialogs/drafts/SaveDraftDialog.tsx b/app/desktop/components/composer/dialogs/drafts/SaveDraftDialog.tsx index 77bc5429..68936f97 100644 --- a/app/desktop/components/composer/dialogs/drafts/SaveDraftDialog.tsx +++ b/app/desktop/components/composer/dialogs/drafts/SaveDraftDialog.tsx @@ -1,6 +1,6 @@ import { createSignal } from 'solid-js'; -import { getCurrentTid } from '~/api/utils/tid'; +import * as TID from '@mary/atproto-tid'; import { closeModal } from '~/com/globals/modals'; @@ -45,7 +45,7 @@ const SaveDraftDialog = (props: SaveDraftDialogProps) => { const shouldClear = clear(); const state = context.state; - const id = getCurrentTid(); + const id = TID.now(); const serialized: ComposerDraft = { id: id, diff --git a/app/desktop/components/composer/utils/cid.ts b/app/desktop/components/composer/utils/cid.ts index c8cfb07b..2791fc47 100644 --- a/app/desktop/components/composer/utils/cid.ts +++ b/app/desktop/components/composer/utils/cid.ts @@ -1,14 +1,15 @@ +import * as CID from '@mary/atproto-cid'; + import { encodeCbor } from '~/api/utils/cbor'; -import { CBOR_CODE, createCID, formatCID } from '~/api/utils/cid'; // Sanity-check by requiring a $type here, this is because the records are // expected to be encoded with it, even though the PDS accepts record writes // without the field. export const serializeRecordCid = async (record: { $type: string }) => { const bytes = encodeCbor(record); - const cid = await createCID(CBOR_CODE, bytes); + const cid = await CID.create(0x71, bytes); - const serialized = formatCID(cid); + const serialized = CID.format(cid); return serialized; }; diff --git a/app/desktop/components/settings/AddDeckDialog.tsx b/app/desktop/components/settings/AddDeckDialog.tsx index 097b7b2b..20f67187 100644 --- a/app/desktop/components/settings/AddDeckDialog.tsx +++ b/app/desktop/components/settings/AddDeckDialog.tsx @@ -1,11 +1,10 @@ import { createSignal } from 'solid-js'; +import * as TID from '@mary/atproto-tid'; import { navigate } from '@pkg/solid-page-router'; import { preferences } from '../../globals/settings'; -import { getCurrentTid } from '~/api/utils/tid'; - import { closeModal } from '~/com/globals/modals'; import { model } from '~/utils/input'; @@ -23,7 +22,7 @@ const AddDeckDialog = () => { const [emoji, setEmoji] = createSignal('⭐'); const handleSubmit = (ev: SubmitEvent) => { - const tid = getCurrentTid(); + const tid = TID.now(); const $name = name(); const $emoji = emoji(); diff --git a/app/desktop/components/settings/AddPaneDialog.tsx b/app/desktop/components/settings/AddPaneDialog.tsx index f8ef4ac0..2220d60f 100644 --- a/app/desktop/components/settings/AddPaneDialog.tsx +++ b/app/desktop/components/settings/AddPaneDialog.tsx @@ -1,8 +1,9 @@ import { type Component, Match, Show, Switch, createSignal } from 'solid-js'; +import * as TID from '@mary/atproto-tid'; + import type { At } from '~/api/atp-schema'; import { getAccountHandle, multiagent } from '~/api/globals/agent'; -import { getCurrentTid } from '~/api/utils/tid'; import { FILTER_ALL } from '~/api/queries/get-notifications'; @@ -72,7 +73,7 @@ const AddPaneDialog = (props: AddPaneDialogProps) => { // @ts-expect-error const pane: PaneConfig = { ...partial, - id: getCurrentTid(), + id: TID.now(), size: SpecificPaneSize.INHERIT, title: null, uid: $user, diff --git a/app/desktop/globals/settings.ts b/app/desktop/globals/settings.ts index 24be6d5a..fe0d9d48 100644 --- a/app/desktop/globals/settings.ts +++ b/app/desktop/globals/settings.ts @@ -1,10 +1,10 @@ +import * as TID from '@mary/atproto-tid'; + import { DEFAULT_MODERATION_LABELER } from '~/api/globals/defaults'; import type { LanguagePreferences, TranslationPreferences } from '~/api/types'; import type { ModerationOptions } from '~/api/moderation'; -import { getCurrentTid } from '~/api/utils/tid'; - import { createReactiveLocalStorage } from '~/utils/storage'; import { type DeckConfig, type PaneConfig, PaneSize, SpecificPaneSize } from './panes'; @@ -129,7 +129,7 @@ export const addPane = ( // @ts-expect-error const pane: PaneConfig = { ...partial, - id: getCurrentTid(), + id: TID.now(), size: SpecificPaneSize.INHERIT, title: null, }; diff --git a/app/desktop/lib/settings/onboarding.ts b/app/desktop/lib/settings/onboarding.ts index 63976c17..48819e27 100644 --- a/app/desktop/lib/settings/onboarding.ts +++ b/app/desktop/lib/settings/onboarding.ts @@ -1,8 +1,8 @@ +import * as TID from '@mary/atproto-tid'; + import type { At } from '~/api/atp-schema'; import { getAccountData } from '~/api/globals/agent'; -import { getCurrentTid } from '~/api/utils/tid'; - import { type DeckConfig, PANE_TYPE_HOME, @@ -15,7 +15,7 @@ import { FILTER_ALL } from '~/api/queries/get-notifications'; export const createEmptyDeck = (): DeckConfig => { return { - id: getCurrentTid(), + id: TID.now(), name: 'Personal', emoji: '⭐', panes: [], @@ -26,13 +26,13 @@ export const createStarterDeck = (uid: At.DID): DeckConfig => { const data = getAccountData(uid)!; return { - id: getCurrentTid(), + id: TID.now(), name: 'Personal', emoji: '⭐', panes: [ { type: PANE_TYPE_HOME, - id: getCurrentTid(), + id: TID.now(), uid: uid, size: SpecificPaneSize.INHERIT, title: null, @@ -42,7 +42,7 @@ export const createStarterDeck = (uid: At.DID): DeckConfig => { }, { type: PANE_TYPE_NOTIFICATIONS, - id: getCurrentTid(), + id: TID.now(), uid: uid, size: SpecificPaneSize.INHERIT, title: null, @@ -50,7 +50,7 @@ export const createStarterDeck = (uid: At.DID): DeckConfig => { }, { type: PANE_TYPE_PROFILE, - id: getCurrentTid(), + id: TID.now(), uid: uid, size: SpecificPaneSize.INHERIT, title: null, diff --git a/app/desktop/views/Layout.tsx b/app/desktop/views/Layout.tsx index 8877cc4b..4226d398 100644 --- a/app/desktop/views/Layout.tsx +++ b/app/desktop/views/Layout.tsx @@ -3,11 +3,11 @@ import { For, Show, Suspense, batch, lazy } from 'solid-js'; import { offset } from '@floating-ui/dom'; import { DragDropProvider, DragDropSensors, SortableProvider, createSortable } from '@thisbeyond/solid-dnd'; +import * as TID from '@mary/atproto-tid'; import { ShowFreeze } from '@pkg/solid-freeze'; import { type RouteComponentProps, location, navigate } from '@pkg/solid-page-router'; import { multiagent } from '~/api/globals/agent'; -import { getCurrentTid } from '~/api/utils/tid'; import { openModal } from '~/com/globals/modals'; import { Title } from '~/com/lib/meta'; @@ -107,7 +107,7 @@ const DashboardLayout = (props: RouteComponentProps) => { if (!deck) { decks.push({ - id: getCurrentTid(), + id: TID.now(), name: 'New deck', emoji: '⭐', panes: [], diff --git a/app/package.json b/app/package.json index 9c4412af..2993a06f 100644 --- a/app/package.json +++ b/app/package.json @@ -12,7 +12,10 @@ "dependencies": { "@floating-ui/dom": "^1.6.3", "@floating-ui/utils": "^0.2.1", + "@mary/atproto-cid": "npm:@jsr/mary__atproto-cid@^0.1.3", + "@mary/atproto-tid": "npm:@jsr/mary__atproto-tid@^0.1.1", "@mary/bluesky-client": "npm:@jsr/mary__bluesky-client@^0.5.7", + "@mary/exif-rm": "npm:@jsr/mary__exif-rm@^0.2.1", "@pkg/emoji-db": "workspace:^", "@pkg/solid-freeze": "workspace:^", "@pkg/solid-navigation": "workspace:^", diff --git a/app/utils/image.ts b/app/utils/image.ts index 055460c4..a3aeff14 100644 --- a/app/utils/image.ts +++ b/app/utils/image.ts @@ -1,4 +1,4 @@ -import { removeExif } from './image/exif-remover'; +import { remove as removeExif } from '@mary/exif-rm'; import type { Signal } from './signals'; @@ -32,7 +32,7 @@ export interface CompressProfileImageOptions { export const compressPostImage = async (blob: Blob): Promise => { { - const exifRemoved = removeExif(await blob.arrayBuffer()); + const exifRemoved = removeExif(new Uint8Array(await blob.arrayBuffer())); // have the images be read again below, to make sure the exif removal code // is working as intended. @@ -74,7 +74,7 @@ export const compressProfileImage = async ( const isEligible = type === 'image/jpeg' || type === 'image/png'; if (isEligible) { - const exifRemoved = removeExif(await blob.arrayBuffer()); + const exifRemoved = removeExif(new Uint8Array(await blob.arrayBuffer())); if (exifRemoved !== null) { blob = new Blob([exifRemoved], { type: blob.type }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 314c0406..a8a2c8ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,9 +56,18 @@ importers: '@floating-ui/utils': specifier: ^0.2.1 version: 0.2.1 + '@mary/atproto-cid': + specifier: npm:@jsr/mary__atproto-cid@^0.1.3 + version: /@jsr/mary__atproto-cid@0.1.3 + '@mary/atproto-tid': + specifier: npm:@jsr/mary__atproto-tid@^0.1.1 + version: /@jsr/mary__atproto-tid@0.1.1 '@mary/bluesky-client': specifier: npm:@jsr/mary__bluesky-client@^0.5.7 version: /@jsr/mary__bluesky-client@0.5.7 + '@mary/exif-rm': + specifier: npm:@jsr/mary__exif-rm@^0.2.1 + version: /@jsr/mary__exif-rm@0.2.1 '@pkg/emoji-db': specifier: workspace:^ version: link:../packages/emoji-db @@ -1698,10 +1707,28 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@jsr/mary__atproto-cid@0.1.3: + resolution: {integrity: sha512-ch0kPJcUJ/+8IndKyz7q5aZfhb00sThgA7kjODO0IBtiImHi6SoYthM9WCBqYB2Y5pF8lQc4ZT8s1hgXJ5jf0A==, tarball: https://npm.jsr.io/~/7/@jsr/mary__atproto-cid/0.1.3.tgz} + dependencies: + '@jsr/mary__varint': 0.1.0 + dev: false + + /@jsr/mary__atproto-tid@0.1.1: + resolution: {integrity: sha512-8T4vjlpG4vruBU3COOC208Z7LC5Gpp6IFyKmyvm/IXI3p/zKz8IBjWpFM4cJiAbAJbE+Ga8eZ2Hs8VFmWeNT9g==, tarball: https://npm.jsr.io/~/7/@jsr/mary__atproto-tid/0.1.1.tgz} + dev: false + /@jsr/mary__bluesky-client@0.5.7: resolution: {integrity: sha512-Ot3MkkQ8xaPaXmn0C5atzy8JAYM28wLML51yQpDwECGw2tPiA177ly23Ok50IxJ70OdRsAJ/k3KtzkDrxbUDJA==, tarball: https://npm.jsr.io/~/7/@jsr/mary__bluesky-client/0.5.7.tgz} dev: false + /@jsr/mary__exif-rm@0.2.1: + resolution: {integrity: sha512-Qpq/jYjkNCPQAp1ApDTNKjLfa9FDrEVumhY9ehuYWV2N00w/HqKQhkMvA+IqzxpWCDLoMwKah0hn7tau4XNBvA==, tarball: https://npm.jsr.io/~/7/@jsr/mary__exif-rm/0.2.1.tgz} + dev: false + + /@jsr/mary__varint@0.1.0: + resolution: {integrity: sha512-mXxzT3ojsZi4NzzgVTogM+0KI7lI2FSgVDeC5MbxQHZPlv/v7n4i2QS7/ya4n7BtuDYo1eMnMmtDFUIwQ1DptQ==, tarball: https://npm.jsr.io/~/7/@jsr/mary__varint/0.1.0.tgz} + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'}