From 8f8b669aaa356196613aeb9562c0af3913ad6908 Mon Sep 17 00:00:00 2001 From: "whey.party" Date: Thu, 12 Feb 2026 07:30:16 +0700 Subject: [PATCH] added labelmerge useAutoLabels, improved useGetOneToOneState, expanded policy.ts --- README.md | 5 + package-lock.json | 92 +++- package.json | 3 +- policy.ts | 104 +++- public/sunset.jpg | Bin 0 -> 96866 bytes src/api/labelmerge/index.ts | 72 +++ src/api/labelmerge/lexicons.ts | 302 ++++++++++++ .../app/reddwarf/labelmerge/queryLabels.ts | 65 +++ .../types/com/atproto/label/defs.ts | 148 ++++++ src/api/labelmerge/util.ts | 84 ++++ src/api/moderation.ts | 4 +- src/auto-imports.d.ts | 7 + src/components/PostEmbeds.tsx | 369 +++++++++----- src/components/UniversalPostRenderer.tsx | 457 +++++++++++++++--- src/hooks/useAutoLabels.ts | 201 ++++++++ src/hooks/useModeration.ts | 24 +- src/main.tsx | 5 + src/providers/AutoLabelProvider.tsx | 341 +++++++++++++ src/providers/PollMutationQueueProvider.tsx | 8 +- src/routes/moderation.tsx | 99 +++- src/routes/notifications.tsx | 174 +++++-- src/routes/profile.$did/index.tsx | 242 +++++++++- src/styles/app.css | 15 + src/utils/atoms.ts | 8 +- src/utils/followState.ts | 35 +- src/utils/useHydrated.ts | 31 +- src/utils/useQuery.ts | 413 +++++++++++++++- 27 files changed, 2987 insertions(+), 321 deletions(-) create mode 100644 public/sunset.jpg create mode 100644 src/api/labelmerge/index.ts create mode 100644 src/api/labelmerge/lexicons.ts create mode 100644 src/api/labelmerge/types/app/reddwarf/labelmerge/queryLabels.ts create mode 100644 src/api/labelmerge/types/com/atproto/label/defs.ts create mode 100644 src/api/labelmerge/util.ts create mode 100644 src/hooks/useAutoLabels.ts create mode 100644 src/providers/AutoLabelProvider.tsx diff --git a/README.md b/README.md index 3e4c999..a7e3e7e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Red Dwarf is a Bluesky client that does not use any AppView servers, instead it huge thanks to [Microcosm](https://microcosm.blue/) for making this possible +issue tracker kanban board: [https://github.com/users/rimar1337/projects/1/views/1]https://github.com/users/rimar1337/projects/1/views/1 + ## running dev and build in the `vite.config.ts` file you should change these values ```ts @@ -30,6 +32,9 @@ Red Dwarf has been upgraded from its original bespoke caching system to Tanstack all core data fetching logic is now centralized in `src/utils/useQuery.ts` and exposed as a collection of custom react hooks. theres two basic types of custom hooks, the use-once, and the inifinite query ones (used for paginated requests like feed skeletons and listrecord) ## UniversalPostRenderer +> [!NOTE] +> UPR is undergoing a refactor, so this info might be out of date + its a mega component rooted in my Masonry "[TestFront](https://testfront-87q.pages.dev/)" project. its goal is simple: have one component render everything. it has several shims to normalize different post data formats into a single format the component can handle. unlike TestFront, it has no animations, though some weird component splits might linger from the old version. to adapt TestFront's bsky-api-based `UniversalPostRenderer` to Red Dwarf's model of fetching records directly from each user's PDS and then querying constellation for backlinks, i wrap it in `UniversalPostRendererATURILoader`, which handles raw record and backlink fetching. to bridge the gap between bsky api shapes like `PostView` and the raw record, i use `UniversalPostRendererRawRecordShim`. this way, the core `UniversalPostRenderer` remains the same between TestFront and Red Dwarf (with the only difference being in the red dwarf version the framer motion animations are removed). diff --git a/package-lock.json b/package-lock.json index 6fb7f13..a9240a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "red-dwarf-tanstack", "dependencies": { - "@atproto/api": "^0.16.6", + "@atproto/api": "^0.18.20", "@atproto/common-web": "^0.4.11", "@atproto/oauth-client-browser": "^0.3.33", "@radix-ui/react-dialog": "^1.1.15", @@ -21,6 +21,7 @@ "@tanstack/react-router": "^1.130.2", "@tanstack/react-router-devtools": "^1.131.5", "@tanstack/router-plugin": "^1.121.2", + "@yornaath/batshit": "^0.14.0", "dompurify": "^3.3.0", "html-to-image": "^1.11.13", "i": "^0.3.7", @@ -206,29 +207,53 @@ "license": "ISC" }, "node_modules/@atproto/api": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.16.6.tgz", - "integrity": "sha512-/ZDWeHNAMmQFicyITAoGCxQujDU+wyzsWjpPnQfVa+ZKMfZEngipMFOr4fBwxHixEFLmuh58+5vJyylAFVrQ4g==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.18.20.tgz", + "integrity": "sha512-BZYZkh2VJIFCXEnc/vzKwAwWjAQQTgbNJ8FBxpBK+z+KYh99O0uPCsRYKoCQsRrnkgrhzdU9+g2G+7zanTIGbw==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.4.2", - "@atproto/lexicon": "^0.5.0", - "@atproto/syntax": "^0.4.1", - "@atproto/xrpc": "^0.7.4", + "@atproto/common-web": "^0.4.15", + "@atproto/lexicon": "^0.6.1", + "@atproto/syntax": "^0.4.3", + "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, + "node_modules/@atproto/api/node_modules/@atproto/lexicon": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", + "integrity": "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw==", + "license": "MIT", + "dependencies": { + "@atproto/common-web": "^0.4.13", + "@atproto/syntax": "^0.4.3", + "iso-datestring-validator": "^2.2.2", + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/api/node_modules/@atproto/xrpc": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz", + "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==", + "license": "MIT", + "dependencies": { + "@atproto/lexicon": "^0.6.0", + "zod": "^3.23.8" + } + }, "node_modules/@atproto/common-web": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.11.tgz", - "integrity": "sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ==", + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.16.tgz", + "integrity": "sha512-Ufvaff5JgxUyUyTAG0/3o7ltpy3lnZ1DvLjyAnvAf+hHfiK7OMQg+8byr+orN+KP9MtIQaRTsCgYPX+PxMKUoA==", "license": "MIT", "dependencies": { - "@atproto/lex-data": "0.0.7", - "@atproto/lex-json": "0.0.7", + "@atproto/lex-data": "^0.0.11", + "@atproto/lex-json": "^0.0.11", + "@atproto/syntax": "^0.4.3", "zod": "^3.23.8" } }, @@ -273,12 +298,11 @@ } }, "node_modules/@atproto/lex-data": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.7.tgz", - "integrity": "sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.11.tgz", + "integrity": "sha512-4+KTtHdqwlhiTKA7D4SACea4jprsNpCQsNALW09wsZ6IHhCDGO5tr1cmV+QnLYe3G3mu1E1yXHXbPUHrUUDT/A==", "license": "MIT", "dependencies": { - "@atproto/syntax": "0.4.2", "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", @@ -286,12 +310,12 @@ } }, "node_modules/@atproto/lex-json": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.7.tgz", - "integrity": "sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.11.tgz", + "integrity": "sha512-2IExAoQ4KsR5fyPa1JjIvtR316PvdgRH/l3BVGLBd3cSxM3m5MftIv1B6qZ9HjNiK60SgkWp0mi9574bTNDhBQ==", "license": "MIT", "dependencies": { - "@atproto/lex-data": "0.0.7", + "@atproto/lex-data": "^0.0.11", "tslib": "^2.8.1" } }, @@ -358,10 +382,13 @@ } }, "node_modules/@atproto/syntax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", - "license": "MIT" + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } }, "node_modules/@atproto/xrpc": { "version": "0.7.5", @@ -5207,6 +5234,21 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@yornaath/batshit": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@yornaath/batshit/-/batshit-0.14.0.tgz", + "integrity": "sha512-0I+xMi5JoRs3+qVXXhk2AmsEl43MwrG+L+VW+nqw/qQqMFtgRPszLaxhJCfsBKnjfJ0gJzTI1Q9Q9+y903HyHQ==", + "license": "MIT", + "dependencies": { + "@yornaath/batshit-devtools": "^1.7.1" + } + }, + "node_modules/@yornaath/batshit-devtools": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@yornaath/batshit-devtools/-/batshit-devtools-1.7.1.tgz", + "integrity": "sha512-AyttV1Njj5ug+XqEWY1smV45dTWMlWKtj1B8jcFYgBKUFyUlF/qEhD+iP1E5UaRYW6hQRYD9T2WNDwFTrOMWzQ==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", diff --git a/package.json b/package.json index fae48a0..5c999dc 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "test": "vitest run" }, "dependencies": { - "@atproto/api": "^0.16.6", + "@atproto/api": "^0.18.20", "@atproto/common-web": "^0.4.11", "@atproto/oauth-client-browser": "^0.3.33", "@radix-ui/react-dialog": "^1.1.15", @@ -25,6 +25,7 @@ "@tanstack/react-router": "^1.130.2", "@tanstack/react-router-devtools": "^1.131.5", "@tanstack/router-plugin": "^1.121.2", + "@yornaath/batshit": "^0.14.0", "dompurify": "^3.3.0", "html-to-image": "^1.11.13", "i": "^0.3.7", diff --git a/policy.ts b/policy.ts index d73dbcc..09a52d4 100644 --- a/policy.ts +++ b/policy.ts @@ -1,23 +1,113 @@ // please change the branding if you are not it hosting on reddwarf.app -export const HOST_TITLE = "Red Dwarf" +export const HOST_MAIN_TITLE = "Red Dwarf" // large text in branding +export const HOST_SUB_TITLE = " .app" // smaller text in branding +export const HOST_TITLE = HOST_MAIN_TITLE + HOST_SUB_TITLE // composite used in paragraphs // also replace favicon files and defaultpfp.png and check LogoSvg.tsx -// todo generate manifest.json and index.html from this file -// todo have the bottom left and right blurbs on the desktop (should move it to settings for mobile) also customizable +export const HOST_LOGO_USE_FAVICON = false; // ignores LogoSvg.tsx (recolorable svg) for a static image (the favicon) +export const HOST_DEFAULT_HUE = 28; // default is 28 for red. mod 360. 294 is a nice purple +export const HOST_HERO = "/sunset.jpg" // path to the "banner" image of the instance +export const HOST_ADMIN = "did:plc:tufumi46dykq4fzwtp2ur6kx" // did of the owner/admin, does not give special perms +export const HOST_DESCRIPTION = "The official flagship hosted Red Dwarf instance, hosted on reddwarf.app, running the latest updates and features" // short 1 sentence description +/** + * --- RED DWARF POLICY.TS — MARKDOWN FLAVOR --- + * + * This file uses a deliberately minimal Markdown subset. + * + * Supported syntax: + * - Two consecutive line breaks create a new paragraph + * - `##` denotes a collapsible section heading + * - links via [link text](link url) + * - Self-closing predefined components (e.g. ``) + * + * REQUIRED COMPONENTS (strictly one of each): + * - + * + * NOTE: + * If the app detects that any required custom component is missing, + * the application will refuse to run. Treat this file as critical. + */ +export const HOST_ABOUT_MARKDOWN = ` +## About this instance +reddwarf.app is the flagship hosted instance of Red Dwarf, a Bluesky application built to provide an independent social experience. +This hosted instance mandates Bluesky Moderation to limit moderation scope and keep resources focused on developing Red Dwarf software. + + +## About Red Dwarf + +Red Dwarf is a Bluesky client that does not rely on Bluesky API App Servers. +Instead, it uses Microcosm to fetch records directly from each user’s PDS (via Slingshot) +and connect them using backlinks (via Constellation). + +## Hosting Your Own Instance + +Red Dwarf is open source. You can host your own instance specifically tailored to your community. +Hosting your own instance gives you full control over policy, branding, and additional features. + +Repository: [https://tangled.org/whey.party/red-dwarf](https://tangled.org/whey.party/red-dwarf) + +Instructions for setup, configuration, and labeler policies are included in the repository. + +## RedDwarf.app Policy + + +` + +export const HOST_UNAUTHED_DEFAULT_FEEDS = [ + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" +] + +export const HOST_LOGIN_BLURB = "Experience Bluesky under a different light" //todo dont be corny +export const HOST_SIGNUP_PDS = false // false-able string + +// very important. if this is empty the app will refuse to load anything +// because this powers all the labels in the app (assuming youre using the newer useAutoLabels and not useModeration) +export const HOST_LABELMERGE = "https://labelmerge.reddwarf.app" + + +// forced label providers +// applies to everyone export const FORCED_LABELER_DIDS = [ "did:plc:ar7c4by46qjdydhdevvrndac" // bluesky moderation ]; + + +// unauthed forced label policy +// hides some content only when not logged in +// needs labelers to be set in FORCED_LABELER_DIDS +// TODO: add separate unauthed_forced_labeler_dids later + export const UNAUTHED_FORCE_WARN_LABELS = new Set([ - // i dont know if some of these are even valid labels "porn", "sexual", "graphic-media", "nudity", "nsfl", - "corpse", "gore", - "!no-unauthenticated" + "!no-unauthenticated", + "illicit", + "self-harm", + "sensitive", ]); -export const UNAUTHED_PREVENT_OPENING_WARNS = true; \ No newline at end of file +export const UNAUTHED_PREVENT_OPENING_WARNS = true; + + + +// forced label policy +// hides content labeled with FORCE_HIDE_LABELS +// needs labelers to be set in FORCED_LABELER_DIDS and FORCE_HIDE_LABELS_WHITELISTED_SOURCE + +export const FORCE_HIDE_LABELS_WHITELISTED_SOURCE = new Set([ + "did:plc:ar7c4by46qjdydhdevvrndac" // bluesky moderation +]) + +export const FORCE_HIDE_LABELS = new Set([ + "!takedown", + "!hide", +]); + + + +// todo generate manifest.json and index.html from this file \ No newline at end of file diff --git a/public/sunset.jpg b/public/sunset.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2c13c5d43f77d9cf65b8a4ed511f769fbd03e464 GIT binary patch literal 96866 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 { + return this._client.call( + 'app.reddwarf.labelmerge.queryLabels', + params, + undefined, + opts, + ) + } +} diff --git a/src/api/labelmerge/lexicons.ts b/src/api/labelmerge/lexicons.ts new file mode 100644 index 0000000..73500c2 --- /dev/null +++ b/src/api/labelmerge/lexicons.ts @@ -0,0 +1,302 @@ +/* eslint-disable unused-imports/no-unused-imports */ +/* eslint-disable simple-import-sort/imports */ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { + type LexiconDoc, + Lexicons, + ValidationError, + type ValidationResult, +} from '@atproto/lexicon' +import { type $Typed, is$typed, maybe$typed } from './util.js' + +export const schemaDict = { + AppReddwarfLabelmergeQueryLabels: { + lexicon: 1, + id: 'app.reddwarf.labelmerge.queryLabels', + defs: { + main: { + type: 'query', + description: + 'Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.', + parameters: { + type: 'params', + properties: { + s: { + type: 'array', + items: { + type: 'string', + }, + description: 'List of label subjects (strings).', + }, + l: { + type: 'array', + items: { + type: 'string', + format: 'did', + }, + description: 'List of label sources (labeler DIDs) to filter on.', + }, + strict: { + type: 'boolean', + description: + 'If true then any errors will throw the entire query', + }, + }, + required: ['s', 'l'], + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + error: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.reddwarf.labelmerge.queryLabels#error', + }, + }, + }, + required: ['labels'], + }, + }, + }, + error: { + type: 'object', + properties: { + s: { + type: 'string', + format: 'did', + }, + e: { + type: 'string', + }, + }, + required: ['s'], + }, + }, + }, + ComAtprotoLabelDefs: { + id: 'com.atproto.label.defs', + defs: { + label: { + type: 'object', + required: ['src', 'uri', 'val', 'cts'], + properties: { + cid: { + type: 'string', + format: 'cid', + description: + "Optionally, CID specifying the specific version of 'uri' resource this label applies to.", + }, + cts: { + type: 'string', + format: 'datetime', + description: 'Timestamp when this label was created.', + }, + exp: { + type: 'string', + format: 'datetime', + description: + 'Timestamp at which this label expires (no longer applies).', + }, + neg: { + type: 'boolean', + description: + 'If true, this is a negation label, overwriting a previous label.', + }, + sig: { + type: 'bytes', + description: 'Signature of dag-cbor encoded label.', + }, + src: { + type: 'string', + format: 'did', + description: 'DID of the actor who created this label.', + }, + uri: { + type: 'string', + format: 'uri', + description: + 'AT URI of the record, repository (account), or other resource that this label applies to.', + }, + val: { + type: 'string', + maxLength: 128, + description: + 'The short string name of the value or type of this label.', + }, + ver: { + type: 'integer', + description: 'The AT Protocol version of the label object.', + }, + }, + description: + 'Metadata tag on an atproto resource (eg, repo or record).', + }, + selfLabel: { + type: 'object', + required: ['val'], + properties: { + val: { + type: 'string', + maxLength: 128, + description: + 'The short string name of the value or type of this label.', + }, + }, + description: + 'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.', + }, + labelValue: { + type: 'string', + knownValues: [ + '!hide', + '!no-promote', + '!warn', + '!no-unauthenticated', + 'dmca-violation', + 'doxxing', + 'porn', + 'sexual', + 'nudity', + 'nsfl', + 'gore', + ], + }, + selfLabels: { + type: 'object', + required: ['values'], + properties: { + values: { + type: 'array', + items: { + ref: 'lex:com.atproto.label.defs#selfLabel', + type: 'ref', + }, + maxLength: 10, + }, + }, + description: + 'Metadata tags on an atproto record, published by the author within the record.', + }, + labelValueDefinition: { + type: 'object', + required: ['identifier', 'severity', 'blurs', 'locales'], + properties: { + blurs: { + type: 'string', + description: + "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", + knownValues: ['content', 'media', 'none'], + }, + locales: { + type: 'array', + items: { + ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings', + type: 'ref', + }, + }, + severity: { + type: 'string', + description: + "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", + knownValues: ['inform', 'alert', 'none'], + }, + adultOnly: { + type: 'boolean', + description: + 'Does the user need to have adult content enabled in order to configure this label?', + }, + identifier: { + type: 'string', + maxLength: 100, + description: + "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", + maxGraphemes: 100, + }, + defaultSetting: { + type: 'string', + default: 'warn', + description: 'The default setting for this label.', + knownValues: ['ignore', 'warn', 'hide'], + }, + }, + description: + 'Declares a label value and its expected interpretations and behaviors.', + }, + labelValueDefinitionStrings: { + type: 'object', + required: ['lang', 'name', 'description'], + properties: { + lang: { + type: 'string', + format: 'language', + description: + 'The code of the language these strings are written in.', + }, + name: { + type: 'string', + maxLength: 640, + description: 'A short human-readable name for the label.', + maxGraphemes: 64, + }, + description: { + type: 'string', + maxLength: 100000, + description: + 'A longer description of what the label means and why it might be applied.', + maxGraphemes: 10000, + }, + }, + description: + 'Strings which describe the label in the UI, localized into a specific language.', + }, + }, + lexicon: 1, + }, +} as const satisfies Record +export const schemas = Object.values(schemaDict) satisfies LexiconDoc[] +export const lexicons: Lexicons = new Lexicons(schemas) + +export function validate( + v: unknown, + id: string, + hash: string, + requiredType: true, +): ValidationResult +export function validate( + v: unknown, + id: string, + hash: string, + requiredType?: false, +): ValidationResult +export function validate( + v: unknown, + id: string, + hash: string, + requiredType?: boolean, +): ValidationResult { + return (requiredType ? is$typed : maybe$typed)(v, id, hash) + ? lexicons.validate(`${id}#${hash}`, v) + : { + success: false, + error: new ValidationError( + `Must be an object with "${hash === 'main' ? id : `${id}#${hash}`}" $type property`, + ), + } +} + +export const ids = { + AppReddwarfLabelmergeQueryLabels: 'app.reddwarf.labelmerge.queryLabels', + ComAtprotoLabelDefs: 'com.atproto.label.defs', +} as const diff --git a/src/api/labelmerge/types/app/reddwarf/labelmerge/queryLabels.ts b/src/api/labelmerge/types/app/reddwarf/labelmerge/queryLabels.ts new file mode 100644 index 0000000..ee24f61 --- /dev/null +++ b/src/api/labelmerge/types/app/reddwarf/labelmerge/queryLabels.ts @@ -0,0 +1,65 @@ +/* eslint-disable unused-imports/no-unused-imports */ +/* eslint-disable simple-import-sort/imports */ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type HeadersMap, XRPCError } from '@atproto/xrpc' +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +// import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../../lexicons' +import { + type $Typed, + is$typed as _is$typed, + type OmitKey, +} from '../../../../util' +import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.js' + +const is$typed = _is$typed, + validate = _validate +const id = 'app.reddwarf.labelmerge.queryLabels' + +export type QueryParams = { + /** List of label subjects (strings). */ + s: string[] + /** List of label sources (labeler DIDs) to filter on. */ + l: string[] + /** If true then any errors will throw the entire query */ + strict?: boolean +} +export type InputSchema = undefined + +export interface OutputSchema { + labels: ComAtprotoLabelDefs.Label[] + error?: Error[] +} + +export interface CallOptions { + signal?: AbortSignal + headers?: HeadersMap +} + +export interface Response { + success: boolean + headers: HeadersMap + data: OutputSchema +} + +export function toKnownErr(e: any) { + return e +} + +export interface Error { + $type?: 'app.reddwarf.labelmerge.queryLabels#error' + s: string + e?: string +} + +const hashError = 'error' + +export function isError(v: V) { + return is$typed(v, id, hashError) +} + +export function validateError(v: V) { + return validate(v, id, hashError) +} diff --git a/src/api/labelmerge/types/com/atproto/label/defs.ts b/src/api/labelmerge/types/com/atproto/label/defs.ts new file mode 100644 index 0000000..5311209 --- /dev/null +++ b/src/api/labelmerge/types/com/atproto/label/defs.ts @@ -0,0 +1,148 @@ +/* eslint-disable unused-imports/no-unused-imports */ +/* eslint-disable simple-import-sort/imports */ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +// import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../../lexicons' +import { + type $Typed, + is$typed as _is$typed, + type OmitKey, +} from '../../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'com.atproto.label.defs' + +/** Metadata tag on an atproto resource (eg, repo or record). */ +export interface Label { + $type?: 'com.atproto.label.defs#label' + /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */ + cid?: string + /** Timestamp when this label was created. */ + cts: string + /** Timestamp at which this label expires (no longer applies). */ + exp?: string + /** If true, this is a negation label, overwriting a previous label. */ + neg?: boolean + /** Signature of dag-cbor encoded label. */ + sig?: Uint8Array + /** DID of the actor who created this label. */ + src: string + /** AT URI of the record, repository (account), or other resource that this label applies to. */ + uri: string + /** The short string name of the value or type of this label. */ + val: string + /** The AT Protocol version of the label object. */ + ver?: number +} + +const hashLabel = 'label' + +export function isLabel(v: V) { + return is$typed(v, id, hashLabel) +} + +export function validateLabel(v: V) { + return validate
feedgen placeholder
; + return
feedgen placeholder
; } else if ( !!reallybaduri && !!reallybadaturi && reallybadaturi.collection === "app.bsky.feed.generator" ) { return ( -
+
); } if (AppBskyGraphDefs.isListView(embed.record)) { - return
list placeholder
; + return
list placeholder
; } else if ( !!reallybaduri && !!reallybadaturi && reallybadaturi.collection === "app.bsky.graph.list" ) { return ( -
+
starter pack card placeholder
; + return
starter pack card placeholder
; } else if ( !!reallybaduri && !!reallybadaturi && reallybadaturi.collection === "app.bsky.graph.starterpack" ) { return ( -
+
); + + } if (AppBskyEmbedRecord.isViewNotFound(embed.record)) { + return ( + + ) } else { console.log("what the hell is a ", embed); return <>sorry; @@ -280,29 +288,40 @@ export function PostEmbeds({ width: "100%", aspectRatio: image.aspectRatio ? (() => { - const { width, height } = image.aspectRatio; - const ratio = width / height; - return ratio < 0.5 ? "1 / 2" : `${width} / ${height}`; - })() + const { width, height } = image.aspectRatio; + const ratio = width / height; + return ratio < 0.5 ? "1 / 2" : `${width} / ${height}`; + })() : "1 / 1", borderRadius: 12, overflow: "hidden", }} className="border border-gray-200 dark:border-gray-800 was7 bg-gray-200 dark:bg-gray-900" > - {image.alt} { - e.stopPropagation(); - setLightboxIndex(0); - }} - /> + {redactedLoading ? ( +
+ ) : ( + {image.alt} { + e.stopPropagation(); + setLightboxIndex(0); + }} + /> + )}
); @@ -326,20 +345,32 @@ export function PostEmbeds({ key={i} style={{ flex: 1, aspectRatio: "1 / 1", position: "relative" }} > - {img.alt} { - e.stopPropagation(); - setLightboxIndex(i); - }} - /> + {redactedLoading ? ( +
+ ) : ( + {img.alt} { + e.stopPropagation(); + setLightboxIndex(i); + }} + /> + )}
))}
@@ -362,20 +393,32 @@ export function PostEmbeds({
- {images[0].alt} { - e.stopPropagation(); - setLightboxIndex(0); - }} - /> + {redactedLoading ? ( +
+ ) : ( + {images[0].alt} { + e.stopPropagation(); + setLightboxIndex(0); + }} + /> + )}
- {images[i].alt} { - e.stopPropagation(); - setLightboxIndex(i + 1); - }} - /> + {redactedLoading ? ( +
+ ) : ( + {images[i].alt} { + e.stopPropagation(); + setLightboxIndex(i + 1); + }} + /> + )}
))}
@@ -440,27 +495,46 @@ export function PostEmbeds({ position: "relative", }} > - {img.alt} { - e.stopPropagation(); - setLightboxIndex(i); - }} - /> + {redactedLoading ? ( +
+ ) : ( + {img.alt} { + e.stopPropagation(); + setLightboxIndex(i); + }} + /> + )}
))}
@@ -476,12 +550,17 @@ export function PostEmbeds({ const hasPollLink = pollLinks && Object.keys(pollLinks).length > 0; if (hasPollLink && postid) { - return ; + // warning: i gave up and warpped it in a div lmao + return ( +
+ +
+ ); } const link = embed.external; return ( - + ); } @@ -493,6 +572,7 @@ export function PostEmbeds({ url={playlist} thumbnail={embed.thumbnail} aspect={embed.aspectRatio} + redactedLoading={redactedLoading} /> ); } @@ -504,10 +584,12 @@ export function ExternalLinkEmbed({ link, onOpen, style, + redactedLoading }: { link: AppBskyEmbedExternal.ViewExternal; onOpen?: () => void; style?: React.CSSProperties; + redactedLoading?: boolean; }) { const { uri, title, description, thumb } = link; const thumbAspectRatio = 1.91; @@ -554,7 +636,7 @@ export function ExternalLinkEmbed({ return (
{ @@ -581,18 +663,32 @@ export function ExternalLinkEmbed({ }} className="border-b border-gray-200 dark:border-gray-800 was7" > - {description} + {redactedLoading ? ( +
+ ) : ( + {description} + )}
)}
{title}
{description}
@@ -629,12 +725,14 @@ export function ExternalLinkEmbed({ gap: 4, }} > - +
+ +
{getDomain(uri)} @@ -649,10 +747,12 @@ export const SmartHLSPlayer = ({ url, thumbnail, aspect, + redactedLoading, }: { url: string; thumbnail?: string; aspect?: AppBskyEmbedDefs.AspectRatio; + redactedLoading?: boolean; }) => { const [playing, setPlaying] = useState(false); const containerRef = useRef(null); @@ -693,24 +793,38 @@ export const SmartHLSPlayer = ({ > {!playing && ( <> - Video thumbnail { - e.stopPropagation(); - setPlaying(true); - }} - /> + {redactedLoading ? ( +
+ ) : ( + Video thumbnail { + e.stopPropagation(); + if (redactedLoading) return; + setPlaying(true); + }} + /> + )}
{ e.stopPropagation(); + if (redactedLoading) return; setPlaying(true); }} style={{ @@ -722,9 +836,9 @@ export const SmartHLSPlayer = ({ pointerEvents: "none", userSelect: "none", }} - className="text-shadow-md" + //className="text-shadow-md" > - +
)} @@ -735,9 +849,8 @@ export const SmartHLSPlayer = ({ width: "100%", borderRadius: 12, overflow: "hidden", - paddingTop: `${ - 100 / (aspect ? aspect.width / aspect.height : 16 / 9) - }%`, + paddingTop: `${100 / (aspect ? aspect.width / aspect.height : 16 / 9) + }%`, }} className="border border-gray-200 dark:border-gray-800 was7" > diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx index 111c735..cd608aa 100644 --- a/src/components/UniversalPostRenderer.tsx +++ b/src/components/UniversalPostRenderer.tsx @@ -15,16 +15,17 @@ import { HoverCard } from "radix-ui"; import * as React from "react"; import { useEffect, useState } from "react"; -import { UNAUTHED_PREVENT_OPENING_WARNS } from "~/../policy"; +import { FORCE_HIDE_LABELS, FORCE_HIDE_LABELS_WHITELISTED_SOURCE, UNAUTHED_PREVENT_OPENING_WARNS } from "~/../policy"; import defaultpfp from "~/../public/defaultpfp.png"; +import { getGetHydratedLabelDefs, useAutoLabels } from "~/hooks/useAutoLabels"; import { useLabelInfo } from "~/hooks/useLabelInfo"; -import { useModeration } from "~/hooks/useModeration"; +//import { useModeration } from "~/hooks/useModeration"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { renderSnack } from "~/routes/__root"; //import { ModerationInner } from "~/routes/moderation"; -import { FollowButton, Mutual } from "~/routes/profile.$did"; +import { FollowButton, getLocaleLabel, type LabelWithHydratedLocaleName, Mutual } from "~/routes/profile.$did"; import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; -import type { ContentLabel } from "~/types/moderation"; +//import type { ContentLabel } from "~/types/moderation"; import { composerAtom, constellationURLAtom, @@ -32,6 +33,7 @@ import { enableWafrnTextAtom, imgCDNAtom, } from "~/utils/atoms"; +import { useGetOneToOneState } from "~/utils/followState"; import { useFastLike } from "~/utils/likeMutationQueue"; import { useHydratedEmbed } from "~/utils/useHydrated"; import { @@ -206,8 +208,52 @@ export function UniversalPostRendererATURILoader({ } })(); - if (!postQuery?.value) { - return <>; + // placeholder for when a post is missing + if (!isPostLoading && !postQuery?.value || isPostError) { + if (feedviewpost) { + return null // if feed view post then missing post isnt important and just remove it from view + } + return ( + <> + {/* todo add reply lines here. */} + {/* todo dont let the UPR render the shitty placeholder uri we received */} + {/*
*/} + +
+
+
+
+
+
+
+ + Missing {isQuote ? "Quoted" : ""} Post +
+
+
+ +
+
+
+
+ + ); } return ( @@ -512,6 +558,7 @@ export function UniversalPostRendererRawRecordShim({ bottomReplyLine={bottomReplyLine} topReplyLine={topReplyLine} bottomBorder={bottomBorder} + feedviewpost={feedviewpost} feedviewpostreplyhandle={feedviewpostreplyhandle} repostedby={feedviewpostrepostedbyhandle} style={style} @@ -540,6 +587,7 @@ export function UniversalPostRenderer({ topReplyLine, salt, bottomBorder = true, + feedviewpost, feedviewpostreplyhandle, depth = 0, repostedby, @@ -563,6 +611,7 @@ export function UniversalPostRenderer({ topReplyLine?: boolean; salt: string; bottomBorder?: boolean; + feedviewpost?: boolean; feedviewpostreplyhandle?: string; depth?: number; repostedby?: string; @@ -575,23 +624,95 @@ export function UniversalPostRenderer({ maxReplies?: number; constellationLinks?: any; }) { - const { isLoading: authorModLoading, labels: authorLabels } = useModeration( + + // todo move moderation to one of the UniversalPostRenderer wrapper components, and not the pure renderer component. please. thanks + // todo please move all moderation including labeling and blocks into a wrapper component please i beg you + + const subjects = [ post.author.did, - ); - const { isLoading: contentModLoading, labels: contentLabels } = useModeration( + `at://${post.author.did}/app.bsky.actor.profile/self`, post.uri, - ); + ] + + const { + results: labelResults, + hydratedLabelDefs, + } = useAutoLabels({ + subjects, + type: "post", // or whatever you’re keying on for now + }) + + const ghld = getGetHydratedLabelDefs(hydratedLabelDefs) + const accountResult = labelResults.get(post.author.did) + const profileResult = labelResults.get( + `at://${post.author.did}/app.bsky.actor.profile/self`, + ) + const postResult = labelResults.get(post.uri) + + const accountLabelVerdict = accountResult?.labelVerdict ?? "unknown" + const authorLabels = accountResult?.labels ?? [] + + const profileLabelVerdict = profileResult?.labelVerdict ?? "unknown" + const profileLabels = profileResult?.labels ?? [] + + const postLabelVerdict = postResult?.labelVerdict ?? "unknown" + const contentLabels = postResult?.labels ?? [] + + const combinedLabels = [...authorLabels, ...profileLabels, ...contentLabels] + + const authorModUnknown = accountLabelVerdict === "unknown"; + const profileModUnknown = profileLabelVerdict === "unknown"; + const contentModUnknown = postLabelVerdict === "unknown"; + + const authorModLoading = accountLabelVerdict === "loading"; + const profileModLoading = profileLabelVerdict === "loading"; + const contentModLoading = postLabelVerdict === "loading"; + + const authorModError = accountLabelVerdict === "error"; + const profileModError = profileLabelVerdict === "error"; + const contentModError = postLabelVerdict === "error"; + + const verdictDebugString = `accountLabelVerdict: ${accountLabelVerdict}, profileLabelVerdict: ${profileLabelVerdict}, postLabelVerdict: ${postLabelVerdict}` + //const verdictDebugStringCauses = + + const strictModerationUnknown = authorModUnknown || profileModUnknown || contentModUnknown + const strictModerationLoading = authorModLoading || profileModLoading || contentModLoading + const strictModerationError = authorModError || profileModError || contentModError + + const strictModerationDontShow = strictModerationUnknown || strictModerationLoading || strictModerationError + const hideAuthorLabels = authorLabels.filter( - (label) => label.preference === "hide", + (label) => ghld(label.src, label.val)?.pref === "hide", ); const warnAuthorLabels = authorLabels.filter( - (label) => label.preference === "warn", + (label) => ghld(label.src, label.val)?.pref === "warn", + ); + // const errorAuthorLabels = authorLabels.filter( + // //(label) => ghld(label.src,label.val)?.severity === "hide", + // ); + const hideProfileLabels = profileLabels.filter( + (label) => ghld(label.src, label.val)?.pref === "hide", + ); + const warnProfileLabels = profileLabels.filter( + (label) => ghld(label.src, label.val)?.pref === "warn", ); const hideContentLabels = contentLabels.filter( - (label) => label.preference === "hide", + (label) => ghld(label.src, label.val)?.pref === "hide", ); const warnContentLabels = contentLabels.filter( - (label) => label.preference === "warn", + (label) => ghld(label.src, label.val)?.pref === "warn", + ); + + const informCombinedLabels: LabelWithHydratedLocaleName[] = combinedLabels.flatMap( + (label) => { + if (ghld(label.src, label.val)?.severity === "inform" && ghld(label.src, label.val)?.pref === "warn") { + return [{ + ...label, + name: getLocaleLabel(ghld(label.src, label.val))?.name || label.val + }] + } + return [] + }, ); const parsed = new AtUri(post.uri); @@ -606,6 +727,30 @@ export function UniversalPostRenderer({ ); const { liked, toggle, backfill } = useFastLike(post.uri, post.cid); + const agentDid = agent?.did; + const authorDid = post.author.did; + + const userBlocksAuthor = useGetOneToOneState( + agentDid && authorDid + ? { + target: authorDid, + user: agentDid, + collection: "app.bsky.graph.block", + path: ".subject", + } + : undefined, + ); + const authorBlocksUser = useGetOneToOneState( + agentDid && authorDid + ? { + target: agentDid, + user: authorDid, + collection: "app.bsky.graph.block", + path: ".subject", + } + : undefined, + ); + const repostOrUnrepostPost = async () => { if (!agent) { console.error("Agent is null or undefined"); @@ -682,21 +827,160 @@ export function UniversalPostRenderer({ const showContentWarning = warnContentLabels.length > 0; const [isOpen, setIsOpen] = useState(!showContentWarning); + const [hasUserTouchedToggleYet, setHasUserTouchedToggleYet] = useState(false); - useEffect(()=>{ - if(!hasUserTouchedToggleYet && showContentWarning) { + // Force Hiddens from host policy + const isForceHiddenAuthor = authorLabels.some((label) => { + return ( + FORCE_HIDE_LABELS.has(label.val) && + FORCE_HIDE_LABELS_WHITELISTED_SOURCE.has(label.src) + ); + }); + const isForceHiddenProfile = profileLabels.some((label) => { + return ( + FORCE_HIDE_LABELS.has(label.val) && + FORCE_HIDE_LABELS_WHITELISTED_SOURCE.has(label.src) + ); + }); + const isForceHiddenPost = contentLabels.some((label) => { + return ( + FORCE_HIDE_LABELS.has(label.val) && + FORCE_HIDE_LABELS_WHITELISTED_SOURCE.has(label.src) + ); + }); + const isForceHidden = isForceHiddenAuthor || isForceHiddenProfile || isForceHiddenPost + + + useEffect(() => { + if (!hasUserTouchedToggleYet && showContentWarning) { + // eslint-disable-next-line react-hooks/set-state-in-effect setIsOpen(false); } - },[hasUserTouchedToggleYet, showContentWarning]) + }, [hasUserTouchedToggleYet, showContentWarning]) + + + console.log("HLLO HLLO HisForceHidden post UPR" + post.uri + post.author.did + isForceHidden, "1what", contentLabels, "2what", authorLabels) + + // if (hideAuthorLabels.length > 0 || hideContentLabels.length > 0 || isForceHidden || strictModerationDontShow) { + // return ( + //
+ // DEBUG LOADING LABELS + // {post.uri} + // {verdictDebugString} + //
+ // ); + // } + // if ( isForceHidden ) { + // return ( + //
+ // Post Hidden + //
+ // ) + // } + + // todo respect the blur label def + // todo scrap the verdict system and rename it into what it is (loading state) + const redactWhileLoadingAuthor = authorModLoading || authorModError || authorModUnknown + const redactWhileLoadingProfile = profileModLoading || profileModError || profileModUnknown + const redactWhileLoadingPost = contentModLoading || contentModError || contentModUnknown + const redactWhileLoadingBlock = userBlocksAuthor.isLoading || authorBlocksUser.isLoading + const redactWhileLoadingSome = redactWhileLoadingAuthor || redactWhileLoadingProfile || redactWhileLoadingPost || redactWhileLoadingBlock + /** + * maybe rules: + * if author is loading, hide everything + * if post is loading, hide text and embeds + * if profile is loading, hide pfp + */ + + // the || !post.record?.createdAt is so that users cant imply theyre replying to a non existant post by a user + // if the post doesnt exist, dont render the name or pfp + + + const redactWhileLoading_name = redactWhileLoadingAuthor || !post.record?.createdAt || redactWhileLoadingBlock + const redactWhileLoading_content = redactWhileLoadingAuthor || redactWhileLoadingPost || !post.record?.createdAt || redactWhileLoadingBlock + const redactWhileLoading_pfp = redactWhileLoadingAuthor || redactWhileLoadingProfile || !post.record?.createdAt || redactWhileLoadingBlock + + + const redactFinalBlock = userBlocksAuthor.uris.length > 0 || authorBlocksUser.uris.length > 0 + + const redactFinalAuthor = hideAuthorLabels.length > 0 || isForceHiddenAuthor || redactFinalBlock + const redactFinalProfile = hideProfileLabels.length > 0 || isForceHiddenProfile || redactFinalBlock + const redactFinalPost = hideContentLabels.length > 0 || isForceHiddenPost || redactFinalBlock + + const redactFinalSome = redactFinalAuthor || redactFinalProfile || redactFinalPost || redactFinalBlock + + // todo consider if adding an explicit "post removed" visible component is better for this + //if (redactFinalSome) return null + // todo preserve reply lines + // todo share the component with the Missing post from above + if (redactFinalSome) { + if (feedviewpost) { + return null // if feed view post then moderated post isnt important and just remove it from view + } + return ( +
{ + setMainItem({ post: post }); + onPostClick(e); + } + : () => { + setMainItem({ post: post }); + } + : undefined + }> + +
+
+
+
+
+
+ + Moderated Post +
+
    + {userBlocksAuthor.uris.length > 0 && (
  • User Blocked by You
  • )} + {authorBlocksUser.uris.length > 0 && (
  • User Blocking You
  • )} + {hideAuthorLabels.length > 0 && (<>{hideAuthorLabels.map((label) => { return
  • Author Label: {getLocaleLabel(ghld(label.src, label.val))?.name || label.val}
  • })})} + {hideProfileLabels.length > 0 && (<>{hideProfileLabels.map((label) => { return
  • Profile Label: {getLocaleLabel(ghld(label.src, label.val))?.name || label.val}
  • })})} + {hideContentLabels.length > 0 && (<>{hideContentLabels.map((label) => { return
  • Post Label: {getLocaleLabel(ghld(label.src, label.val))?.name || label.val}
  • })})} +
+
+
- if (hideAuthorLabels.length > 0 || hideContentLabels.length > 0) { - return null; +
+
+
+
+ ) } + // ${redactWhileLoadingSome && "blur"} return ( -
+
+ {/* {JSON.stringify(post, null, 2)} */}
Reposted by @{isRepost}
@@ -777,15 +1062,27 @@ export function UniversalPostRenderer({ }} onClick={onProfileClick} > - avatar + {redactWhileLoading_pfp ? ( +
+ ) : ( + avatar + ) + } +
@@ -797,23 +1094,28 @@ export function UniversalPostRenderer({ >
- avatar + {redactWhileLoading_pfp ? ( +
+ ) : ( + avatar + ) + }
-
- {post.author.displayName || post.author.handle} +
+ {redactWhileLoading_name ? "Person Display Name" : (post.author.displayName || post.author.handle)}
-
+
@ - {post.author.handle} + {redactWhileLoading_name ? "person.placeholder" : post.author.handle}
{uprrrsauthor?.description && ( @@ -892,9 +1194,9 @@ export function UniversalPostRenderer({ gap: 4, alignItems: "center", }} - className="text-gray-900 dark:text-gray-100" + className={`text-gray-900 dark:text-gray-100 ${redactWhileLoading_name && "animate-pulse blur"}`} > - {post.author.displayName || post.author.handle} + {redactWhileLoading_name ? "Person Display Name" : post.author.displayName || post.author.handle} {post.author.verification?.verifiedStatus == "valid" && ( )} @@ -910,9 +1212,9 @@ export function UniversalPostRenderer({ flexGrow: 0, minWidth: 0, }} - className="text-gray-500 dark:text-gray-400" + className={`text-gray-500 dark:text-gray-400 ${redactWhileLoading_name && "animate-pulse blur"}`} > - @{post.author.handle} + @{redactWhileLoading_name ? "person.placeholder" : post.author.handle}
*/} {authorModLoading ? (
-
- {/* + / avatar */} + /> / loading badges... -
+
*/}
) : ( -
- {warnAuthorLabels.map((label, index) => ( +
+ {informCombinedLabels.map((label, index) => ( ))}
@@ -979,12 +1281,13 @@ export function UniversalPostRenderer({ opacity: !(expanded || isQuote) && !!feedviewpostreplyhandle ? 1 : 0, }} - className="text-gray-500 dark:text-gray-400" + className={`text-gray-500 dark:text-gray-400 ${redactWhileLoading_content && "animate-pulse blur"}`} > Reply to @{feedviewpostreplyhandle}
)} {/* */} + {/* todo migrate cw stuff to the new useAutoLabels system */} {showContentWarning && ( {fedi ? ( <> @@ -1038,6 +1341,7 @@ export function UniversalPostRenderer({
{post.embed && depth < 1 && !concise ? ( 0 && ( <> -
+
(there is an embed here thats too deep to render)
@@ -1074,7 +1378,7 @@ export function UniversalPostRenderer({ borderBottomWidth: 1, marginBottom: 8, }} - className="text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-800 was7" + className={`text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-800 was7 ${redactWhileLoading_content && "animate-pulse blur"}`} > {fullDateTimeFormat(post.indexedAt)}
@@ -1098,6 +1402,7 @@ export function UniversalPostRenderer({ style={{ ...btnstyle, }} + className={redactWhileLoading_content && "animate-pulse blur" || undefined} > {post.replyCount} @@ -1110,6 +1415,7 @@ export function UniversalPostRenderer({ ...(hasRetweeted ? { color: "#5CEFAA" } : {}), }} aria-label="Repost or quote post" + className={redactWhileLoading_content && "animate-pulse blur" || undefined} > {hasRetweeted ? ( @@ -1159,6 +1465,7 @@ export function UniversalPostRenderer({ ...btnstyle, ...(liked ? { color: "#EC4899" } : {}), }} + className={redactWhileLoading_content && "animate-pulse blur" || undefined} > {liked ? ( @@ -1234,7 +1541,7 @@ export function ContentWarning({ onPress, }: { unauthedgate?: boolean; - labels: ContentLabel[]; + labels: ATPAPI.ComAtprotoLabelDefs.Label[]; isOpen: boolean; onPress: React.MouseEventHandler; }) { @@ -1242,7 +1549,7 @@ export function ContentWarning({ // Pre-calculate text for cleaner JSX const labelText = labels - .map((label) => getLabelInfo(label.sourceDid, label.val).name) + .map((label) => getLabelInfo(label.src, label.val).name) .join(", "); return ( @@ -1288,29 +1595,49 @@ export function SmallAuthorLabelBadge({ label, large, }: { - label: ContentLabel; + label: LabelWithHydratedLocaleName; large?: boolean; }) { /* -{" "} - {label.preference} (from {label.sourceDid}) + {ghld(label.src,label.val)?.severity} (from {label.sourceDid}) */ - const { getLabelInfo } = useLabelInfo(); - const info = getLabelInfo(label.sourceDid, label.val); + //const info = getLabelInfo(label.src, label.val); const [imgcdn] = useAtom(imgCDNAtom); const { data: opProfile } = useQueryProfile( - `at://${label.sourceDid}/app.bsky.actor.profile/self`, + `at://${label.src}/app.bsky.actor.profile/self`, ); - const resolvedpfp = getAvatarUrl(opProfile, label.sourceDid, imgcdn); + const resolvedpfp = getAvatarUrl(opProfile, label.src, imgcdn); + return ( + + ); +} + +// todo add click event to explain the label or soemthing +export function SmallAuthorLabelBadgeInner({ + resolvedpfp, + text, + large, + disablepfp = false, +}: { + resolvedpfp?: string; + text: string; + large?: boolean; + disablepfp?: boolean; +}) { return (
- avatar - {info.name || label.val} + />)} + {text}
); } diff --git a/src/hooks/useAutoLabels.ts b/src/hooks/useAutoLabels.ts new file mode 100644 index 0000000..e6fce07 --- /dev/null +++ b/src/hooks/useAutoLabels.ts @@ -0,0 +1,201 @@ +import * as ATPAPI from "@atproto/api"; + +import { + type HydratedLabelValueDefinition, + type labelpref, + useAutoLabelConfig, +} from "~/providers/AutoLabelProvider"; +import { useQueryLabels } from "~/utils/useQuery"; + +export function normalizeLabels( + labels: ATPAPI.ComAtprotoLabelDefs.Label[], +): ATPAPI.ComAtprotoLabelDefs.Label[] { + const map = new Map(); + + for (const label of labels) { + const key = `${label.src}::${label.uri}::${label.val}`; + + if (label.neg) { + map.delete(key); + } else { + map.set(key, label); + } + } + + return [...map.values()]; +} + +function computeVerdict( + prefs: labelpref[], + labels: ATPAPI.ComAtprotoLabelDefs.Label[], +): ATPAPI.AppBskyActorDefs.ContentLabelPref["visibility"] { + for (const pref of prefs) { + // check if any label matches this pref + const hit = labels.some((l) => l.src === pref.did && l.val === pref.label); + + if (!hit) continue; + + if (pref.visibility === "hide") { + return "hide"; + } + + if (pref.visibility === "warn") { + return "warn"; + } + } + + return "ignore"; +} + +export function useAutoLabels({ + subjects, + type, +}: { + subjects: string[]; + type: "post" | "profile" | "account"; +}): { + results: Map< + string, + { + labelVerdict: ATPAPI.AppBskyActorDefs.ContentLabelPref["visibility"]; + labels: ATPAPI.ComAtprotoLabelDefs.Label[]; + } + >; + hydratedLabelDefs: Map< + string, + HydratedLabelValueDefinition + >; +} { + const { + activeLabelerDids, + mergedPrefContentLabels, + hydratedLabelDefs, + isLoading: configLoading, + isError: configError, + sendError, + } = useAutoLabelConfig(); + + const results = new Map< + string, + { + labelVerdict: ATPAPI.AppBskyActorDefs.ContentLabelPref["visibility"]; + labels: ATPAPI.ComAtprotoLabelDefs.Label[]; + } + >(); + + // const res = useQueryLabelMerge({ + // s: subjects, + // l: activeLabelerDids, + // strict: false, + // }); + const res = useQueryLabels(subjects,activeLabelerDids); + if (configLoading || res.isLoading) { + for (const subject of subjects) { + const subjectLabels = [] as ATPAPI.ComAtprotoLabelDefs.Label[]; + results.set(subject, { + labelVerdict: "loading", + labels: subjectLabels, + }); + } + return { + results, + hydratedLabelDefs, + }; + } + if (configError || res.isError || res.data?.error) { + if (res.isError) { + sendError({ + did: "!all", + message: "queryFailure" + }) + } else + if (res.data?.error) { + console.log("fuck shit !internal-IDK-unknown, ", res.data) + res.data?.error?.forEach((err)=>{ + sendError({ + did: err.s, + message: err.e || "!internal-uAL-unknown, len: " + res.data.error?.length + ". label-length" + res.data.labels.length + }) + }) + } + console.error("Error fetching auto-label configuration or subject labels",{ + configError: configError, + isLoading: res.isLoading, + isError: res.isError, + data: res.data, + //error: res.error, + }); + + for (const subject of subjects) { + const subjectLabels = [] as ATPAPI.ComAtprotoLabelDefs.Label[]; + results.set(subject, { + labelVerdict: "error", + labels: subjectLabels, + }); + } + return { + results, + hydratedLabelDefs, + }; + } + + if (res.data && (res.data.labels.length === 0) ) { + // early return because wow you did it theres no labels to be computed! wonderful! wow! + // group labels by subject (uri) + + for (const subject of subjects) { + const subjectLabels = [] as ATPAPI.ComAtprotoLabelDefs.Label[]; + results.set(subject, { + labelVerdict: "ignore", + labels: subjectLabels, + }); + } + return { + results, + hydratedLabelDefs, + }; + } + + const effectiveLabels = normalizeLabels(res.data?.labels ?? []); + + // group labels by subject (uri) + const labelsBySubject = new Map< + string, + ATPAPI.ComAtprotoLabelDefs.Label[] + >(); + + for (const label of effectiveLabels) { + const arr = labelsBySubject.get(label.uri) ?? []; + arr.push(label); + labelsBySubject.set(label.uri, arr); + } + + for (const subject of subjects) { + const subjectLabels = labelsBySubject.get(subject) ?? []; + const verdict = computeVerdict( + mergedPrefContentLabels, + subjectLabels, + ); + results.set(subject, { + labelVerdict: verdict, + labels: subjectLabels, + }); + } + + return { + results, + hydratedLabelDefs, + }; +} + +export function getGetHydratedLabelDefs( + hydratedLabelDefs: Map< + string, + HydratedLabelValueDefinition + >, +) { + function getHydratedLabelDefs(did: string, label: string) { + return hydratedLabelDefs.get(did + "::" + label); + } + return getHydratedLabelDefs; +} diff --git a/src/hooks/useModeration.ts b/src/hooks/useModeration.ts index ab7b47b..728e898 100644 --- a/src/hooks/useModeration.ts +++ b/src/hooks/useModeration.ts @@ -8,24 +8,28 @@ import { pendingUriQueueAtom, processingUriSetAtom, } from "~/state/moderationAtoms"; - -export const useModeration = (uri: string) => { +/** + * Deprecated, left as is for potential future use + * @deprecated please use the newer useAutoLabels() hook + */ +export const useModeration = (opaqueIdentifierStringToBeModerated: string) => { const setQueue = useSetAtom(pendingUriQueueAtom); // 1. Select ONLY this URI's cache entry const entryAtom = useMemo( - () => selectAtom(moderationCacheAtom, (cache) => cache.get(uri)), - [uri], + () => selectAtom(moderationCacheAtom, (cache) => cache.get(opaqueIdentifierStringToBeModerated)), + [opaqueIdentifierStringToBeModerated], ); const [cachedEntry] = useAtom(entryAtom); - // 2. Select ONLY this URI's processing state + // 2. Select ONLY this opaqueIdentifierStringToBeModerated's processing state const isProcessingAtom = useMemo( - () => selectAtom(processingUriSetAtom, (set) => set.has(uri)), - [uri], + () => selectAtom(processingUriSetAtom, (set) => set.has(opaqueIdentifierStringToBeModerated)), + [opaqueIdentifierStringToBeModerated], ); const [isProcessing] = useAtom(isProcessingAtom); + // eslint-disable-next-line react-hooks/purity const now = Date.now(); const exists = cachedEntry !== undefined; const isStale = exists && now - cachedEntry.timestamp > CACHE_TIMEOUT_MS; @@ -36,12 +40,12 @@ export const useModeration = (uri: string) => { // Queue it setQueue((prev) => { - if (prev.has(uri)) return prev; + if (prev.has(opaqueIdentifierStringToBeModerated)) return prev; const next = new Set(prev); - next.add(uri); + next.add(opaqueIdentifierStringToBeModerated); return next; }); - }, [uri, exists, isStale, isProcessing, setQueue]); + }, [opaqueIdentifierStringToBeModerated, exists, isStale, isProcessing, setQueue]); return { // Show loading ONLY if we have absolutely no data (first load) diff --git a/src/main.tsx b/src/main.tsx index 8f35436..82f44ea 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -30,6 +30,11 @@ const localStoragePersister = createSyncStoragePersister({ persistQueryClient({ queryClient, persister: localStoragePersister, + dehydrateOptions: { + shouldDehydrateQuery: (query) => { + return !query.queryKey.includes('__volatile') + }, + }, }); // Create a new router instance diff --git a/src/providers/AutoLabelProvider.tsx b/src/providers/AutoLabelProvider.tsx new file mode 100644 index 0000000..418fd23 --- /dev/null +++ b/src/providers/AutoLabelProvider.tsx @@ -0,0 +1,341 @@ +import * as ATPAPI from "@atproto/api"; +import { isContentLabelPref, isLabelersPref } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; +import { useQueries, useQueryClient } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { Dialog } from "radix-ui"; +import React, { createContext, useContext, useMemo } from "react"; + +import { FORCED_LABELER_DIDS } from "~/../policy"; +import { useAuth } from "~/providers/UnifiedAuthProvider"; +import { NotificationItem } from "~/routes/notifications"; +import { disabledLabelersAtom, slingshotURLAtom } from "~/utils/atoms"; +import { useQueryIdentity, useQueryPreferences } from "~/utils/useQuery"; + +// higher prio overwrites lower prio +export const LABEL_PRIO_DEFAULT = 0 +export const LABEL_PRIO_USER = 5 +// export const LABEL_PRIO_FORCED = 20 // Not used in pref merging + +export interface HydratedLabelValueDefinition extends ATPAPI.ComAtprotoLabelDefs.LabelValueDefinition { + pref: ATPAPI.ComAtprotoLabelDefs.LabelValueDefinition["defaultSetting"] +} + +export type labelpref = { + did: string, + label: string, + visibility: ATPAPI.AppBskyActorDefs.ContentLabelPref["visibility"], + priority: number +} + +type labeldefpref = { + did: string, + labeldefs?: ATPAPI.ComAtprotoLabelDefs.LabelValueDefinition[], + err?: string +} + +type hydratedlabeldefpref = { + did: string, + labeldefs?: HydratedLabelValueDefinition[], + err?: string +} + +type AutoLabelConfig = { + activeLabelerDids: string[]; + mergedPrefContentLabels: labelpref[]; + hydratedLabelDefs: Map; + isLoading: boolean; + isError: boolean; + sendError: (error: LabelerError) => void; +}; + +export type LabelerError = { did: string; message: string }; + +const AutoLabelContext = createContext(undefined); + +export function useAutoLabelConfig(): AutoLabelConfig { + const context = useContext(AutoLabelContext); + if (!context) { + throw new Error('useAutoLabelConfig must be used within an AutoLabelProvider'); + } + return context; +} + +export function AutoLabelProvider({ children }: { children: React.ReactNode }) { + const { agent, status } = useAuth(); + const [slingshoturl] = useAtom(slingshotURLAtom); + const queryClient = useQueryClient(); + + const [disabledLabelers, setDisabledLabelers] = useAtom(disabledLabelersAtom); + + // Modal state + const [errorModalOpen, setErrorModalOpen] = React.useState(false); + const [labelerErrors, setLabelerErrors] = React.useState([]); + const erroredLabelerDids = new Set( + labelerErrors.map(e => e.did) + ); + + const sendError = React.useCallback((error: LabelerError) => { + setLabelerErrors((prev) => { + // Avoid duplicates + if (prev.find(e => e.did === error.did)) return prev; + return [...prev, error]; + }); + setErrorModalOpen(true); + }, []); + + + const isUnauthed = status === "signedOut" || !agent; + + // 1. Get User Identity & Preferences + const { data: identity } = useQueryIdentity(agent?.did); + const { + data: prefs, + isLoading: prefsLoading, + isError: prefsError + } = useQueryPreferences({ + agent: agent ?? undefined, + pdsUrl: identity?.pds, + }); + + // 2. Identify Labeler DIDs & User Preferences (Use useMemo for stability) + const userPrefLabelerDids = useMemo(() => + prefs?.preferences?.find(isLabelersPref)?.labelers.map(l => l.did) ?? [] + , [prefs]); + + const userPrefContentLabelsRaw = useMemo(() => + prefs?.preferences?.filter(isContentLabelPref) ?? [] + , [prefs]); + + const userPrefContentLabels: labelpref[] = useMemo(() => userPrefContentLabelsRaw.map((pref) => { + const res: labelpref = { + did: pref.labelerDid || "global", + label: pref.label, + visibility: pref.visibility, + priority: LABEL_PRIO_USER + } + return res + }), [userPrefContentLabelsRaw]); + + // 3. Force Bsky DID + User DIDs + const userPrefLabelerDidsFiltered = React.useMemo( + () => userPrefLabelerDids.filter(did => !disabledLabelers.includes(did)), + [userPrefLabelerDids, disabledLabelers] + ); + + const activeLabelerDids = useMemo(() => Array.from( + new Set([...FORCED_LABELER_DIDS, ...userPrefLabelerDidsFiltered]) + ), [userPrefLabelerDidsFiltered]); + + // 4. Parallel fetch Service Records (The expensive part) + const labelerServiceQueries = useQueries({ + queries: activeLabelerDids.map((did: string) => ({ + queryKey: ["labelerServiceDefaultstestwhatever", did], + queryFn: async () => { + const host = slingshoturl; + const response = await fetch( + `https://${host}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.labeler.service&rkey=self`, + ); + if (!response.ok) throw new Error("Failed to fetch labeler service"); + try { + const res = await response.json() as { + uri: string; + cid: string; + value: ATPAPI.AppBskyLabelerService.Record; + }; + const labeldefs = res.value.policies.labelValueDefinitions + return { + did: did, + labeldefs: labeldefs + } as labeldefpref + } catch { + sendError({ did, message: ".json()" }); + return { + did: did, + err: ".json()" + } as labeldefpref + } + }, + staleTime: 1000 * 60 * 60, // Cache for 1 hour + })), + }); + + const defaultPrefContentLabels: labelpref[] = useMemo(() => + labelerServiceQueries.flatMap(labeler => { + if (labeler.error || labeler.data?.err || !labeler.data) { + return [] + } + + const labelerDid = labeler.data.did + + return labeler.data.labeldefs?.map(label => ({ + did: labelerDid, + label: label.identifier, + visibility: label.defaultSetting, + priority: LABEL_PRIO_DEFAULT, + })) ?? [] + }) + , [labelerServiceQueries]); + + // 5. Merge Default and User Preferences + const mergedPrefContentLabels: labelpref[] = useMemo(() => { + const allrawPrefContentLabels = [...defaultPrefContentLabels, ...userPrefContentLabels] + + return Object.values( + allrawPrefContentLabels.reduce( + (acc, pref) => { + const key = `${pref.did}::${pref.label}` + const existing = acc[key] + if (!existing || pref.priority > existing.priority) { + acc[key] = pref + } + return acc + }, + {} as Record + ) + ) + }, [defaultPrefContentLabels, userPrefContentLabels]); + + const hydratedLabelDefs = convertLabelDefsToMap( + labelerServiceQueries + .map(item => item.data) + .filter((data): data is labeldefpref => data !== undefined), + mergedPrefContentLabels + ) + + const labelerServiceLoading = labelerServiceQueries.some(q => q.isLoading); + const labelerServiceError = labelerServiceQueries.some(q => q.isError); + + const isLoading = prefsLoading || labelerServiceLoading; + const isError = (!isUnauthed && prefsError) || labelerServiceError; + + const value = useMemo(() => ({ + activeLabelerDids, + mergedPrefContentLabels, + hydratedLabelDefs, + isLoading, + isError, + sendError + }), [activeLabelerDids, mergedPrefContentLabels, hydratedLabelDefs, isLoading, isError, sendError]); + + // The provider must wrap the application root where useAutoLabels is called + return ( + <> + {children} + {errorModalOpen && ( + + + +
+
+ Labeler Errors + These labelers have returned an error.
You can disable them to continue using the app.
+
+
+ {labelerErrors.map(e => ( + //
  • + // {e.did}: {e.message} + // + //
  • + + ))} +
    +
    + {/* */} + +
    +
    +
    +
    + )} + + ); +} + +function convertLabelDefsToMap( + labelDefPrefs: labeldefpref[], + mergedPrefContentLabels: labelpref[] +): Map { + + const labelMap = new Map(); + + for (const pref of labelDefPrefs) { + const did = pref.did; + + // Only process if labeldefs array exists + if (pref.labeldefs && pref.labeldefs.length > 0) { + for (const def of pref.labeldefs) { + // Construct the key: did + "::" + identifier + + const key = `${did}::${def.identifier}`; + + const prefdlabelvis = mergedPrefContentLabels.find(i => i.did === did && i.label === def.identifier)?.visibility + const hydrateddef: HydratedLabelValueDefinition = { + ...def, + pref: collapsePrefs(prefdlabelvis ? prefdlabelvis : def.defaultSetting) + }; + + // Set the key and the LabelValueDefinition as the value + labelMap.set(key, hydrateddef); + } + } + } + + return labelMap; +} + +function collapsePrefs(def: "ignore" | "warn" | "hide" | "show" | (string & {})): "ignore" | "warn" | "hide" { + if (def === "ignore") return "ignore" + if (def === "warn") return "warn" + if (def === "hide") return "hide" + if (def === "show") return "ignore" + if (def === "alert") return "warn" + if (def === "inform") return "warn" + if (def === "none") return "ignore" + return "ignore" +} \ No newline at end of file diff --git a/src/providers/PollMutationQueueProvider.tsx b/src/providers/PollMutationQueueProvider.tsx index 023f7d8..694ccc4 100644 --- a/src/providers/PollMutationQueueProvider.tsx +++ b/src/providers/PollMutationQueueProvider.tsx @@ -299,7 +299,7 @@ function usePollSelfVotes(pollUri: string) { const { agent } = useAuth(); const agentDid = agent?.did; - const userVotesA = useGetOneToOneState( + const { uris: userVotesA } = useGetOneToOneState( agentDid ? { target: pollUri, @@ -309,7 +309,7 @@ function usePollSelfVotes(pollUri: string) { } : undefined, ); - const userVotesB = useGetOneToOneState( + const { uris: userVotesB } = useGetOneToOneState( agentDid ? { target: pollUri, @@ -319,7 +319,7 @@ function usePollSelfVotes(pollUri: string) { } : undefined, ); - const userVotesC = useGetOneToOneState( + const { uris: userVotesC } = useGetOneToOneState( agentDid ? { target: pollUri, @@ -329,7 +329,7 @@ function usePollSelfVotes(pollUri: string) { } : undefined, ); - const userVotesD = useGetOneToOneState( + const { uris: userVotesD } = useGetOneToOneState( agentDid ? { target: pollUri, diff --git a/src/routes/moderation.tsx b/src/routes/moderation.tsx index ad48466..165bf03 100644 --- a/src/routes/moderation.tsx +++ b/src/routes/moderation.tsx @@ -12,6 +12,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { useAtom } from "jotai"; import { Switch } from "radix-ui"; +import { FORCED_LABELER_DIDS } from "~/../policy"; import { Header } from "~/components/Header"; import { useModeration } from "~/hooks/useModeration"; import { useAuth } from "~/providers/UnifiedAuthProvider"; @@ -22,6 +23,53 @@ import { renderSnack } from "./__root"; import { NotificationItem } from "./notifications"; import { SettingHeading } from "./settings"; +const FOUR_GLOBAL_LABELS = [ + "porn", + "sexual", + "graphic-media", + "nudity", +] as const; + +const FOUR_GLOBAL_LABELS_TEXT: Record = { + porn: { + title: "Adult Content", + desc: "Explicit sexual images." + }, + sexual: { + title: "Sexually Suggestive", + desc: "Does not include nudity." + }, + "graphic-media": { + title: "Graphic Media", + desc: "Explicit or potentially disturbing media." + }, + nudity: { + title: "Non-sexual Nudity", + desc: "E.g. artistic nudes." + } +}; + +type FourGlobalLabel = typeof FOUR_GLOBAL_LABELS[number]; + +// todo please make this part of labeler resolution process / policies.ts +const DEFAULT_FOUR_GLOBAL_PREFS: Record = { + porn: "ignore", + sexual: "ignore", + "graphic-media": "ignore", + nudity: "ignore", +}; + +function normalizeFourGlobalPrefs( + prefs: Record, +): Record { + return Object.fromEntries( + FOUR_GLOBAL_LABELS.map((label) => [ + label, + prefs[label] ?? DEFAULT_FOUR_GLOBAL_PREFS[label], + ]), + ) as Record; +} + export const Route = createFileRoute("/moderation")({ component: RouteComponent, }); @@ -47,10 +95,16 @@ function RouteComponent() { const parsedPref = parsePreferences(rawprefs); + const hostmandate = FORCED_LABELER_DIDS + + const fourGlobalPrefs = normalizeFourGlobalPrefs(parsedPref?.contentLabelPrefs ?? {}) + + console.log(parsedPref?.labelers?.map(l => `&l=${l}`).join("") ?? "") + return (
    { if (window.history.length > 1) { window.history.back(); @@ -77,7 +131,18 @@ function RouteComponent() { - Verification settings

    */} - + +
    + TODO: hello please add the entire bsky mod tools set including but not limited to: + Interaction settings, + Muted Words & Tags, + Moderation lists, + Muted accounts, + Blocked accounts, + Verification settings + +
    +
    ); } +export function LabelerToggleLocalEnablementButton({ + labeler, +}: { + labeler: string; +}) { + const [disabledLabelers, setDisabledLabelers] = useAtom(disabledLabelersAtom); + const labelerEnabledState = !disabledLabelers.includes(labeler) + const isMandatory = FORCED_LABELER_DIDS.includes(labeler) + + function toggleLocalLabelerEnabledState() { + if (labeler) { + if (labelerEnabledState) { + console.log("button clicked disabled it") + setDisabledLabelers([...disabledLabelers, labeler]) + } else { + console.log("button clicked enabled it") + setDisabledLabelers(disabledLabelers.filter(v => v !== labeler)) + } + } + } + + if (isMandatory) { + return ( + <> + mandated by host + + /** + * relative inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium transition-all + bg-surface-container-low text-on-surface-variant border border-outline + hover:bg-primary/30 active:scale-[0.97] + dark:border-outline-variant + + */ + ) + } + + return ( + { + e.stopPropagation(); + toggleLocalLabelerEnabledState(); + }} + onCheckedChange={() => { + // e.stopPropagation(); + // toggleLocalLabelerEnabledState(); + }} + className="m3switch root shrink-0" + > + + + // + ); +} + + export const EmptyState = ({ text }: { text: string }) => (
    {text} diff --git a/src/routes/profile.$did/index.tsx b/src/routes/profile.$did/index.tsx index 515eaca..5743b0a 100644 --- a/src/routes/profile.$did/index.tsx +++ b/src/routes/profile.$did/index.tsx @@ -6,6 +6,7 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useAtom } from "jotai"; import React, { type ReactNode, useEffect, useState } from "react"; +import { FORCE_HIDE_LABELS, FORCE_HIDE_LABELS_WHITELISTED_SOURCE } from "~/../policy"; import defaultpfp from "~/../public/defaultpfp.png"; import { Header } from "~/components/Header"; import { @@ -14,10 +15,13 @@ import { } from "~/components/ReusableTabRoute"; import { SmallAuthorLabelBadge, + SmallAuthorLabelBadgeInner, UniversalPostRendererATURILoader, } from "~/components/UniversalPostRenderer"; import { renderTextWithFacets } from "~/components/UtilityFunctions"; -import { useModeration } from "~/hooks/useModeration"; +import { getGetHydratedLabelDefs, useAutoLabels } from "~/hooks/useAutoLabels"; +import type { HydratedLabelValueDefinition } from "~/providers/AutoLabelProvider"; +//import { useModeration } from "~/hooks/useModeration"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { enableBitesAtom, imgCDNAtom, profileChipsAtom } from "~/utils/atoms"; import { @@ -34,15 +38,22 @@ import { useQueryIdentity, useQueryProfile, } from "~/utils/useQuery"; +// README this weird manual import is required because icon auto imports in this file (and some other files) are broken +// for some reason which i dont know. +import IconMdiMoreHoriz from "~icons/mdi/more-horiz.jsx"; import IconMdiShieldOutline from "~icons/mdi/shield-outline.jsx"; import { renderSnack } from "../__root"; -import { Chip } from "../notifications"; +import { Chip, NotificationItem } from "../notifications"; export const Route = createFileRoute("/profile/$did/")({ component: ProfileComponent, }); +export interface LabelWithHydratedLocaleName extends ATPAPI.ComAtprotoLabelDefs.Label { + name: string +} + function ProfileComponent() { // booo bad this is not always the did it might be a handle, use identity.did instead const { did } = Route.useParams(); @@ -55,15 +66,97 @@ function ProfileComponent() { error: identityError, } = useQueryIdentity(did); + const agentDid = agent?.did; + const authorDid = identity?.did; - const { isLoading: authorModLoading, labels: authorLabels } = useModeration( - did, + const userBlocksAuthor = useGetOneToOneState( + agentDid && authorDid + ? { + target: authorDid, + user: agentDid, + collection: "app.bsky.graph.block", + path: ".subject", + } + : undefined, ); + const authorBlocksUser = useGetOneToOneState( + agentDid && authorDid + ? { + target: agentDid, + user: authorDid, + collection: "app.bsky.graph.block", + path: ".subject", + } + : undefined, + ); + + const redactWhileLoadingBlock = userBlocksAuthor.isLoading || authorBlocksUser.isLoading + const redactFinalBlock = userBlocksAuthor.uris.length > 0 || authorBlocksUser.uris.length > 0 + + const subjects = identity ? [ + identity.did, + `at://${identity.did}/app.bsky.actor.profile/self`, + ] : [] + + const { + results: labelResults, + hydratedLabelDefs, + } = useAutoLabels({ + subjects, + type: "post", // or whatever you’re keying on for now + }) + + const ghld = getGetHydratedLabelDefs(hydratedLabelDefs) + const accountResult = labelResults.get(identity?.did || did) + const profileResult = labelResults.get( + `at://${identity?.did || did}/app.bsky.actor.profile/self`, + ) + + const accountLabelVerdict = accountResult?.labelVerdict ?? "unknown" + const authorLabels = accountResult?.labels ?? [] + + const profileLabelVerdict = profileResult?.labelVerdict ?? "unknown" + const profileLabels = profileResult?.labels ?? [] + + const authorModUnknown = accountLabelVerdict === "unknown"; + const profileModUnknown = profileLabelVerdict === "unknown"; + + const authorModLoading = accountLabelVerdict === "loading"; + const profileModLoading = profileLabelVerdict === "loading"; + + const authorModError = accountLabelVerdict === "error"; + const profileModError = profileLabelVerdict === "error"; + + const strictModerationUnknown = authorModUnknown || profileModUnknown + const strictModerationLoading = authorModLoading || profileModLoading || redactWhileLoadingBlock + const strictModerationError = authorModError || profileModError + + const strictModerationDontShow = strictModerationUnknown || strictModerationLoading || strictModerationError || redactFinalBlock + + const verdictDebugString = `accountLabelVerdict: ${accountLabelVerdict}, profileLabelVerdict: ${profileLabelVerdict}` + const hideAuthorLabels = authorLabels.filter( - label => label.preference === 'hide' + (label) => ghld(label.src,label.val)?.pref === "hide", ); const warnAuthorLabels = authorLabels.filter( - label => label.preference === 'warn' + (label) => ghld(label.src,label.val)?.severity === "warn" && ghld(label.src,label.val)?.pref === "warn", + ); + const informAuthorLabels: LabelWithHydratedLocaleName[] = authorLabels.flatMap( + (label) => { + if (ghld(label.src,label.val)?.severity === "inform" && ghld(label.src,label.val)?.pref === "warn") { + return [{ + ...label, + name: getLocaleLabel(ghld(label.src,label.val))?.name || label.val + }] + } + return [] + }, + ); + const hideProfileLabels = profileLabels.filter( + (label) => ghld(label.src,label.val)?.pref === "hide", + ); + const warnProfileLabels = profileLabels.filter( + (label) => ghld(label.src,label.val)?.pref === "warn", ); // i was gonna check the did doc but useQueryIdentity doesnt return that info (slingshot minidoc) @@ -123,10 +216,101 @@ function ProfileComponent() { const followercount = resultwhateversure?.data?.total; + const isForceHidden = [...[...authorLabels].filter((label) => { + return ( + FORCE_HIDE_LABELS.has(label.val) && + FORCE_HIDE_LABELS_WHITELISTED_SOURCE.has(label.src) + ); + }), ...hideAuthorLabels]; + + // // todo remove this replace it with blurs + // if (strictModerationLoading) { + // return ( + //
    + //
    { + // if (window.history.length > 1) { + // window.history.back(); + // } else { + // window.location.assign("/"); + // } + // }} + // bottomBorderDisabled={true} + // /> + //
    + // DEBUG LOADING LABELS + // {identity?.did || did} + // {verdictDebugString} + //
    + //
    + // ); + // } + + console.log("HLLO HLLO HisForceHidden" + did + isForceHidden + authorLabels) + if (isForceHidden.length > 0 || redactFinalBlock) { + // todo pretify this please + return ( +
    +
    { + if (window.history.length > 1) { + window.history.back(); + } else { + window.location.assign("/"); + } + }} + bottomBorderDisabled={true} + /> +
    +
    + {/* todo: separate host-mandated labelers from user-picked labelers. + currently assumes all labelers that hides profiles are host-mandated */} +

    This profile is hidden due to these reason(s):

    + {isForceHidden.map((item) => { + return ( + // todo this sucks + + ) + })} + {/* todo add unblock button duhhhhhh */} + {/* {userBlocksAuthor.uris.length > 0 && (
    User Blocked by You
    )} */} + {userBlocksAuthor.uris.length > 0 && ( + + )} + {/* {authorBlocksUser.uris.length > 0 && (
    User Blocking You
    )} */} + {authorBlocksUser.uris.length > 0 && ( + + )} +
    + + Moderation Settings + + + Host instance's policies + +
    +
    +
    +
    + ) + } return (
    { if (window.history.length > 1) { window.history.back(); @@ -161,7 +345,8 @@ function ProfileComponent() {
    - {!getAvatarUrl(profile) && isLabeler ? ( + {strictModerationLoading ? ( +
    +
    +
    + ) : !getAvatarUrl(profile) && isLabeler ? (
    @@ -194,7 +386,7 @@ function ProfileComponent() { */}
    {/* Info Card */} -
    +
    {displayName}
    @@ -228,7 +420,7 @@ function ProfileComponent() { Follows
    - {description && ( + {!strictModerationLoading && description && (
    {/* {description} */} @@ -257,8 +449,9 @@ function ProfileComponent() { : (
    - {warnAuthorLabels.map((label, index) => ( - + {/* authorLabels{JSON.stringify(authorLabels,null,2)} */} + {informAuthorLabels.map((label, index) => ( + ))}
    ) @@ -968,7 +1161,7 @@ export function FollowButton({ queryClient: queryClient, }); }} - className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" + className=" shrink-0 font-medium rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" > Follow @@ -983,7 +1176,7 @@ export function FollowButton({ queryClient: queryClient, }); }} - className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" + className=" shrink-0 font-medium rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" > Unfollow @@ -991,7 +1184,7 @@ export function FollowButton({ ) : ( @@ -1083,7 +1276,7 @@ export function Mutual({ targetdidorhandle }: { targetdidorhandle: string }) { const { agent } = useAuth(); const { data: identity } = useQueryIdentity(targetdidorhandle); - const theyFollowYouRes = useGetOneToOneState( + const { uris: theyFollowYouRes } = useGetOneToOneState( agent?.did ? { target: agent?.did, @@ -1177,3 +1370,12 @@ export function RichTextRenderer({ description }: { description: string }) { return <>{richDescription}; } + +export function getLocaleLabel(hlvd?: HydratedLabelValueDefinition) { + if (!hlvd) return undefined + const userLang = "en"; + const locale = hlvd.locales.find((l) => l.lang === userLang) + || hlvd.locales.find((l) => l.lang === "en") + || hlvd.locales[0]; + return locale +} \ No newline at end of file diff --git a/src/styles/app.css b/src/styles/app.css index 9305d07..aa82993 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -374,4 +374,19 @@ body[data-scroll-locked]{ transform: translate(calc(-1 * (var(--root-size) / 2) + 50%),0) scale(0.88); } } +} + +/* todo: check this. might break everything */ +html, :host { + line-height: normal; +} + +:root { + /* bg-gray-300 dark:bg-gray-600 */ + --color-placeholder: var(--color-gray-300); +} +@media (prefers-color-scheme: dark) { + :root { + --color-placeholder: var(--color-gray-600); + } } \ No newline at end of file diff --git a/src/utils/atoms.ts b/src/utils/atoms.ts index ff61977..3967b5e 100644 --- a/src/utils/atoms.ts +++ b/src/utils/atoms.ts @@ -2,6 +2,7 @@ import { atom, createStore, useAtomValue } from "jotai"; import { atomWithStorage } from "jotai/utils"; import { useEffect } from "react"; +import { HOST_DEFAULT_HUE } from "~/../policy"; import { type ProfilePostsFilter } from "~/routes/profile.$did"; export const store = createStore(); @@ -98,7 +99,12 @@ export const lycanURLAtom = atomWithStorage( defaultLycanURL ); -export const defaulthue = 28; +export const disabledLabelersAtom = atomWithStorage( + "disabledLabelers", + [] +); + +export const defaulthue = HOST_DEFAULT_HUE; export const hueAtom = atomWithStorage("hue", defaulthue); export const isAtTopAtom = atom(true); diff --git a/src/utils/followState.ts b/src/utils/followState.ts index dbdd2ff..573f7aa 100644 --- a/src/utils/followState.ts +++ b/src/utils/followState.ts @@ -1,6 +1,6 @@ import { type Agent, AtUri } from "@atproto/api"; import { TID } from "@atproto/common-web"; -import type { QueryClient } from "@tanstack/react-query"; +import type { QueryClient, UseQueryResult } from "@tanstack/react-query"; import { type linksRecordsResponse, useQueryConstellation } from "./useQuery"; @@ -134,8 +134,12 @@ export function useGetOneToOneState(params?: { user: string; collection: string; path: string; -}): string[] | undefined { - const { data: arbitrarydata } = useQueryConstellation( +}): { + uris: string[], + isLoading: boolean; + isError: boolean; +} { + const whatever = useQueryConstellation( params && params.user ? { method: "/links", @@ -151,15 +155,28 @@ export function useGetOneToOneState(params?: { } : { method: "undefined", target: "whatever" }, // overloading sucks so much - ) as { data: linksRecordsResponse | undefined }; - if (!params || !params.user) return undefined; + ) as UseQueryResult; + if (!params || !params.user) return { + uris: [], + isError: true, + isLoading: false, + }; + const arbitrarydata = whatever.data; const data = arbitrarydata?.linking_records.slice(0, 50) ?? []; if (data.length > 0) { - return data.map((linksRecord) => { - return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`; - }); + return { + uris: data.map((linksRecord) => { + return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`; + }), + isError: false, + isLoading: false, + }; } - return undefined; + return { + uris: [], + isError: whatever.isLoading, + isLoading: whatever.isError, + }; } diff --git a/src/utils/useHydrated.ts b/src/utils/useHydrated.ts index b2635d1..9d435cf 100644 --- a/src/utils/useHydrated.ts +++ b/src/utils/useHydrated.ts @@ -84,8 +84,20 @@ function hydrateEmbedRecord( quotedIdentity: QueryResultData, cdn: string ): $Typed | undefined { + // if (!quotedPost || !quotedProfile || !quotedIdentity) { + // return undefined; + // } if (!quotedPost || !quotedProfile || !quotedIdentity) { - return undefined; + const failureViewRecord: $Typed = asTyped({ + $type: "app.bsky.embed.record#viewNotFound" as const, + uri: embed.record.uri, + notFound: true as const, + }) + + return asTyped({ + $type: "app.bsky.embed.record#view" as const, + record: failureViewRecord, + }); } const author: $Typed = asTyped({ @@ -194,15 +206,14 @@ export function useHydratedEmbed( const hydratedEmbed: HydratedEmbedView | undefined = (() => { if (!embed || !postAuthorDid) return undefined; - if ( - isRecordType && - (!usequerypostresults?.data || - !quotedProfile || - !queryidentityresult?.data) - ) { - return undefined; - } - + // if ( + // isRecordType && + // (!usequerypostresults?.data || + // !quotedProfile || + // !queryidentityresult?.data) + // ) { + // return undefined; + // } try { if (AppBskyEmbedImages.isMain(embed)) { return hydrateEmbedImages(embed, postAuthorDid, imgcdn); diff --git a/src/utils/useQuery.ts b/src/utils/useQuery.ts index b846539..c0b7130 100644 --- a/src/utils/useQuery.ts +++ b/src/utils/useQuery.ts @@ -1,14 +1,24 @@ import * as ATPAPI from "@atproto/api"; import { infiniteQueryOptions, + QueryClient, type QueryFunctionContext, queryOptions, useInfiniteQuery, + useQueries, useQuery, type UseQueryResult, } from "@tanstack/react-query"; +import { create, windowScheduler } from "@yornaath/batshit"; import { useAtom } from "jotai"; +import { useMemo } from "react"; +import { HOST_LABELMERGE } from "~/../policy"; +import type { + Error as LabelMergeQueryLabelsOutputSchemaError, + OutputSchema as LabelMergeQueryLabelsOutputSchema, + QueryParams as LabelMergeQueryLabelsQueryParams, +} from "~/api/labelmerge/types/app/reddwarf/labelmerge/queryLabels"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { constellationURLAtom, lycanURLAtom, slingshotURLAtom } from "./atoms"; @@ -387,11 +397,12 @@ export function useQueryConstellation(query?: { | undefined { //if (!query) return; const [constellationurl] = useAtom(constellationURLAtom); - return useQuery( + const res = useQuery( constructConstellationQuery( query && { constellation: constellationurl, ...query }, ), ); + return res } export type linksRecord = { @@ -532,7 +543,8 @@ export function constructPreferencesQuery( const url = `${pdsUrl}/xrpc/app.bsky.actor.getPreferences`; const res = await agent.fetchHandler(url, { method: "GET" }); if (!res.ok) throw new Error("Failed to fetch preferences"); - return res.json(); + // todo, i just gave it real types (atproto api types) so theres gonna be a bunch of errors so pls fix thx + return (await res.json()) as ATPAPI.AppBskyActorGetPreferences.OutputSchema; }, }); } @@ -975,3 +987,400 @@ export function constructLycanSearchQuery(options: { getNextPageParam: (lastPage) => lastPage?.cursor as null | undefined, }); } + +// HOST_LABELMERGE + +export async function innerLabelMergeQueryFn(options: LabelMergeQueryLabelsQueryParams): Promise { + const { s, l, strict } = options; + const params = new URLSearchParams(); + s.forEach((v) => params.append("s", v)); + l.forEach((v) => params.append("l", v)); + if (strict) { + params.append("strict", "true"); + } + const qs = params.toString(); + + const url = + `${HOST_LABELMERGE}/xrpc/app.reddwarf.labelmerge.queryLabels?` + qs; + console.log("LabelMerge URL", url); + const res = await fetch(url); + if (!res.ok) + throw new Error(`Labelmerge fetch failed: ${res.statusText}`); + return (await res.json()) as LabelMergeQueryLabelsOutputSchema; +} + +export function constructLabelMergeQuery( + options: LabelMergeQueryLabelsQueryParams, +) { + const { s, l, strict } = options; + + return queryOptions({ + queryKey: [ + "LabelMergeQueryLabelsQuery", + [...s].sort().join(","), + [...l].sort().join(","), + strict, + ], + + enabled: + Array.isArray(s) && s.length > 0 && Array.isArray(l) && l.length > 0, + + queryFn: ()=>innerLabelMergeQueryFn(options), + }); +} +export function useQueryLabelMerge(options: LabelMergeQueryLabelsQueryParams) { + return useQuery(constructLabelMergeQuery(options)); +} + +export type PartialLabelQuery = { + s: string; + l: string[]; +}; +export type SingularLabelQuery = { + s: string; + l: string; +}; + +export type SingularLabelResult = { + labels?: ATPAPI.ComAtprotoLabelDefs.Label; + error?: LabelMergeQueryLabelsOutputSchemaError; +}; //ATPAPI.ComAtprotoLabelDefs.Label | LabelMergeQueryLabelsOutputSchemaError | null +export type PartialLabelResult = { + subject: string; + labels?: ATPAPI.ComAtprotoLabelDefs.Label[]; + error?: LabelMergeQueryLabelsOutputSchemaError[]; +}; + +function flattenLabelQueries( + partials: PartialLabelQuery[], +): SingularLabelQuery[] { + return partials.flatMap((p) => p.l.map((label) => ({ s: p.s, l: label }))); +} + +// batShitQueryClient +export const unpersistedQueryClient = new QueryClient(/*{ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, // 5 minutes + //cacheTime: 10 * 60 * 1000, // 10 minutes + gcTime: 5 * 60 * 1000, + }, + }, +}*/); + + +interface MicroSingleResult { + l: string, + t: number, +} + +const labelmerge = create( + /*[], SingularLabelQuery>*/ { + // The fetcher resolves the list of queries(here just a list of user ids as number) to one single api call. + fetcher: async (slqa: SingularLabelQuery[]) => { + // Use a shared QueryClient if possible; creating a new one per fetch is usually not needed + + // Deduplicate, but don’t sort + const sarr = Array.from(new Set(slqa.map((slq) => slq.s))); + const larr = Array.from(new Set(slqa.map((slq) => slq.l))); + + //const result = await batShitQueryClient.fetchQuery( + // constructLabelMergeQuery({ s: sarr, l: larr }), + //); + const result = await innerLabelMergeQueryFn({s:sarr, l: larr}) + //const qfn = constructLabelMergeQuery({ s: sarr, l: larr }).queryFn + //const result = await (qfn ? qfn() : ()=>{}) + if (!result) return []; + + // Build maps for quick lookup + const errmap = new Map(); + const resmap = new Map(); + + result.error?.forEach((err) => errmap.set(err.s, err)); + result.labels?.forEach((label) => resmap.set(`${label.src}::${label.uri}`, label)); + + // Map back to the original queries + const output: Record[] = slqa.map((slq) => { + const key = `${slq.l}::${slq.s}`; // or just slq.l if you prefer + + const err = errmap.get(slq.l); + const label = resmap.get(key); + + if (err) return { [key]: { error: err } }; + if (label) return { [key]: { labels: label } }; + + // if result is neither, it means the subject is free of labels + return { + [key]: { labels: undefined} + }; + // idiot + // return { + // [key]: { error: { + // s: slq.l, + // e: `!internal-bslm-unknown: ${slq.s}` + // }} + // }; + }); + + return output; + }, + // when we call users.fetch, this will resolve the correct user using the field `id` + resolver: (rslra, slq) => { + if (rslra.length < 1) { + return undefined; + } + // const result: SingularLabelResult | undefined = slra.find((slr, i) => { + // // find if error first + // const error = slr.error; + // const label = slr.labels; + // if (error) { + // if (slq.l === error.s) { + // return slq; + // } + // } else if (label) { + // // if not error + // if (slq.l === label.src && slq.s === label.uri) { + // return slq; + // } + // // else unhandled not found + // } else { + // return undefined; + // } + // return undefined; + // }); + const outputMap: Record = Object.assign({}, ...rslra) + const key = `${slq.l}::${slq.s}`; // or just slq.l if you prefer + const result: SingularLabelResult | undefined = outputMap[key] + return result; + }, + scheduler: windowScheduler(10 * 100), // 1 second + }, +); + +// const labelmergepartial = create/**/({ +// // The fetcher resolves the list of queries(here just a list of user ids as number) to one single api call. +// fetcher: async (plqa: PartialLabelQuery[]) => { +// const singulars = flattenLabelQueries(plqa); // SingularLabelQuery[] +// const singularResults = await Promise.all(singulars.map(q => labelmerge.fetch(q))); + +// // Now we need to **group singularResults back by the original PartialLabelQuery.s** +// // so that each PartialLabelQuery gets a PartialLabelResult (LabelMergeQueryLabelsOutputSchema) +// const grouped: Record = {}; +// singulars.forEach((q, i) => { +// if (!grouped[q.s]) grouped[q.s] = []; +// if (singularResults[i]) { +// grouped[q.s].push(singularResults[i]); +// } else { +// grouped[q.s].push({}); +// } +// }); + +// // Convert grouped record to your PartialLabelResult format +// const result: PartialLabelResult[] = Object.entries(grouped).map(([s, labels]) => { +// const cleanLabels = labels +// .map(l => l?.labels) +// .filter((l): l is ATPAPI.ComAtprotoLabelDefs.Label => !!l?.val) +// const cleanErrors = labels +// .map(l => l?.error) +// .filter((e): e is LabelMergeQueryLabelsOutputSchemaError => !!e?.s) +// return { +// subject: s, +// labels: cleanLabels, +// error: cleanErrors ? cleanErrors : undefined +// } +// }); +// return result +// }, +// resolver: (plra, plq) => { +// if (plra.length < 1) { +// return undefined +// } +// const subject = plq.s; +// const result: PartialLabelResult | undefined = plra.find((plr,i)=>{ +// return plr.subject === subject; +// }) +// return result +// }, +// // this will batch all calls to users.fetch that are made within 10 milliseconds. +// scheduler: windowScheduler(10*100) // 1 second +// }) + +// export const useQueryLabel = (s: string, la: string[]) => { +// return useQuery({ +// queryKey: ["useQueryLabel (single) sla", s, la], +// queryFn: async () => { +// return labelmergepartial.fetch({ s, l: la }) +// }, +// }) +// } + +/** + * todo: + * - [x] switch from useQuery to normal custom hook and switch from Promise.All to useQueries + * - [ ] Move neg normalization to the batshit unmerging, and make the cache labels only (pre sorted) + * - [ ] Also do signature verification on the constructSingularQuery + */ + +// also the cache hits from constructSingularLabelQuery is not being sent fast because +// it waits for all of them first +// but also if we send it fast would it cause even worse synchronous traffic jams downstream ? +// export const useQueryLabels = (subjects: string[], labelers: string[]) => { +// const queryClient = useQueryClient(); +// return useQuery({ +// queryKey: ["useQueryLabelFull", subjects, labelers], +// queryFn: async (): Promise => { +// // Build all singular queries +// const singulars: SingularLabelQuery[] = subjects.flatMap((s) => +// labelers.map((l) => ({ s, l })), +// ); + +// // Fetch all results in parallel +// const results = await Promise.all( +// // singulars.map((q) => +// // queryClient.fetchQuery(constructSingularLabelQuery(q)), +// // ), +// singulars.map((q) => +// queryClient.fetchQuery(constructSingularLabelQuery(q)).catch((e: Error)=>{ +// return { +// error: { +// s: q.l, +// e: e.message.toString(), +// } +// } as SingularLabelResult +// }), +// ), +// // singulars.map(q => queryClient.fetchQuery(constructSingularLabelQuery(q)).catch((err:SingularLabelResult)=>{ +// // return err +// // })) +// //singulars.map(q => labelmerge.fetch(q).catch(err => ({ error: err } as SingularLabelResult))) +// ); + +// const labels = Array.from( +// new Map( +// results +// .map(r => r?.labels) +// .filter((l): l is ATPAPI.ComAtprotoLabelDefs.Label => !!l?.src) +// .map(l => [`${l.src}::${l.uri}`, l]) +// ).values() +// ); +// const errors = Array.from( +// new Map( +// results +// .map(r => r?.error) +// .filter( +// (e): e is LabelMergeQueryLabelsOutputSchemaError => +// !!e && typeof e.s === "string" +// ) +// .map(e => [`${e.s}::${e.e ?? ""}`, e]) +// ).values() +// ); + +// const result: LabelMergeQueryLabelsOutputSchema = { +// labels: labels, +// error: errors.length < 1 ? undefined : errors, +// }; + +// return result; +// }, +// }); +// }; + +function buildSingularQueries(subjects: string[], labelers: string[]) { + return subjects.flatMap((s) => + labelers.map((l) => ({ + s, + l, + })), + ); +} + +export function useQueryLabels(subjects: string[], labelers: string[]) { + const singulars = useMemo( + () => buildSingularQueries(subjects, labelers), + [subjects, labelers], + ); + + const queries = useQueries({ + queries: singulars.map((q) => + constructSingularLabelQuery(q), + ), + }); + + // derive merged state synchronously + const labels = useMemo(() => { + return Array.from( + new Map( + queries + .map((q) => q.data?.labels) + .filter( + (l): l is ATPAPI.ComAtprotoLabelDefs.Label => + !!l?.src, + ) + .map((l) => [`${l.src}::${l.uri}`, l]), + ).values(), + ); + }, [queries]); + + const errors = useMemo(() => { + return Array.from( + new Map( + queries + .map((q) => q.data?.error) + .flat() + .filter( + (e): e is LabelMergeQueryLabelsOutputSchemaError => + !!e && typeof e.s === "string", + ) + .map((e) => [`${e.s}::${e.e ?? ""}`, e]), + ).values(), + ); + }, [queries]); + + const isLoading = queries.some((q) => q.isLoading); + const isError = queries.some((q) => q.isError); + const isFetching = queries.some((q) => q.isFetching); + + return { + data: { + labels, + error: errors.length ? errors : undefined, + }, + isLoading, + isError, + isFetching, + }; +} + +export function constructSingularLabelQuery(options: SingularLabelQuery) { + const { s, l } = options; + + return queryOptions({ + queryKey: ["__volatile","slq", s, l], + + enabled: !!s && !!l, + + queryFn: async (): Promise => { + // const result = (await labelmerge.fetch(options).catch(err => {throw { error: err } as SingularLabelResult})) as SingularLabelResult + // if (result.error) { + // throw result.error + // } + // return result; + const result = (await labelmerge + .fetch(options) + .catch( + (err) => ({ error: err }) as SingularLabelResult, + )) as SingularLabelResult; + + if (result === undefined) { + throw new Error("what the hell happened") + } + return result; + }, + + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 5 * 60 * 1000, + }); +} +export function useQuerySingularLabelQuery(options: SingularLabelQuery) { + return useQuery(constructSingularLabelQuery(options)); +} -- 2.51.2