diff --git a/drizzle/20260530050716_overrated_captain_cross/migration.sql b/drizzle/20260530050716_overrated_captain_cross/migration.sql new file mode 100644 index 000000000..4c85f001e --- /dev/null +++ b/drizzle/20260530050716_overrated_captain_cross/migration.sql @@ -0,0 +1,10 @@ +ALTER TABLE "post_link" ADD COLUMN "score" double precision DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "post_link" ADD COLUMN "weighted_mass" double precision DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "post_link" ADD COLUMN "recency_component" double precision DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "post_link" ADD COLUMN "post_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "post_link" ADD COLUMN "first_shared_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "post_link" ADD COLUMN "latest_activity_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "post_link" ADD COLUMN "score_updated" timestamp with time zone;--> statement-breakpoint +CREATE INDEX "idx_post_link_score" ON "post_link" ("score" desc,"id" desc) WHERE ("latest_activity_at" is not null);--> statement-breakpoint +CREATE INDEX "idx_post_link_first_shared" ON "post_link" ("first_shared_at" desc,"id" desc) WHERE ("latest_activity_at" is not null);--> statement-breakpoint +CREATE INDEX "idx_post_link_weighted_mass" ON "post_link" ("weighted_mass" desc,"id" desc) WHERE ("latest_activity_at" is not null); \ No newline at end of file diff --git a/drizzle/20260530050716_overrated_captain_cross/snapshot.json b/drizzle/20260530050716_overrated_captain_cross/snapshot.json new file mode 100644 index 000000000..4f7c7aee4 --- /dev/null +++ b/drizzle/20260530050716_overrated_captain_cross/snapshot.json @@ -0,0 +1,7909 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "0b7f2771-f642-4c40-a396-8a2f3f604176", + "prevIds": [ + "c62c5aa3-887c-46d7-8c8a-65b2a4a8de5a" + ], + "ddl": [ + { + "values": [ + "Ed25519", + "RSASSA-PKCS1-v1_5" + ], + "name": "account_key_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "activitypub", + "akkoma", + "bluesky", + "codeberg", + "dev", + "discord", + "facebook", + "github", + "gitlab", + "hackernews", + "hollo", + "instagram", + "keybase", + "lemmy", + "linkedin", + "lobsters", + "mastodon", + "matrix", + "misskey", + "pixelfed", + "pleroma", + "qiita", + "reddit", + "sourcehut", + "threads", + "velog", + "web", + "wikipedia", + "x", + "zenn" + ], + "name": "account_link_icon", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "Application", + "Group", + "Organization", + "Person", + "Service" + ], + "name": "actor_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "image/gif", + "image/jpeg", + "image/png", + "image/webp" + ], + "name": "medium_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "follow", + "mention", + "reply", + "share", + "quote", + "shared_post_updated", + "quoted_post_updated", + "react" + ], + "name": "notification_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "singleDevice", + "multiDevice" + ], + "name": "passkey_device_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "ble", + "cable", + "hybrid", + "internal", + "nfc", + "smart-card", + "usb" + ], + "name": "passkey_transport", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "image/gif", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/webp", + "video/mp4", + "video/webm", + "video/quicktime" + ], + "name": "post_medium_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "Article", + "Note", + "Question" + ], + "name": "post_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "public", + "unlisted", + "followers", + "direct", + "none" + ], + "name": "post_visibility", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "public_only", + "all", + "none" + ], + "name": "push_notification_preview_policy", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "apns", + "fcm", + "web_push" + ], + "name": "push_notification_service", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "everyone", + "followers", + "self" + ], + "name": "quote_policy", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "denied" + ], + "name": "quote_target_state", + "entityType": "enums", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "account_email", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "account_key", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "account_link", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "actor", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "admin_state", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_content", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_draft_medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_draft", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_source_medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_source", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "blocking", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "bookmark", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "custom_emoji", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "following", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "hashtag_following", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "instance", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "invitation_link", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "mention", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "muting", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "note_source_medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "note_source", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "notification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "passkey", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "pin", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "poll_option", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "poll", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "poll_vote", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "post_link", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "post_medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "post", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "push_notification_target", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "quote_authorization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "quote_request", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "reaction", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "timeline_item", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "public", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "account_key_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "public", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "private", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "index", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "handle", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "account_link_icon", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'web'", + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "username", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "old_username", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "username_changed", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bio", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatar_medium_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "og_image_key", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "locales", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "moderator", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notification_read", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "left_invitations", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hide_from_invitation_tree", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hide_foreign_languages", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "prefer_ai_summary", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "post_visibility", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'public'", + "generated": null, + "identity": null, + "name": "note_visibility", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "post_visibility", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'public'", + "generated": null, + "identity": null, + "name": "share_visibility", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'everyone'", + "generated": null, + "identity": null, + "name": "quote_policy", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "push_notification_preview_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'public_only'", + "generated": null, + "identity": null, + "name": "push_notification_preview_policy", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "actor_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "username", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "instance_host", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "handle_host", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": { + "as": "'@' || \"actor\".\"username\" || '@' || \"actor\".\"handle_host\"", + "type": "stored" + }, + "identity": null, + "name": "handle", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bio_html", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "automatically_approves_followers", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatar_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "header_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inbox_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shared_inbox_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "followers_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "featured_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "field_htmls", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "emojis", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "sensitive", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "successor_id", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::text[])", + "generated": null, + "identity": null, + "name": "aliases", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "followees_count", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "followers_count", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "posts_count", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "admin_state" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "admin_state" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "admin_state" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "summary", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "summary_started", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "summary_unnecessary", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "og_image_key", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "original_language", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "translator_id", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "translation_requester_id", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "being_translated", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article_draft_id", + "entityType": "columns", + "schema": "public", + "table": "article_draft_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "article_draft_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "medium_id", + "entityType": "columns", + "schema": "public", + "table": "article_draft_medium" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "article_draft_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article_source_id", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::text[])", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article_source_id", + "entityType": "columns", + "schema": "public", + "table": "article_source_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "article_source_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "medium_id", + "entityType": "columns", + "schema": "public", + "table": "article_source_medium" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "article_source_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "EXTRACT(year FROM CURRENT_TIMESTAMP)", + "generated": null, + "identity": null, + "name": "published_year", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::text[])", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allow_llm_translation", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'everyone'", + "generated": null, + "identity": null, + "name": "quote_policy", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "blocker_id", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "blockee_id", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "bookmark" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "bookmark" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "bookmark" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_type", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_url", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "follower_id", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "followee_id", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accepted", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "hashtag_following" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tag", + "entityType": "columns", + "schema": "public", + "table": "hashtag_following" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "pinned", + "entityType": "columns", + "schema": "public", + "table": "hashtag_following" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "hashtag_following" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "software", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "software_version", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "invitations_left", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "medium_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_hash", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "width", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "height", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "mention" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "mention" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "muting" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "muter_id", + "entityType": "columns", + "schema": "public", + "table": "muting" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mutee_id", + "entityType": "columns", + "schema": "public", + "table": "muting" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "muting" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "note_source_id", + "entityType": "columns", + "schema": "public", + "table": "note_source_medium" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "index", + "entityType": "columns", + "schema": "public", + "table": "note_source_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "medium_id", + "entityType": "columns", + "schema": "public", + "table": "note_source_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "alt", + "entityType": "columns", + "schema": "public", + "table": "note_source_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "post_visibility", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'public'", + "generated": null, + "identity": null, + "name": "visibility", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'everyone'", + "generated": null, + "identity": null, + "name": "quote_policy", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "notification_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::uuid[])", + "generated": null, + "identity": null, + "name": "actor_ids", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "emoji", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "custom_emoji_id", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "public_key", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthn_user_id", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "counter", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "passkey_device_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_type", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "backed_up", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "passkey_transport", + "typeSchema": "public", + "notNull": false, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "transports", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_used", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "pin" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "pin" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "pin" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "poll_option" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "index", + "entityType": "columns", + "schema": "public", + "table": "poll_option" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "poll_option" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "votes_count", + "entityType": "columns", + "schema": "public", + "table": "poll_option" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "poll" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "multiple", + "entityType": "columns", + "schema": "public", + "table": "poll" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "voters_count", + "entityType": "columns", + "schema": "public", + "table": "poll" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ends", + "entityType": "columns", + "schema": "public", + "table": "poll" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "poll_vote" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "option_index", + "entityType": "columns", + "schema": "public", + "table": "poll_vote" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "poll_vote" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "poll_vote" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "site_name", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_url", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_alt", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_type", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_width", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_height", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "creator_id", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "score", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "weighted_mass", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "recency_component", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "post_count", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "first_shared_at", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "latest_activity_at", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "score_updated", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "scraped", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "index", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "post_medium_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "alt", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "width", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "height", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_key", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "sensitive", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "post_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "post_visibility", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'unlisted'", + "generated": null, + "identity": null, + "name": "visibility", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'everyone'", + "generated": null, + "identity": null, + "name": "quote_policy", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_request_policy", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article_source_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "note_source_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shared_post_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reply_target_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quoted_post_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_authorization_iri", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "quote_target_state", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_target_state", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "summary", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_html", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::text[])", + "generated": null, + "identity": null, + "name": "relayed_tags", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "emojis", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "sensitive", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "replies_count", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "shares_count", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "quotes_count", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "reactions_counts", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": { + "as": "json_sum_object_values(\"post\".\"reactions_counts\")", + "type": "stored" + }, + "identity": null, + "name": "reactions_count", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "link_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "link_url", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "push_notification_service", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "service", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endpoint", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "p256dh", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiration_time", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_post_iri", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_post_id", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quoted_post_id", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attributed_actor_id", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "revoked", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_post_id", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quoted_post_id", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accepted", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rejected", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "emoji", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "custom_emoji_id", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "original_author_id", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_sharer_id", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sharers_count", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "post_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_type", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "added", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "appended", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lower(\"email\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_account_email_lower_email", + "entityType": "indexes", + "schema": "public", + "table": "account_email" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "avatar_medium_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_avatar_medium_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "medium_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "article_draft_medium_medium_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "article_draft_medium" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "medium_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "article_source_medium_medium_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "article_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "blockee_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blocking_blockee_id_index", + "entityType": "indexes", + "schema": "public", + "table": "blocking" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"created\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_bookmark_account_created", + "entityType": "indexes", + "schema": "public", + "table": "bookmark" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "bookmark_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "bookmark" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "follower_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "following_follower_id_index", + "entityType": "indexes", + "schema": "public", + "table": "following" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "tag", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "hashtag_following_tag_account_id_index", + "entityType": "indexes", + "schema": "public", + "table": "hashtag_following" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "mention_actor_id_index", + "entityType": "indexes", + "schema": "public", + "table": "mention" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "mutee_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "muting_mutee_id_index", + "entityType": "indexes", + "schema": "public", + "table": "muting" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "medium_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "note_source_medium_medium_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "note_source_medium" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"created\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_notification_account_id_created", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"post_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "actor_ids", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" = 'follow'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_account_id_actor_ids_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" NOT IN ('follow', 'react')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_account_id_type_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "emoji", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" = 'react' AND \"custom_emoji_id\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_account_id_post_id_emoji_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "custom_emoji_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" = 'react' AND \"emoji\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_account_id_post_id_custom_emoji_id_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkey_account_id_index", + "entityType": "indexes", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "webauthn_user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkey_webauthn_user_id_index", + "entityType": "indexes", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pin_actor_id_index", + "entityType": "indexes", + "schema": "public", + "table": "pin" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "creator_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_link_creator_id_index", + "entityType": "indexes", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"score\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"latest_activity_at\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_link_score", + "entityType": "indexes", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"first_shared_at\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"latest_activity_at\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_link_first_shared", + "entityType": "indexes", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"weighted_mass\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"latest_activity_at\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_link_weighted_mass", + "entityType": "indexes", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "visibility", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"published\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_visibility_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"published\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_actor_id_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "reply_target_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_reply_target_id_index", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "shared_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"shared_post_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_shared_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "quoted_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"quoted_post_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_quoted_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "quote_authorization_iri", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"quote_authorization_iri\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_quote_authorization_iri_index", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"published\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"note_source_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_note_source_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"published\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"article_source_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_article_source_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "visibility", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"published\"::timestamptz(3) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\n \"reply_target_id\" IS NULL\n AND (\n \"note_source_id\" IS NOT NULL\n OR \"article_source_id\" IS NOT NULL\n OR \"shared_post_id\" IS NOT NULL\n )\n ", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_public_local_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "visibility", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"published\"::timestamptz(3) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"reply_target_id\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_public_top_level_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "content_html", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "idx_post_content_html_trgm", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tags", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "idx_post_tags_gin", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "push_notification_target_account_id_index", + "entityType": "indexes", + "schema": "public", + "table": "push_notification_target" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "service", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"token\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "push_notification_target_service_token_unique", + "entityType": "indexes", + "schema": "public", + "table": "push_notification_target" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "endpoint", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"endpoint\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "push_notification_target_endpoint_unique", + "entityType": "indexes", + "schema": "public", + "table": "push_notification_target" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quote_post_iri", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_authorization_quote_post_iri_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quote_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_authorization_quote_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quoted_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_authorization_quoted_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quote_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_request_quote_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_request" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quoted_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_request_quoted_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_request" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "emoji", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"custom_emoji_id\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "reaction_post_id_actor_id_emoji_index", + "entityType": "indexes", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "custom_emoji_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"emoji\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "reaction_post_id_actor_id_custom_emoji_id_index", + "entityType": "indexes", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "reaction_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"added\"::timestamptz(3)) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_timeline_item_account_id_added", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"appended\"::timestamptz(3)) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_timeline_item_account_id_appended", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"appended\"::timestamptz(3)) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_timeline_item_account_id_post_type_appended", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"added\"::timestamptz(3)) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_timeline_item_account_id_post_type_added", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "timeline_item_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_email_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account_email" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_key_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account_key" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_link_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account_link" + }, + { + "nameExplicit": false, + "columns": [ + "avatar_medium_id" + ], + "schemaTo": "public", + "tableTo": "medium", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "account_avatar_medium_id_medium_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "inviter_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "account_inviter_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "instance_host" + ], + "schemaTo": "public", + "tableTo": "instance", + "columnsTo": [ + "host" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "actor_instance_host_instance_host_fk", + "entityType": "fks", + "schema": "public", + "table": "actor" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "actor_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "actor" + }, + { + "nameExplicit": false, + "columns": [ + "successor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "actor_successor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "actor" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "article_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_content_source_id_article_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_content" + }, + { + "nameExplicit": false, + "columns": [ + "translator_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "article_content_translator_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_content" + }, + { + "nameExplicit": false, + "columns": [ + "translation_requester_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "article_content_translation_requester_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_content" + }, + { + "nameExplicit": false, + "columns": [ + "source_id", + "original_language" + ], + "schemaTo": "public", + "tableTo": "article_content", + "columnsTo": [ + "source_id", + "language" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_content_source_id_original_language_article_content_sou", + "entityType": "fks", + "schema": "public", + "table": "article_content" + }, + { + "nameExplicit": false, + "columns": [ + "article_draft_id" + ], + "schemaTo": "public", + "tableTo": "article_draft", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_draft_medium_article_draft_id_article_draft_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_draft_medium" + }, + { + "nameExplicit": false, + "columns": [ + "medium_id" + ], + "schemaTo": "public", + "tableTo": "medium", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "article_draft_medium_medium_id_medium_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_draft_medium" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_draft_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_draft" + }, + { + "nameExplicit": false, + "columns": [ + "article_source_id" + ], + "schemaTo": "public", + "tableTo": "article_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_draft_article_source_id_article_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_draft" + }, + { + "nameExplicit": false, + "columns": [ + "article_source_id" + ], + "schemaTo": "public", + "tableTo": "article_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_source_medium_article_source_id_article_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + "medium_id" + ], + "schemaTo": "public", + "tableTo": "medium", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "article_source_medium_medium_id_medium_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_source_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_source" + }, + { + "nameExplicit": false, + "columns": [ + "blocker_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "blocking_blocker_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blocking" + }, + { + "nameExplicit": false, + "columns": [ + "blockee_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "blocking_blockee_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blocking" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "bookmark_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "bookmark" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "bookmark_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "bookmark" + }, + { + "nameExplicit": false, + "columns": [ + "follower_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "following_follower_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "following" + }, + { + "nameExplicit": false, + "columns": [ + "followee_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "following_followee_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "following" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "hashtag_following_account_id_account_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "hashtag_following" + }, + { + "nameExplicit": false, + "columns": [ + "inviter_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_link_inviter_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "invitation_link" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "mention_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "mention" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "mention_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "mention" + }, + { + "nameExplicit": false, + "columns": [ + "muter_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "muting_muter_id_actor_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "muting" + }, + { + "nameExplicit": false, + "columns": [ + "mutee_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "muting_mutee_id_actor_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "muting" + }, + { + "nameExplicit": false, + "columns": [ + "note_source_id" + ], + "schemaTo": "public", + "tableTo": "note_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "note_source_medium_note_source_id_note_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "note_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + "medium_id" + ], + "schemaTo": "public", + "tableTo": "medium", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "note_source_medium_medium_id_medium_id_fk", + "entityType": "fks", + "schema": "public", + "table": "note_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "note_source_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "note_source" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + "custom_emoji_id" + ], + "schemaTo": "public", + "tableTo": "custom_emoji", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_custom_emoji_id_custom_emoji_id_fk", + "entityType": "fks", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "passkey_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pin_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "pin" + }, + { + "nameExplicit": false, + "columns": [ + "post_id", + "actor_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id", + "actor_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pin_post_id_actor_id_post_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "pin" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "poll", + "columnsTo": [ + "post_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "poll_option_post_id_poll_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "poll_option" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "poll_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "poll" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "poll", + "columnsTo": [ + "post_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "poll_vote_post_id_poll_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "poll_vote" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "poll_vote_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "poll_vote" + }, + { + "nameExplicit": false, + "columns": [ + "post_id", + "option_index" + ], + "schemaTo": "public", + "tableTo": "poll_option", + "columnsTo": [ + "post_id", + "index" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "poll_vote_post_id_option_index_poll_option_post_id_index_fk", + "entityType": "fks", + "schema": "public", + "table": "poll_vote" + }, + { + "nameExplicit": false, + "columns": [ + "creator_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "post_link_creator_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_medium_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post_medium" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "article_source_id" + ], + "schemaTo": "public", + "tableTo": "article_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_article_source_id_article_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "note_source_id" + ], + "schemaTo": "public", + "tableTo": "note_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_note_source_id_note_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "shared_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_shared_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "reply_target_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "post_reply_target_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "quoted_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "post_quoted_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "link_id" + ], + "schemaTo": "public", + "tableTo": "post_link", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "post_link_id_post_link_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "push_notification_target_account_id_account_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "push_notification_target" + }, + { + "nameExplicit": false, + "columns": [ + "quote_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "quote_authorization_quote_post_id_post_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + "quoted_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "quote_authorization_quoted_post_id_post_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + "attributed_actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "quote_authorization_attributed_actor_id_actor_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + "quote_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "quote_request_quote_post_id_post_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_request" + }, + { + "nameExplicit": false, + "columns": [ + "quoted_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "quote_request_quoted_post_id_post_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_request" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "reaction_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "reaction_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + "custom_emoji_id" + ], + "schemaTo": "public", + "tableTo": "custom_emoji", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "reaction_custom_emoji_id_custom_emoji_id_fk", + "entityType": "fks", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "timeline_item_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "timeline_item_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": false, + "columns": [ + "original_author_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "timeline_item_original_author_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": false, + "columns": [ + "last_sharer_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "timeline_item_last_sharer_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "timeline_item" + }, + { + "columns": [ + "account_id", + "type" + ], + "nameExplicit": false, + "name": "account_key_account_id_type_pk", + "entityType": "pks", + "schema": "public", + "table": "account_key" + }, + { + "columns": [ + "account_id", + "index" + ], + "nameExplicit": false, + "name": "account_link_account_id_index_pk", + "entityType": "pks", + "schema": "public", + "table": "account_link" + }, + { + "columns": [ + "source_id", + "language" + ], + "nameExplicit": false, + "name": "article_content_source_id_language_pk", + "entityType": "pks", + "schema": "public", + "table": "article_content" + }, + { + "columns": [ + "article_draft_id", + "key" + ], + "nameExplicit": false, + "name": "article_draft_medium_article_draft_id_key_pk", + "entityType": "pks", + "schema": "public", + "table": "article_draft_medium" + }, + { + "columns": [ + "article_source_id", + "key" + ], + "nameExplicit": false, + "name": "article_source_medium_article_source_id_key_pk", + "entityType": "pks", + "schema": "public", + "table": "article_source_medium" + }, + { + "columns": [ + "account_id", + "post_id" + ], + "nameExplicit": false, + "name": "bookmark_account_id_post_id_pk", + "entityType": "pks", + "schema": "public", + "table": "bookmark" + }, + { + "columns": [ + "account_id", + "tag" + ], + "nameExplicit": false, + "name": "hashtag_following_pkey", + "entityType": "pks", + "schema": "public", + "table": "hashtag_following" + }, + { + "columns": [ + "post_id", + "actor_id" + ], + "nameExplicit": false, + "name": "mention_post_id_actor_id_pk", + "entityType": "pks", + "schema": "public", + "table": "mention" + }, + { + "columns": [ + "note_source_id", + "index" + ], + "nameExplicit": false, + "name": "note_source_medium_note_source_id_index_pk", + "entityType": "pks", + "schema": "public", + "table": "note_source_medium" + }, + { + "columns": [ + "post_id", + "actor_id" + ], + "nameExplicit": false, + "name": "pin_post_id_actor_id_pk", + "entityType": "pks", + "schema": "public", + "table": "pin" + }, + { + "columns": [ + "post_id", + "index" + ], + "nameExplicit": false, + "name": "poll_option_post_id_index_pk", + "entityType": "pks", + "schema": "public", + "table": "poll_option" + }, + { + "columns": [ + "post_id", + "option_index", + "actor_id" + ], + "nameExplicit": false, + "name": "poll_vote_post_id_option_index_actor_id_pk", + "entityType": "pks", + "schema": "public", + "table": "poll_vote" + }, + { + "columns": [ + "post_id", + "index" + ], + "nameExplicit": false, + "name": "post_medium_post_id_index_pk", + "entityType": "pks", + "schema": "public", + "table": "post_medium" + }, + { + "columns": [ + "account_id", + "post_id" + ], + "nameExplicit": false, + "name": "timeline_item_account_id_post_id_pk", + "entityType": "pks", + "schema": "public", + "table": "timeline_item" + }, + { + "columns": [ + "email" + ], + "nameExplicit": false, + "name": "account_email_pkey", + "schema": "public", + "table": "account_email", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "actor_pkey", + "schema": "public", + "table": "actor", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "admin_state_pkey", + "schema": "public", + "table": "admin_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "article_draft_pkey", + "schema": "public", + "table": "article_draft", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "article_source_pkey", + "schema": "public", + "table": "article_source", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blocking_pkey", + "schema": "public", + "table": "blocking", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "custom_emoji_pkey", + "schema": "public", + "table": "custom_emoji", + "entityType": "pks" + }, + { + "columns": [ + "iri" + ], + "nameExplicit": false, + "name": "following_pkey", + "schema": "public", + "table": "following", + "entityType": "pks" + }, + { + "columns": [ + "host" + ], + "nameExplicit": false, + "name": "instance_pkey", + "schema": "public", + "table": "instance", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "invitation_link_pkey", + "schema": "public", + "table": "invitation_link", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "medium_pkey", + "schema": "public", + "table": "medium", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "muting_pkey", + "schema": "public", + "table": "muting", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "note_source_pkey", + "schema": "public", + "table": "note_source", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "notification_pkey", + "schema": "public", + "table": "notification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "passkey_pkey", + "schema": "public", + "table": "passkey", + "entityType": "pks" + }, + { + "columns": [ + "post_id" + ], + "nameExplicit": false, + "name": "poll_pkey", + "schema": "public", + "table": "poll", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "post_link_pkey", + "schema": "public", + "table": "post_link", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "post_pkey", + "schema": "public", + "table": "post", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "push_notification_target_pkey", + "schema": "public", + "table": "push_notification_target", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "quote_authorization_pkey", + "schema": "public", + "table": "quote_authorization", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "quote_request_pkey", + "schema": "public", + "table": "quote_request", + "entityType": "pks" + }, + { + "columns": [ + "iri" + ], + "nameExplicit": false, + "name": "reaction_pkey", + "schema": "public", + "table": "reaction", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "username", + "instance_host" + ], + "nullsNotDistinct": false, + "name": "actor_username_instance_host_unique", + "entityType": "uniques", + "schema": "public", + "table": "actor" + }, + { + "nameExplicit": false, + "columns": [ + "account_id", + "published_year", + "slug" + ], + "nullsNotDistinct": false, + "name": "article_source_account_id_published_year_slug_unique", + "entityType": "uniques", + "schema": "public", + "table": "article_source" + }, + { + "nameExplicit": false, + "columns": [ + "blocker_id", + "blockee_id" + ], + "nullsNotDistinct": false, + "name": "blocking_blocker_id_blockee_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "blocking" + }, + { + "nameExplicit": false, + "columns": [ + "follower_id", + "followee_id" + ], + "nullsNotDistinct": false, + "name": "following_follower_id_followee_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "following" + }, + { + "nameExplicit": false, + "columns": [ + "muter_id", + "mutee_id" + ], + "nullsNotDistinct": false, + "name": "muting_muter_id_mutee_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "muting" + }, + { + "nameExplicit": false, + "columns": [ + "account_id", + "webauthn_user_id" + ], + "nullsNotDistinct": false, + "name": "passkey_account_id_webauthn_user_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": false, + "columns": [ + "post_id", + "title" + ], + "nullsNotDistinct": false, + "name": "poll_option_post_id_title_unique", + "entityType": "uniques", + "schema": "public", + "table": "poll_option" + }, + { + "nameExplicit": false, + "columns": [ + "id", + "actor_id" + ], + "nullsNotDistinct": false, + "name": "post_id_actor_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id", + "shared_post_id" + ], + "nullsNotDistinct": false, + "name": "post_actor_id_shared_post_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "username" + ], + "nullsNotDistinct": false, + "name": "account_username_unique", + "schema": "public", + "table": "account", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "og_image_key" + ], + "nullsNotDistinct": false, + "name": "account_og_image_key_unique", + "schema": "public", + "table": "account", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "actor_iri_unique", + "schema": "public", + "table": "actor", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "nullsNotDistinct": false, + "name": "actor_account_id_unique", + "schema": "public", + "table": "actor", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "og_image_key" + ], + "nullsNotDistinct": false, + "name": "article_content_og_image_key_unique", + "schema": "public", + "table": "article_content", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "blocking_iri_unique", + "schema": "public", + "table": "blocking", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "custom_emoji_iri_unique", + "schema": "public", + "table": "custom_emoji", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "key" + ], + "nullsNotDistinct": false, + "name": "medium_key_unique", + "schema": "public", + "table": "medium", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "content_hash" + ], + "nullsNotDistinct": false, + "name": "medium_content_hash_unique", + "schema": "public", + "table": "medium", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "url" + ], + "nullsNotDistinct": false, + "name": "post_link_url_unique", + "schema": "public", + "table": "post_link", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "thumbnail_key" + ], + "nullsNotDistinct": false, + "name": "post_medium_thumbnail_key_unique", + "schema": "public", + "table": "post_medium", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "post_iri_unique", + "schema": "public", + "table": "post", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "article_source_id" + ], + "nullsNotDistinct": false, + "name": "post_article_source_id_unique", + "schema": "public", + "table": "post", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "note_source_id" + ], + "nullsNotDistinct": false, + "name": "post_note_source_id_unique", + "schema": "public", + "table": "post", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "quote_authorization_iri_key", + "schema": "public", + "table": "quote_authorization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "quote_request_iri_key", + "schema": "public", + "table": "quote_request", + "entityType": "uniques" + }, + { + "value": "\"public\" IS JSON OBJECT", + "name": "account_key_public_check", + "entityType": "checks", + "schema": "public", + "table": "account_key" + }, + { + "value": "\"private\" IS JSON OBJECT", + "name": "account_key_private_check", + "entityType": "checks", + "schema": "public", + "table": "account_key" + }, + { + "value": "\n char_length(\"name\") <= 50 AND\n \"name\" !~ '^[[:space:]]' AND\n \"name\" !~ '[[:space:]]$'\n ", + "name": "account_link_name_check", + "entityType": "checks", + "schema": "public", + "table": "account_link" + }, + { + "value": "\"username\" ~ '^[a-z0-9_]{1,50}$'", + "name": "account_username_check", + "entityType": "checks", + "schema": "public", + "table": "account" + }, + { + "value": "\n char_length(\"name\") <= 50 AND\n \"name\" !~ '^[[:space:]]' AND\n \"name\" !~ '[[:space:]]$'\n ", + "name": "account_name_check", + "entityType": "checks", + "schema": "public", + "table": "account" + }, + { + "value": "\"username\" NOT LIKE '%@%'", + "name": "actor_username_check", + "entityType": "checks", + "schema": "public", + "table": "actor" + }, + { + "value": "(\n \"translator_id\" IS NULL AND\n \"translation_requester_id\" IS NULL\n ) = (\"original_language\" IS NULL)", + "name": "article_content_original_language_check", + "entityType": "checks", + "schema": "public", + "table": "article_content" + }, + { + "value": "\"translator_id\" IS NULL OR \"translation_requester_id\" IS NULL", + "name": "article_content_translator_translation_requester_id_check", + "entityType": "checks", + "schema": "public", + "table": "article_content" + }, + { + "value": "NOT \"being_translated\" OR (\"original_language\" IS NOT NULL)", + "name": "article_content_being_translated_check", + "entityType": "checks", + "schema": "public", + "table": "article_content" + }, + { + "value": "\"published_year\" = EXTRACT(year FROM \"published\")", + "name": "article_source_published_year_check", + "entityType": "checks", + "schema": "public", + "table": "article_source" + }, + { + "value": "\"blocker_id\" != \"blockee_id\"", + "name": "blocking_blocker_blockee_check", + "entityType": "checks", + "schema": "public", + "table": "blocking" + }, + { + "value": "\"name\" ~ '^:[^:[:space:]]+:$'", + "name": "custom_emoji_name_check", + "entityType": "checks", + "schema": "public", + "table": "custom_emoji" + }, + { + "value": "\n CASE\n WHEN \"image_type\" IS NULL THEN true\n ELSE \"image_type\" ~ '^image/'\n END\n ", + "name": "custom_emoji_image_type_check", + "entityType": "checks", + "schema": "public", + "table": "custom_emoji" + }, + { + "value": "\"image_url\" ~ '^https?://'", + "name": "custom_emoji_image_url_check", + "entityType": "checks", + "schema": "public", + "table": "custom_emoji" + }, + { + "value": "\"host\" NOT LIKE '%@%'", + "name": "instance_host_check", + "entityType": "checks", + "schema": "public", + "table": "instance" + }, + { + "value": "\n CASE\n WHEN \"width\" IS NULL THEN \"height\" IS NULL\n ELSE \"height\" IS NOT NULL AND\n \"width\" > 0 AND \"height\" > 0\n END\n ", + "name": "medium_width_height_check", + "entityType": "checks", + "schema": "public", + "table": "medium" + }, + { + "value": "\"muter_id\" != \"mutee_id\"", + "name": "muting_muter_mutee_check", + "entityType": "checks", + "schema": "public", + "table": "muting" + }, + { + "value": "\"index\" >= 0", + "name": "note_source_medium_index_check", + "entityType": "checks", + "schema": "public", + "table": "note_source_medium" + }, + { + "value": "\n CASE \"type\"\n WHEN 'follow' THEN \"post_id\" IS NULL\n ELSE \"post_id\" IS NOT NULL\n END\n ", + "name": "notification_post_id_check", + "entityType": "checks", + "schema": "public", + "table": "notification" + }, + { + "value": "\n CASE \"type\"\n WHEN 'react'\n THEN \"emoji\" IS NOT NULL AND \"custom_emoji_id\" IS NULL\n OR \"emoji\" IS NULL AND \"custom_emoji_id\" IS NOT NULL\n ELSE \"emoji\" IS NULL AND \"custom_emoji_id\" IS NULL\n END\n ", + "name": "notification_emoji_check", + "entityType": "checks", + "schema": "public", + "table": "notification" + }, + { + "value": "\"name\" !~ '^[[:space:]]*$'", + "name": "passkey_name_check", + "entityType": "checks", + "schema": "public", + "table": "passkey" + }, + { + "value": "\"index\" >= 0", + "name": "poll_option_index_check", + "entityType": "checks", + "schema": "public", + "table": "poll_option" + }, + { + "value": "\"votes_count\" >= 0", + "name": "poll_option_votes_count_check", + "entityType": "checks", + "schema": "public", + "table": "poll_option" + }, + { + "value": "\"voters_count\" >= 0", + "name": "poll_voters_count_check", + "entityType": "checks", + "schema": "public", + "table": "poll" + }, + { + "value": "\"url\" ~ '^https?://'", + "name": "post_link_url_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\"image_url\" ~ '^https?://'", + "name": "post_link_image_url_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\"image_alt\" IS NULL OR \"image_url\" IS NOT NULL", + "name": "post_link_image_alt_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\n CASE\n WHEN \"image_type\" IS NULL THEN true\n ELSE \"image_type\" ~ '^image/' AND\n \"image_url\" IS NOT NULL\n END\n ", + "name": "post_link_image_type_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\n CASE\n WHEN \"image_width\" IS NOT NULL\n THEN \"image_url\" IS NOT NULL AND\n \"image_height\" IS NOT NULL AND\n \"image_width\" > 0 AND\n \"image_height\" > 0\n WHEN \"image_height\" IS NOT NULL\n THEN \"image_url\" IS NOT NULL AND\n \"image_width\" IS NOT NULL AND\n \"image_width\" > 0 AND\n \"image_height\" > 0\n ELSE true\n END\n ", + "name": "post_link_image_width_height_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\"index\" >= 0", + "name": "post_medium_index_check", + "entityType": "checks", + "schema": "public", + "table": "post_medium" + }, + { + "value": "\"url\" ~ '^https?://'", + "name": "post_medium_url_check", + "entityType": "checks", + "schema": "public", + "table": "post_medium" + }, + { + "value": "\n CASE\n WHEN \"width\" IS NULL THEN \"height\" IS NULL\n ELSE \"height\" IS NOT NULL AND\n \"width\" > 0 AND \"height\" > 0\n END\n ", + "name": "post_medium_width_height_check", + "entityType": "checks", + "schema": "public", + "table": "post_medium" + }, + { + "value": "\"type\" = 'Article' OR \"article_source_id\" IS NULL", + "name": "post_article_source_id_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "\"type\" = 'Note' OR \"note_source_id\" IS NULL", + "name": "post_note_source_id_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "\"shared_post_id\" IS NULL OR \"reply_target_id\" IS NULL", + "name": "post_shared_post_id_reply_target_id_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "\"reactions_counts\" IS JSON OBJECT", + "name": "post_reactions_acounts_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "(\"link_id\" IS NULL) = (\"link_url\" IS NULL)", + "name": "post_link_id_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "\n CASE \"service\"\n WHEN 'apns' THEN\n \"token\" IS NOT NULL AND\n \"token\" ~ '^[0-9a-f]{64}$' AND\n \"endpoint\" IS NULL AND\n \"p256dh\" IS NULL AND\n \"auth\" IS NULL AND\n \"expiration_time\" IS NULL\n WHEN 'fcm' THEN\n \"token\" IS NOT NULL AND\n length(\"token\") > 0 AND\n \"endpoint\" IS NULL AND\n \"p256dh\" IS NULL AND\n \"auth\" IS NULL AND\n \"expiration_time\" IS NULL\n WHEN 'web_push' THEN\n \"token\" IS NULL AND\n \"endpoint\" IS NOT NULL AND\n length(\"endpoint\") > 0 AND\n \"p256dh\" IS NOT NULL AND\n length(\"p256dh\") > 0 AND\n \"auth\" IS NOT NULL AND\n length(\"auth\") > 0\n END\n ", + "name": "push_notification_target_shape_check", + "entityType": "checks", + "schema": "public", + "table": "push_notification_target" + }, + { + "value": "NOT (\"accepted\" IS NOT NULL AND \"rejected\" IS NOT NULL)", + "name": "quote_request_terminal_state_check", + "entityType": "checks", + "schema": "public", + "table": "quote_request" + }, + { + "value": "\n \"emoji\" IS NOT NULL\n AND length(\"emoji\") > 0\n AND \"emoji\" !~ '^[[:space:]:]+|[[:space:]:]+$'\n AND \"custom_emoji_id\" IS NULL\n OR\n \"emoji\" IS NULL AND \"custom_emoji_id\" IS NOT NULL\n ", + "name": "reaction_emoji_check", + "entityType": "checks", + "schema": "public", + "table": "reaction" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/drizzle/20260530145328_pretty_blacklash/migration.sql b/drizzle/20260530145328_pretty_blacklash/migration.sql new file mode 100644 index 000000000..63a67b911 --- /dev/null +++ b/drizzle/20260530145328_pretty_blacklash/migration.sql @@ -0,0 +1,12 @@ +CREATE TABLE "news_excluded_pattern" ( + "id" uuid PRIMARY KEY, + "pattern" text NOT NULL UNIQUE, + "note" text, + "creator_id" uuid, + "created" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +ALTER TABLE "post_link" ADD COLUMN "score_penalty" double precision DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "post_link" ADD COLUMN "excluded_from_news" boolean DEFAULT false NOT NULL;--> statement-breakpoint +CREATE INDEX "news_excluded_pattern_creator_id_index" ON "news_excluded_pattern" ("creator_id");--> statement-breakpoint +ALTER TABLE "news_excluded_pattern" ADD CONSTRAINT "news_excluded_pattern_creator_id_account_id_fkey" FOREIGN KEY ("creator_id") REFERENCES "account"("id") ON DELETE SET NULL; \ No newline at end of file diff --git a/drizzle/20260530145328_pretty_blacklash/snapshot.json b/drizzle/20260530145328_pretty_blacklash/snapshot.json new file mode 100644 index 000000000..771cfd4ce --- /dev/null +++ b/drizzle/20260530145328_pretty_blacklash/snapshot.json @@ -0,0 +1,8065 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "ee3e2286-4875-4486-999f-0f32f9a5e50a", + "prevIds": [ + "0b7f2771-f642-4c40-a396-8a2f3f604176" + ], + "ddl": [ + { + "values": [ + "Ed25519", + "RSASSA-PKCS1-v1_5" + ], + "name": "account_key_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "activitypub", + "akkoma", + "bluesky", + "codeberg", + "dev", + "discord", + "facebook", + "github", + "gitlab", + "hackernews", + "hollo", + "instagram", + "keybase", + "lemmy", + "linkedin", + "lobsters", + "mastodon", + "matrix", + "misskey", + "pixelfed", + "pleroma", + "qiita", + "reddit", + "sourcehut", + "threads", + "velog", + "web", + "wikipedia", + "x", + "zenn" + ], + "name": "account_link_icon", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "Application", + "Group", + "Organization", + "Person", + "Service" + ], + "name": "actor_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "image/gif", + "image/jpeg", + "image/png", + "image/webp" + ], + "name": "medium_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "follow", + "mention", + "reply", + "share", + "quote", + "shared_post_updated", + "quoted_post_updated", + "react" + ], + "name": "notification_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "singleDevice", + "multiDevice" + ], + "name": "passkey_device_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "ble", + "cable", + "hybrid", + "internal", + "nfc", + "smart-card", + "usb" + ], + "name": "passkey_transport", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "image/gif", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/webp", + "video/mp4", + "video/webm", + "video/quicktime" + ], + "name": "post_medium_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "Article", + "Note", + "Question" + ], + "name": "post_type", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "public", + "unlisted", + "followers", + "direct", + "none" + ], + "name": "post_visibility", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "public_only", + "all", + "none" + ], + "name": "push_notification_preview_policy", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "apns", + "fcm", + "web_push" + ], + "name": "push_notification_service", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "everyone", + "followers", + "self" + ], + "name": "quote_policy", + "entityType": "enums", + "schema": "public" + }, + { + "values": [ + "pending", + "denied" + ], + "name": "quote_target_state", + "entityType": "enums", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "account_email", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "account_key", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "account_link", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "actor", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "admin_state", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_content", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_draft_medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_draft", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_source_medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "article_source", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "blocking", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "bookmark", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "custom_emoji", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "following", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "hashtag_following", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "instance", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "invitation_link", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "mention", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "muting", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "news_excluded_pattern", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "note_source_medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "note_source", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "notification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "passkey", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "pin", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "poll_option", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "poll", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "poll_vote", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "post_link", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "post_medium", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "post", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "push_notification_target", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "quote_authorization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "quote_request", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "reaction", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "timeline_item", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "public", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "account_email" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "account_key_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "public", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "private", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "account_key" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "index", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "handle", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "account_link_icon", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'web'", + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "account_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "username", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "old_username", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "username_changed", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bio", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatar_medium_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "og_image_key", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "locales", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "moderator", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notification_read", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "left_invitations", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hide_from_invitation_tree", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hide_foreign_languages", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "prefer_ai_summary", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "post_visibility", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'public'", + "generated": null, + "identity": null, + "name": "note_visibility", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "post_visibility", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'public'", + "generated": null, + "identity": null, + "name": "share_visibility", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'everyone'", + "generated": null, + "identity": null, + "name": "quote_policy", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "push_notification_preview_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'public_only'", + "generated": null, + "identity": null, + "name": "push_notification_preview_policy", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "actor_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "username", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "instance_host", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "handle_host", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": { + "as": "'@' || \"actor\".\"username\" || '@' || \"actor\".\"handle_host\"", + "type": "stored" + }, + "identity": null, + "name": "handle", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bio_html", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "automatically_approves_followers", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatar_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "header_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inbox_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shared_inbox_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "followers_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "featured_url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "field_htmls", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "emojis", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "sensitive", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "successor_id", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::text[])", + "generated": null, + "identity": null, + "name": "aliases", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "followees_count", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "followers_count", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "posts_count", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "actor" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "admin_state" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "admin_state" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "admin_state" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "summary", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "summary_started", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "summary_unnecessary", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "og_image_key", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "original_language", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "translator_id", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "translation_requester_id", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "being_translated", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "article_content" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article_draft_id", + "entityType": "columns", + "schema": "public", + "table": "article_draft_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "article_draft_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "medium_id", + "entityType": "columns", + "schema": "public", + "table": "article_draft_medium" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "article_draft_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article_source_id", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::text[])", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "article_draft" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article_source_id", + "entityType": "columns", + "schema": "public", + "table": "article_source_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "article_source_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "medium_id", + "entityType": "columns", + "schema": "public", + "table": "article_source_medium" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "article_source_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "EXTRACT(year FROM CURRENT_TIMESTAMP)", + "generated": null, + "identity": null, + "name": "published_year", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::text[])", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allow_llm_translation", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'everyone'", + "generated": null, + "identity": null, + "name": "quote_policy", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "article_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "blocker_id", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "blockee_id", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "blocking" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "bookmark" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "bookmark" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "bookmark" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_type", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_url", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "custom_emoji" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "follower_id", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "followee_id", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accepted", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "following" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "hashtag_following" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tag", + "entityType": "columns", + "schema": "public", + "table": "hashtag_following" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "pinned", + "entityType": "columns", + "schema": "public", + "table": "hashtag_following" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "hashtag_following" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "software", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "software_version", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "instance" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "invitations_left", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires", + "entityType": "columns", + "schema": "public", + "table": "invitation_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "medium_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_hash", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "width", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "height", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "mention" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "mention" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "muting" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "muter_id", + "entityType": "columns", + "schema": "public", + "table": "muting" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mutee_id", + "entityType": "columns", + "schema": "public", + "table": "muting" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "muting" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "news_excluded_pattern" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pattern", + "entityType": "columns", + "schema": "public", + "table": "news_excluded_pattern" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "note", + "entityType": "columns", + "schema": "public", + "table": "news_excluded_pattern" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "creator_id", + "entityType": "columns", + "schema": "public", + "table": "news_excluded_pattern" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "news_excluded_pattern" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "note_source_id", + "entityType": "columns", + "schema": "public", + "table": "note_source_medium" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "index", + "entityType": "columns", + "schema": "public", + "table": "note_source_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "medium_id", + "entityType": "columns", + "schema": "public", + "table": "note_source_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "alt", + "entityType": "columns", + "schema": "public", + "table": "note_source_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "post_visibility", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'public'", + "generated": null, + "identity": null, + "name": "visibility", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'everyone'", + "generated": null, + "identity": null, + "name": "quote_policy", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "note_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "notification_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::uuid[])", + "generated": null, + "identity": null, + "name": "actor_ids", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "emoji", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "custom_emoji_id", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "public_key", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthn_user_id", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "counter", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "passkey_device_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_type", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "backed_up", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "passkey_transport", + "typeSchema": "public", + "notNull": false, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "transports", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_used", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "passkey" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "pin" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "pin" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "pin" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "poll_option" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "index", + "entityType": "columns", + "schema": "public", + "table": "poll_option" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "poll_option" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "votes_count", + "entityType": "columns", + "schema": "public", + "table": "poll_option" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "poll" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "multiple", + "entityType": "columns", + "schema": "public", + "table": "poll" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "voters_count", + "entityType": "columns", + "schema": "public", + "table": "poll" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ends", + "entityType": "columns", + "schema": "public", + "table": "poll" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "poll_vote" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "option_index", + "entityType": "columns", + "schema": "public", + "table": "poll_vote" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "poll_vote" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "poll_vote" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "site_name", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_url", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_alt", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_type", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_width", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_height", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "creator_id", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "score", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "weighted_mass", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "recency_component", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "post_count", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "first_shared_at", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "latest_activity_at", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "score_updated", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "score_penalty", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "excluded_from_news", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "scraped", + "entityType": "columns", + "schema": "public", + "table": "post_link" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "smallint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "index", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "post_medium_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "alt", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "width", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "height", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_key", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "sensitive", + "entityType": "columns", + "schema": "public", + "table": "post_medium" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "post_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "post_visibility", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'unlisted'", + "generated": null, + "identity": null, + "name": "visibility", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": "'everyone'", + "generated": null, + "identity": null, + "name": "quote_policy", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "quote_policy", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_request_policy", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "article_source_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "note_source_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "shared_post_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reply_target_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quoted_post_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_authorization_iri", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "quote_target_state", + "typeSchema": "public", + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_target_state", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "summary", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_html", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "(ARRAY[]::text[])", + "generated": null, + "identity": null, + "name": "relayed_tags", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "emojis", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "sensitive", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "replies_count", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "shares_count", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "quotes_count", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "reactions_counts", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": { + "as": "json_sum_object_values(\"post\".\"reactions_counts\")", + "type": "stored" + }, + "identity": null, + "name": "reactions_count", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "link_id", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "link_url", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "published", + "entityType": "columns", + "schema": "public", + "table": "post" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "push_notification_service", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "service", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endpoint", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "p256dh", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiration_time", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "push_notification_target" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_post_iri", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_post_id", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quoted_post_id", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attributed_actor_id", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "revoked", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "quote_authorization" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quote_post_id", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "quoted_post_id", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accepted", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rejected", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "updated", + "entityType": "columns", + "schema": "public", + "table": "quote_request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iri", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actor_id", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "emoji", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "custom_emoji_id", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "created", + "entityType": "columns", + "schema": "public", + "table": "reaction" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_id", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "original_author_id", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_sharer_id", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sharers_count", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "post_type", + "typeSchema": "public", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "post_type", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "added", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "CURRENT_TIMESTAMP", + "generated": null, + "identity": null, + "name": "appended", + "entityType": "columns", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lower(\"email\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_account_email_lower_email", + "entityType": "indexes", + "schema": "public", + "table": "account_email" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "avatar_medium_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_avatar_medium_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "medium_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "article_draft_medium_medium_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "article_draft_medium" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "medium_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "article_source_medium_medium_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "article_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "blockee_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blocking_blockee_id_index", + "entityType": "indexes", + "schema": "public", + "table": "blocking" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"created\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_bookmark_account_created", + "entityType": "indexes", + "schema": "public", + "table": "bookmark" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "bookmark_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "bookmark" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "follower_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "following_follower_id_index", + "entityType": "indexes", + "schema": "public", + "table": "following" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "tag", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "hashtag_following_tag_account_id_index", + "entityType": "indexes", + "schema": "public", + "table": "hashtag_following" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "mention_actor_id_index", + "entityType": "indexes", + "schema": "public", + "table": "mention" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "mutee_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "muting_mutee_id_index", + "entityType": "indexes", + "schema": "public", + "table": "muting" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "creator_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "news_excluded_pattern_creator_id_index", + "entityType": "indexes", + "schema": "public", + "table": "news_excluded_pattern" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "medium_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "note_source_medium_medium_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "note_source_medium" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"created\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_notification_account_id_created", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"post_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "actor_ids", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" = 'follow'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_account_id_actor_ids_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" NOT IN ('follow', 'react')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_account_id_type_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "emoji", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" = 'react' AND \"custom_emoji_id\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_account_id_post_id_emoji_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "custom_emoji_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"type\" = 'react' AND \"emoji\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_account_id_post_id_custom_emoji_id_index", + "entityType": "indexes", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkey_account_id_index", + "entityType": "indexes", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "webauthn_user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkey_webauthn_user_id_index", + "entityType": "indexes", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pin_actor_id_index", + "entityType": "indexes", + "schema": "public", + "table": "pin" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "creator_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_link_creator_id_index", + "entityType": "indexes", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"score\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"latest_activity_at\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_link_score", + "entityType": "indexes", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"first_shared_at\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"latest_activity_at\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_link_first_shared", + "entityType": "indexes", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"weighted_mass\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"latest_activity_at\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_link_weighted_mass", + "entityType": "indexes", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "visibility", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"published\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_visibility_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"published\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_actor_id_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "reply_target_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_reply_target_id_index", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "shared_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"shared_post_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_shared_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "quoted_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"quoted_post_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_quoted_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "quote_authorization_iri", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"quote_authorization_iri\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "post_quote_authorization_iri_index", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"published\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"note_source_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_note_source_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "\"published\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "(\"article_source_id\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_article_source_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "visibility", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"published\"::timestamptz(3) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\n \"reply_target_id\" IS NULL\n AND (\n \"note_source_id\" IS NOT NULL\n OR \"article_source_id\" IS NOT NULL\n OR \"shared_post_id\" IS NOT NULL\n )\n ", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_public_local_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "visibility", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"published\"::timestamptz(3) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"reply_target_id\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_post_public_top_level_published", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "content_html", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "idx_post_content_html_trgm", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tags", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "idx_post_tags_gin", + "entityType": "indexes", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "push_notification_target_account_id_index", + "entityType": "indexes", + "schema": "public", + "table": "push_notification_target" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "service", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"token\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "push_notification_target_service_token_unique", + "entityType": "indexes", + "schema": "public", + "table": "push_notification_target" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "endpoint", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"endpoint\" is not null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "push_notification_target_endpoint_unique", + "entityType": "indexes", + "schema": "public", + "table": "push_notification_target" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quote_post_iri", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_authorization_quote_post_iri_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quote_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_authorization_quote_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quoted_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_authorization_quoted_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quote_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_request_quote_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_request" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "quoted_post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "quote_request_quoted_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "quote_request" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "emoji", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"custom_emoji_id\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "reaction_post_id_actor_id_emoji_index", + "entityType": "indexes", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "actor_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "custom_emoji_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "(\"emoji\" is null)", + "with": "", + "method": "btree", + "concurrently": false, + "name": "reaction_post_id_actor_id_custom_emoji_id_index", + "entityType": "indexes", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "reaction_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"added\"::timestamptz(3)) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_timeline_item_account_id_added", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"appended\"::timestamptz(3)) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_timeline_item_account_id_appended", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"appended\"::timestamptz(3)) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_timeline_item_account_id_post_type_appended", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "post_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "(\"added\"::timestamptz(3)) desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "\"post_id\" desc", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_timeline_item_account_id_post_type_added", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "post_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "timeline_item_post_id_index", + "entityType": "indexes", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_email_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account_email" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_key_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account_key" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_link_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account_link" + }, + { + "nameExplicit": false, + "columns": [ + "avatar_medium_id" + ], + "schemaTo": "public", + "tableTo": "medium", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "account_avatar_medium_id_medium_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "inviter_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "account_inviter_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "instance_host" + ], + "schemaTo": "public", + "tableTo": "instance", + "columnsTo": [ + "host" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "actor_instance_host_instance_host_fk", + "entityType": "fks", + "schema": "public", + "table": "actor" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "actor_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "actor" + }, + { + "nameExplicit": false, + "columns": [ + "successor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "actor_successor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "actor" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "schemaTo": "public", + "tableTo": "article_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_content_source_id_article_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_content" + }, + { + "nameExplicit": false, + "columns": [ + "translator_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "article_content_translator_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_content" + }, + { + "nameExplicit": false, + "columns": [ + "translation_requester_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "article_content_translation_requester_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_content" + }, + { + "nameExplicit": false, + "columns": [ + "source_id", + "original_language" + ], + "schemaTo": "public", + "tableTo": "article_content", + "columnsTo": [ + "source_id", + "language" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_content_source_id_original_language_article_content_sou", + "entityType": "fks", + "schema": "public", + "table": "article_content" + }, + { + "nameExplicit": false, + "columns": [ + "article_draft_id" + ], + "schemaTo": "public", + "tableTo": "article_draft", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_draft_medium_article_draft_id_article_draft_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_draft_medium" + }, + { + "nameExplicit": false, + "columns": [ + "medium_id" + ], + "schemaTo": "public", + "tableTo": "medium", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "article_draft_medium_medium_id_medium_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_draft_medium" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_draft_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_draft" + }, + { + "nameExplicit": false, + "columns": [ + "article_source_id" + ], + "schemaTo": "public", + "tableTo": "article_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_draft_article_source_id_article_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_draft" + }, + { + "nameExplicit": false, + "columns": [ + "article_source_id" + ], + "schemaTo": "public", + "tableTo": "article_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_source_medium_article_source_id_article_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + "medium_id" + ], + "schemaTo": "public", + "tableTo": "medium", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "article_source_medium_medium_id_medium_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "article_source_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "article_source" + }, + { + "nameExplicit": false, + "columns": [ + "blocker_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "blocking_blocker_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blocking" + }, + { + "nameExplicit": false, + "columns": [ + "blockee_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "blocking_blockee_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blocking" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "bookmark_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "bookmark" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "bookmark_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "bookmark" + }, + { + "nameExplicit": false, + "columns": [ + "follower_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "following_follower_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "following" + }, + { + "nameExplicit": false, + "columns": [ + "followee_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "following_followee_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "following" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "hashtag_following_account_id_account_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "hashtag_following" + }, + { + "nameExplicit": false, + "columns": [ + "inviter_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_link_inviter_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "invitation_link" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "mention_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "mention" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "mention_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "mention" + }, + { + "nameExplicit": false, + "columns": [ + "muter_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "muting_muter_id_actor_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "muting" + }, + { + "nameExplicit": false, + "columns": [ + "mutee_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "muting_mutee_id_actor_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "muting" + }, + { + "nameExplicit": false, + "columns": [ + "creator_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "news_excluded_pattern_creator_id_account_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "news_excluded_pattern" + }, + { + "nameExplicit": false, + "columns": [ + "note_source_id" + ], + "schemaTo": "public", + "tableTo": "note_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "note_source_medium_note_source_id_note_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "note_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + "medium_id" + ], + "schemaTo": "public", + "tableTo": "medium", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "note_source_medium_medium_id_medium_id_fk", + "entityType": "fks", + "schema": "public", + "table": "note_source_medium" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "note_source_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "note_source" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + "custom_emoji_id" + ], + "schemaTo": "public", + "tableTo": "custom_emoji", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_custom_emoji_id_custom_emoji_id_fk", + "entityType": "fks", + "schema": "public", + "table": "notification" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "passkey_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pin_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "pin" + }, + { + "nameExplicit": false, + "columns": [ + "post_id", + "actor_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id", + "actor_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pin_post_id_actor_id_post_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "pin" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "poll", + "columnsTo": [ + "post_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "poll_option_post_id_poll_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "poll_option" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "poll_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "poll" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "poll", + "columnsTo": [ + "post_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "poll_vote_post_id_poll_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "poll_vote" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "poll_vote_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "poll_vote" + }, + { + "nameExplicit": false, + "columns": [ + "post_id", + "option_index" + ], + "schemaTo": "public", + "tableTo": "poll_option", + "columnsTo": [ + "post_id", + "index" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "poll_vote_post_id_option_index_poll_option_post_id_index_fk", + "entityType": "fks", + "schema": "public", + "table": "poll_vote" + }, + { + "nameExplicit": false, + "columns": [ + "creator_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "post_link_creator_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post_link" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_medium_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post_medium" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "article_source_id" + ], + "schemaTo": "public", + "tableTo": "article_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_article_source_id_article_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "note_source_id" + ], + "schemaTo": "public", + "tableTo": "note_source", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_note_source_id_note_source_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "shared_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "post_shared_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "reply_target_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "post_reply_target_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "quoted_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "post_quoted_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "link_id" + ], + "schemaTo": "public", + "tableTo": "post_link", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "post_link_id_post_link_id_fk", + "entityType": "fks", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "push_notification_target_account_id_account_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "push_notification_target" + }, + { + "nameExplicit": false, + "columns": [ + "quote_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "quote_authorization_quote_post_id_post_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + "quoted_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "quote_authorization_quoted_post_id_post_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + "attributed_actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "quote_authorization_attributed_actor_id_actor_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_authorization" + }, + { + "nameExplicit": false, + "columns": [ + "quote_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "quote_request_quote_post_id_post_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_request" + }, + { + "nameExplicit": false, + "columns": [ + "quoted_post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "quote_request_quoted_post_id_post_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "quote_request" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "reaction_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "reaction_actor_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + "custom_emoji_id" + ], + "schemaTo": "public", + "tableTo": "custom_emoji", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "reaction_custom_emoji_id_custom_emoji_id_fk", + "entityType": "fks", + "schema": "public", + "table": "reaction" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "public", + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "timeline_item_account_id_account_id_fk", + "entityType": "fks", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": false, + "columns": [ + "post_id" + ], + "schemaTo": "public", + "tableTo": "post", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "timeline_item_post_id_post_id_fk", + "entityType": "fks", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": false, + "columns": [ + "original_author_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "timeline_item_original_author_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "timeline_item" + }, + { + "nameExplicit": false, + "columns": [ + "last_sharer_id" + ], + "schemaTo": "public", + "tableTo": "actor", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "timeline_item_last_sharer_id_actor_id_fk", + "entityType": "fks", + "schema": "public", + "table": "timeline_item" + }, + { + "columns": [ + "account_id", + "type" + ], + "nameExplicit": false, + "name": "account_key_account_id_type_pk", + "entityType": "pks", + "schema": "public", + "table": "account_key" + }, + { + "columns": [ + "account_id", + "index" + ], + "nameExplicit": false, + "name": "account_link_account_id_index_pk", + "entityType": "pks", + "schema": "public", + "table": "account_link" + }, + { + "columns": [ + "source_id", + "language" + ], + "nameExplicit": false, + "name": "article_content_source_id_language_pk", + "entityType": "pks", + "schema": "public", + "table": "article_content" + }, + { + "columns": [ + "article_draft_id", + "key" + ], + "nameExplicit": false, + "name": "article_draft_medium_article_draft_id_key_pk", + "entityType": "pks", + "schema": "public", + "table": "article_draft_medium" + }, + { + "columns": [ + "article_source_id", + "key" + ], + "nameExplicit": false, + "name": "article_source_medium_article_source_id_key_pk", + "entityType": "pks", + "schema": "public", + "table": "article_source_medium" + }, + { + "columns": [ + "account_id", + "post_id" + ], + "nameExplicit": false, + "name": "bookmark_account_id_post_id_pk", + "entityType": "pks", + "schema": "public", + "table": "bookmark" + }, + { + "columns": [ + "account_id", + "tag" + ], + "nameExplicit": false, + "name": "hashtag_following_pkey", + "entityType": "pks", + "schema": "public", + "table": "hashtag_following" + }, + { + "columns": [ + "post_id", + "actor_id" + ], + "nameExplicit": false, + "name": "mention_post_id_actor_id_pk", + "entityType": "pks", + "schema": "public", + "table": "mention" + }, + { + "columns": [ + "note_source_id", + "index" + ], + "nameExplicit": false, + "name": "note_source_medium_note_source_id_index_pk", + "entityType": "pks", + "schema": "public", + "table": "note_source_medium" + }, + { + "columns": [ + "post_id", + "actor_id" + ], + "nameExplicit": false, + "name": "pin_post_id_actor_id_pk", + "entityType": "pks", + "schema": "public", + "table": "pin" + }, + { + "columns": [ + "post_id", + "index" + ], + "nameExplicit": false, + "name": "poll_option_post_id_index_pk", + "entityType": "pks", + "schema": "public", + "table": "poll_option" + }, + { + "columns": [ + "post_id", + "option_index", + "actor_id" + ], + "nameExplicit": false, + "name": "poll_vote_post_id_option_index_actor_id_pk", + "entityType": "pks", + "schema": "public", + "table": "poll_vote" + }, + { + "columns": [ + "post_id", + "index" + ], + "nameExplicit": false, + "name": "post_medium_post_id_index_pk", + "entityType": "pks", + "schema": "public", + "table": "post_medium" + }, + { + "columns": [ + "account_id", + "post_id" + ], + "nameExplicit": false, + "name": "timeline_item_account_id_post_id_pk", + "entityType": "pks", + "schema": "public", + "table": "timeline_item" + }, + { + "columns": [ + "email" + ], + "nameExplicit": false, + "name": "account_email_pkey", + "schema": "public", + "table": "account_email", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "actor_pkey", + "schema": "public", + "table": "actor", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "admin_state_pkey", + "schema": "public", + "table": "admin_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "article_draft_pkey", + "schema": "public", + "table": "article_draft", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "article_source_pkey", + "schema": "public", + "table": "article_source", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blocking_pkey", + "schema": "public", + "table": "blocking", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "custom_emoji_pkey", + "schema": "public", + "table": "custom_emoji", + "entityType": "pks" + }, + { + "columns": [ + "iri" + ], + "nameExplicit": false, + "name": "following_pkey", + "schema": "public", + "table": "following", + "entityType": "pks" + }, + { + "columns": [ + "host" + ], + "nameExplicit": false, + "name": "instance_pkey", + "schema": "public", + "table": "instance", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "invitation_link_pkey", + "schema": "public", + "table": "invitation_link", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "medium_pkey", + "schema": "public", + "table": "medium", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "muting_pkey", + "schema": "public", + "table": "muting", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "news_excluded_pattern_pkey", + "schema": "public", + "table": "news_excluded_pattern", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "note_source_pkey", + "schema": "public", + "table": "note_source", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "notification_pkey", + "schema": "public", + "table": "notification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "passkey_pkey", + "schema": "public", + "table": "passkey", + "entityType": "pks" + }, + { + "columns": [ + "post_id" + ], + "nameExplicit": false, + "name": "poll_pkey", + "schema": "public", + "table": "poll", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "post_link_pkey", + "schema": "public", + "table": "post_link", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "post_pkey", + "schema": "public", + "table": "post", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "push_notification_target_pkey", + "schema": "public", + "table": "push_notification_target", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "quote_authorization_pkey", + "schema": "public", + "table": "quote_authorization", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "quote_request_pkey", + "schema": "public", + "table": "quote_request", + "entityType": "pks" + }, + { + "columns": [ + "iri" + ], + "nameExplicit": false, + "name": "reaction_pkey", + "schema": "public", + "table": "reaction", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "username", + "instance_host" + ], + "nullsNotDistinct": false, + "name": "actor_username_instance_host_unique", + "entityType": "uniques", + "schema": "public", + "table": "actor" + }, + { + "nameExplicit": false, + "columns": [ + "account_id", + "published_year", + "slug" + ], + "nullsNotDistinct": false, + "name": "article_source_account_id_published_year_slug_unique", + "entityType": "uniques", + "schema": "public", + "table": "article_source" + }, + { + "nameExplicit": false, + "columns": [ + "blocker_id", + "blockee_id" + ], + "nullsNotDistinct": false, + "name": "blocking_blocker_id_blockee_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "blocking" + }, + { + "nameExplicit": false, + "columns": [ + "follower_id", + "followee_id" + ], + "nullsNotDistinct": false, + "name": "following_follower_id_followee_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "following" + }, + { + "nameExplicit": false, + "columns": [ + "muter_id", + "mutee_id" + ], + "nullsNotDistinct": false, + "name": "muting_muter_id_mutee_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "muting" + }, + { + "nameExplicit": false, + "columns": [ + "account_id", + "webauthn_user_id" + ], + "nullsNotDistinct": false, + "name": "passkey_account_id_webauthn_user_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "passkey" + }, + { + "nameExplicit": false, + "columns": [ + "post_id", + "title" + ], + "nullsNotDistinct": false, + "name": "poll_option_post_id_title_unique", + "entityType": "uniques", + "schema": "public", + "table": "poll_option" + }, + { + "nameExplicit": false, + "columns": [ + "id", + "actor_id" + ], + "nullsNotDistinct": false, + "name": "post_id_actor_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "actor_id", + "shared_post_id" + ], + "nullsNotDistinct": false, + "name": "post_actor_id_shared_post_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "post" + }, + { + "nameExplicit": false, + "columns": [ + "username" + ], + "nullsNotDistinct": false, + "name": "account_username_unique", + "schema": "public", + "table": "account", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "og_image_key" + ], + "nullsNotDistinct": false, + "name": "account_og_image_key_unique", + "schema": "public", + "table": "account", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "actor_iri_unique", + "schema": "public", + "table": "actor", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "nullsNotDistinct": false, + "name": "actor_account_id_unique", + "schema": "public", + "table": "actor", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "og_image_key" + ], + "nullsNotDistinct": false, + "name": "article_content_og_image_key_unique", + "schema": "public", + "table": "article_content", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "blocking_iri_unique", + "schema": "public", + "table": "blocking", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "custom_emoji_iri_unique", + "schema": "public", + "table": "custom_emoji", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "key" + ], + "nullsNotDistinct": false, + "name": "medium_key_unique", + "schema": "public", + "table": "medium", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "content_hash" + ], + "nullsNotDistinct": false, + "name": "medium_content_hash_unique", + "schema": "public", + "table": "medium", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "pattern" + ], + "nullsNotDistinct": false, + "name": "news_excluded_pattern_pattern_key", + "schema": "public", + "table": "news_excluded_pattern", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "url" + ], + "nullsNotDistinct": false, + "name": "post_link_url_unique", + "schema": "public", + "table": "post_link", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "thumbnail_key" + ], + "nullsNotDistinct": false, + "name": "post_medium_thumbnail_key_unique", + "schema": "public", + "table": "post_medium", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "post_iri_unique", + "schema": "public", + "table": "post", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "article_source_id" + ], + "nullsNotDistinct": false, + "name": "post_article_source_id_unique", + "schema": "public", + "table": "post", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "note_source_id" + ], + "nullsNotDistinct": false, + "name": "post_note_source_id_unique", + "schema": "public", + "table": "post", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "quote_authorization_iri_key", + "schema": "public", + "table": "quote_authorization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "iri" + ], + "nullsNotDistinct": false, + "name": "quote_request_iri_key", + "schema": "public", + "table": "quote_request", + "entityType": "uniques" + }, + { + "value": "\"public\" IS JSON OBJECT", + "name": "account_key_public_check", + "entityType": "checks", + "schema": "public", + "table": "account_key" + }, + { + "value": "\"private\" IS JSON OBJECT", + "name": "account_key_private_check", + "entityType": "checks", + "schema": "public", + "table": "account_key" + }, + { + "value": "\n char_length(\"name\") <= 50 AND\n \"name\" !~ '^[[:space:]]' AND\n \"name\" !~ '[[:space:]]$'\n ", + "name": "account_link_name_check", + "entityType": "checks", + "schema": "public", + "table": "account_link" + }, + { + "value": "\"username\" ~ '^[a-z0-9_]{1,50}$'", + "name": "account_username_check", + "entityType": "checks", + "schema": "public", + "table": "account" + }, + { + "value": "\n char_length(\"name\") <= 50 AND\n \"name\" !~ '^[[:space:]]' AND\n \"name\" !~ '[[:space:]]$'\n ", + "name": "account_name_check", + "entityType": "checks", + "schema": "public", + "table": "account" + }, + { + "value": "\"username\" NOT LIKE '%@%'", + "name": "actor_username_check", + "entityType": "checks", + "schema": "public", + "table": "actor" + }, + { + "value": "(\n \"translator_id\" IS NULL AND\n \"translation_requester_id\" IS NULL\n ) = (\"original_language\" IS NULL)", + "name": "article_content_original_language_check", + "entityType": "checks", + "schema": "public", + "table": "article_content" + }, + { + "value": "\"translator_id\" IS NULL OR \"translation_requester_id\" IS NULL", + "name": "article_content_translator_translation_requester_id_check", + "entityType": "checks", + "schema": "public", + "table": "article_content" + }, + { + "value": "NOT \"being_translated\" OR (\"original_language\" IS NOT NULL)", + "name": "article_content_being_translated_check", + "entityType": "checks", + "schema": "public", + "table": "article_content" + }, + { + "value": "\"published_year\" = EXTRACT(year FROM \"published\")", + "name": "article_source_published_year_check", + "entityType": "checks", + "schema": "public", + "table": "article_source" + }, + { + "value": "\"blocker_id\" != \"blockee_id\"", + "name": "blocking_blocker_blockee_check", + "entityType": "checks", + "schema": "public", + "table": "blocking" + }, + { + "value": "\"name\" ~ '^:[^:[:space:]]+:$'", + "name": "custom_emoji_name_check", + "entityType": "checks", + "schema": "public", + "table": "custom_emoji" + }, + { + "value": "\n CASE\n WHEN \"image_type\" IS NULL THEN true\n ELSE \"image_type\" ~ '^image/'\n END\n ", + "name": "custom_emoji_image_type_check", + "entityType": "checks", + "schema": "public", + "table": "custom_emoji" + }, + { + "value": "\"image_url\" ~ '^https?://'", + "name": "custom_emoji_image_url_check", + "entityType": "checks", + "schema": "public", + "table": "custom_emoji" + }, + { + "value": "\"host\" NOT LIKE '%@%'", + "name": "instance_host_check", + "entityType": "checks", + "schema": "public", + "table": "instance" + }, + { + "value": "\n CASE\n WHEN \"width\" IS NULL THEN \"height\" IS NULL\n ELSE \"height\" IS NOT NULL AND\n \"width\" > 0 AND \"height\" > 0\n END\n ", + "name": "medium_width_height_check", + "entityType": "checks", + "schema": "public", + "table": "medium" + }, + { + "value": "\"muter_id\" != \"mutee_id\"", + "name": "muting_muter_mutee_check", + "entityType": "checks", + "schema": "public", + "table": "muting" + }, + { + "value": "\"index\" >= 0", + "name": "note_source_medium_index_check", + "entityType": "checks", + "schema": "public", + "table": "note_source_medium" + }, + { + "value": "\n CASE \"type\"\n WHEN 'follow' THEN \"post_id\" IS NULL\n ELSE \"post_id\" IS NOT NULL\n END\n ", + "name": "notification_post_id_check", + "entityType": "checks", + "schema": "public", + "table": "notification" + }, + { + "value": "\n CASE \"type\"\n WHEN 'react'\n THEN \"emoji\" IS NOT NULL AND \"custom_emoji_id\" IS NULL\n OR \"emoji\" IS NULL AND \"custom_emoji_id\" IS NOT NULL\n ELSE \"emoji\" IS NULL AND \"custom_emoji_id\" IS NULL\n END\n ", + "name": "notification_emoji_check", + "entityType": "checks", + "schema": "public", + "table": "notification" + }, + { + "value": "\"name\" !~ '^[[:space:]]*$'", + "name": "passkey_name_check", + "entityType": "checks", + "schema": "public", + "table": "passkey" + }, + { + "value": "\"index\" >= 0", + "name": "poll_option_index_check", + "entityType": "checks", + "schema": "public", + "table": "poll_option" + }, + { + "value": "\"votes_count\" >= 0", + "name": "poll_option_votes_count_check", + "entityType": "checks", + "schema": "public", + "table": "poll_option" + }, + { + "value": "\"voters_count\" >= 0", + "name": "poll_voters_count_check", + "entityType": "checks", + "schema": "public", + "table": "poll" + }, + { + "value": "\"url\" ~ '^https?://'", + "name": "post_link_url_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\"image_url\" ~ '^https?://'", + "name": "post_link_image_url_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\"image_alt\" IS NULL OR \"image_url\" IS NOT NULL", + "name": "post_link_image_alt_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\n CASE\n WHEN \"image_type\" IS NULL THEN true\n ELSE \"image_type\" ~ '^image/' AND\n \"image_url\" IS NOT NULL\n END\n ", + "name": "post_link_image_type_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\n CASE\n WHEN \"image_width\" IS NOT NULL\n THEN \"image_url\" IS NOT NULL AND\n \"image_height\" IS NOT NULL AND\n \"image_width\" > 0 AND\n \"image_height\" > 0\n WHEN \"image_height\" IS NOT NULL\n THEN \"image_url\" IS NOT NULL AND\n \"image_width\" IS NOT NULL AND\n \"image_width\" > 0 AND\n \"image_height\" > 0\n ELSE true\n END\n ", + "name": "post_link_image_width_height_check", + "entityType": "checks", + "schema": "public", + "table": "post_link" + }, + { + "value": "\"index\" >= 0", + "name": "post_medium_index_check", + "entityType": "checks", + "schema": "public", + "table": "post_medium" + }, + { + "value": "\"url\" ~ '^https?://'", + "name": "post_medium_url_check", + "entityType": "checks", + "schema": "public", + "table": "post_medium" + }, + { + "value": "\n CASE\n WHEN \"width\" IS NULL THEN \"height\" IS NULL\n ELSE \"height\" IS NOT NULL AND\n \"width\" > 0 AND \"height\" > 0\n END\n ", + "name": "post_medium_width_height_check", + "entityType": "checks", + "schema": "public", + "table": "post_medium" + }, + { + "value": "\"type\" = 'Article' OR \"article_source_id\" IS NULL", + "name": "post_article_source_id_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "\"type\" = 'Note' OR \"note_source_id\" IS NULL", + "name": "post_note_source_id_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "\"shared_post_id\" IS NULL OR \"reply_target_id\" IS NULL", + "name": "post_shared_post_id_reply_target_id_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "\"reactions_counts\" IS JSON OBJECT", + "name": "post_reactions_acounts_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "(\"link_id\" IS NULL) = (\"link_url\" IS NULL)", + "name": "post_link_id_check", + "entityType": "checks", + "schema": "public", + "table": "post" + }, + { + "value": "\n CASE \"service\"\n WHEN 'apns' THEN\n \"token\" IS NOT NULL AND\n \"token\" ~ '^[0-9a-f]{64}$' AND\n \"endpoint\" IS NULL AND\n \"p256dh\" IS NULL AND\n \"auth\" IS NULL AND\n \"expiration_time\" IS NULL\n WHEN 'fcm' THEN\n \"token\" IS NOT NULL AND\n length(\"token\") > 0 AND\n \"endpoint\" IS NULL AND\n \"p256dh\" IS NULL AND\n \"auth\" IS NULL AND\n \"expiration_time\" IS NULL\n WHEN 'web_push' THEN\n \"token\" IS NULL AND\n \"endpoint\" IS NOT NULL AND\n length(\"endpoint\") > 0 AND\n \"p256dh\" IS NOT NULL AND\n length(\"p256dh\") > 0 AND\n \"auth\" IS NOT NULL AND\n length(\"auth\") > 0\n END\n ", + "name": "push_notification_target_shape_check", + "entityType": "checks", + "schema": "public", + "table": "push_notification_target" + }, + { + "value": "NOT (\"accepted\" IS NOT NULL AND \"rejected\" IS NOT NULL)", + "name": "quote_request_terminal_state_check", + "entityType": "checks", + "schema": "public", + "table": "quote_request" + }, + { + "value": "\n \"emoji\" IS NOT NULL\n AND length(\"emoji\") > 0\n AND \"emoji\" !~ '^[[:space:]:]+|[[:space:]:]+$'\n AND \"custom_emoji_id\" IS NULL\n OR\n \"emoji\" IS NULL AND \"custom_emoji_id\" IS NOT NULL\n ", + "name": "reaction_emoji_check", + "entityType": "checks", + "schema": "public", + "table": "reaction" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/federation/inbox/subscribe.ts b/federation/inbox/subscribe.ts index ce84d50e5..d665f4d24 100644 --- a/federation/inbox/subscribe.ts +++ b/federation/inbox/subscribe.ts @@ -13,6 +13,7 @@ import { } from "@fedify/vocab"; import { getPersistedActor, persistActor } from "@hackerspub/models/actor"; import type { ContextData } from "@hackerspub/models/context"; +import { refreshNewsScoresForPostId } from "@hackerspub/models/news"; import { createMentionNotification, createQuoteNotification, @@ -213,11 +214,15 @@ export async function onReactionUndoneOnPost( .returning(); if (rows.length < 1) return false; await updateReactionsCounts(db, rows[0].postId); + // A removed remote reaction leaves no recent source row for the sweep, so + // re-score the reacted post's link (if any) here. + await refreshNewsScoresForPostId(db, rows[0].postId); return true; } else if (object instanceof Like || object instanceof EmojiReact) { const reaction = await deleteReaction(db, object, fedCtx); if (reaction == null) return false; await updateReactionsCounts(db, reaction.postId); + await refreshNewsScoresForPostId(db, reaction.postId); return true; } return false; diff --git a/graphql/deno.json b/graphql/deno.json index 1749917eb..7d57532af 100644 --- a/graphql/deno.json +++ b/graphql/deno.json @@ -12,7 +12,7 @@ }, "tasks": { "codegen": "deno run -A --env-file=../.env mod.ts", - "dev": "deno run -A --env-file=../.env main.ts", - "start": "deno run -A --unstable-otel --env-file=../.env main.ts" + "dev": "deno run -A --unstable-cron --env-file=../.env main.ts", + "start": "deno run -A --unstable-otel --unstable-cron --env-file=../.env main.ts" } } diff --git a/graphql/main.ts b/graphql/main.ts index b678f0602..71a7e7a5c 100644 --- a/graphql/main.ts +++ b/graphql/main.ts @@ -2,6 +2,8 @@ import "./instrument.ts"; import { getXForwardedRequest } from "@hongminhee/x-forwarded-fetch"; +import { getLogger } from "@logtape/logtape"; +import { recomputeNewsScores } from "@hackerspub/models/news"; import * as models from "./ai.ts"; import { db } from "./db.ts"; import { drive } from "./drive.ts"; @@ -19,6 +21,29 @@ const appleAppSiteAssociationJson = Deno.readTextFileSync( const yogaServer = createYogaServer(); +// Periodic news-score sweep. The write hook re-scores a link only when the +// link itself is (un)shared, so engagement-driven re-ranking (a new reply, +// quote, or reaction on an existing story) relies on this sweep. It recomputes +// links with any activity since the window, derived from source timestamps. +// The moderator "recompute" mutation is the authoritative full rebuild and +// reconciles anything the incremental/sweep paths miss. Scoped to +// `activeSince` to bound cost; idempotent, so a multi-replica double-fire is +// wasteful but harmless. Lives here in the server entry point (not in +// `mod.ts`) so codegen and tests never register it. +const newsLogger = getLogger(["hackerspub", "graphql", "news"]); +const NEWS_SWEEP_ACTIVE_WINDOW_MS = 30 * 24 * 60 * 60 * 1000; +Deno.cron("recompute-news-scores", "*/5 * * * *", async () => { + try { + const activeSince = new Date(Date.now() - NEWS_SWEEP_ACTIVE_WINDOW_MS); + const result = await recomputeNewsScores(db, { activeSince }); + newsLogger.debug("News score sweep updated {linksUpdated} link(s).", { + linksUpdated: result.linksUpdated, + }); + } catch (error) { + newsLogger.error("News score sweep failed: {error}", { error }); + } +}); + Deno.serve({ port: 8080 }, async (req, info) => { try { req = await getXForwardedRequest(req); diff --git a/graphql/mod.ts b/graphql/mod.ts index 4a864f24f..5c58cf278 100644 --- a/graphql/mod.ts +++ b/graphql/mod.ts @@ -14,6 +14,7 @@ import "./invitation-link.ts"; import "./login.ts"; import "./markdown.ts"; import "./misc.ts"; +import "./news.ts"; import "./notification.ts"; import "./passkey.ts"; import "./poll.ts"; diff --git a/graphql/news.test.ts b/graphql/news.test.ts new file mode 100644 index 000000000..1d4456879 --- /dev/null +++ b/graphql/news.test.ts @@ -0,0 +1,983 @@ +import { assert } from "@std/assert/assert"; +import { assertEquals } from "@std/assert/equals"; +import { recomputeNewsScores } from "@hackerspub/models/news"; +import { accountTable } from "@hackerspub/models/schema"; +import { eq } from "drizzle-orm"; +import { execute, parse } from "graphql"; +import type { Transaction } from "@hackerspub/models/db"; +import { schema } from "./mod.ts"; +import { + type AuthenticatedAccount, + insertAccountWithActor, + insertNotePost, + insertPostLink, + insertRemoteActor, + makeGuestContext, + makeUserContext, + withRollback, +} from "../test/postgres.ts"; + +async function makeModerator( + tx: Transaction, + values: { username: string; name: string; email: string }, +): Promise { + const { account } = await insertAccountWithActor(tx, values); + await tx.update(accountTable).set({ moderator: true }).where( + eq(accountTable.id, account.id), + ); + return { ...account, moderator: true }; +} + +const newsStoriesQuery = parse(` + query NewsStories($order: NewsOrder, $first: Int, $after: String) { + newsStories(order: $order, first: $first, after: $after) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + } + edges { + cursor + node { + url + score + weightedMass + postCount + } + } + } + } +`); + +interface NewsStoriesResult { + newsStories: { + pageInfo: { + hasNextPage: boolean; + hasPreviousPage: boolean; + endCursor: string | null; + }; + edges: { cursor: string; node: { url: string } }[]; + }; +} + +Deno.test({ + name: "newsStories ranks links by score for a guest, popular by default", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "gqlnews", + name: "GQL News", + email: "gqlnews@example.com", + }); + const high = await insertPostLink(tx, { + url: "https://example.com/high", + }); + const low = await insertPostLink(tx, { url: "https://example.com/low" }); + await insertNotePost(tx, { + account: sharer.account, + reactionsCounts: { "❤️": 10 }, + published: new Date("2026-05-20T00:00:00.000Z"), + link: { id: high.id, url: high.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: new Date("2026-05-20T00:00:00.000Z"), + link: { id: low.id, url: low.url }, + }); + await recomputeNewsScores(tx); + + const result = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { first: 10 }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(result.errors, undefined); + const data = result.data as unknown as NewsStoriesResult; + assertEquals(data.newsStories.edges.map((e) => e.node.url), [ + high.url, + low.url, + ]); + }); + }, +}); + +Deno.test({ + name: "newsStories order arg switches between popular, newest, and allTime", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "gqlorder", + name: "GQL Order", + email: "gqlorder@example.com", + }); + const heavyOld = await insertPostLink(tx, { + url: "https://example.com/heavy", + }); + const lightNew = await insertPostLink(tx, { + url: "https://example.com/light", + }); + await insertNotePost(tx, { + account: sharer.account, + reactionsCounts: { "❤️": 100 }, + published: new Date("2025-05-30T00:00:00.000Z"), + link: { id: heavyOld.id, url: heavyOld.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: new Date("2026-05-30T00:00:00.000Z"), + link: { id: lightNew.id, url: lightNew.url }, + }); + await recomputeNewsScores(tx); + + const firstUrl = async (order: string) => { + const result = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { order, first: 10 }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(result.errors, undefined); + return (result.data as unknown as NewsStoriesResult) + .newsStories.edges[0].node.url; + }; + + assertEquals(await firstUrl("POPULAR"), lightNew.url); + assertEquals(await firstUrl("NEWEST"), lightNew.url); + assertEquals(await firstUrl("ALL_TIME"), heavyOld.url); + }); + }, +}); + +Deno.test({ + name: "newsStories paginates forward by cursor without gaps", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "gqlpage", + name: "GQL Page", + email: "gqlpage@example.com", + }); + const urls: string[] = []; + for (let i = 0; i < 3; i++) { + const link = await insertPostLink(tx, { + url: `https://example.com/p${i}`, + }); + await insertNotePost(tx, { + account: sharer.account, + reactionsCounts: { "❤️": i + 1 }, // distinct scores + published: new Date("2026-05-20T00:00:00.000Z"), + link: { id: link.id, url: link.url }, + }); + urls.push(link.url); + } + await recomputeNewsScores(tx); + + const page1 = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { first: 2 }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(page1.errors, undefined); + const data1 = page1.data as unknown as NewsStoriesResult; + assertEquals(data1.newsStories.edges.length, 2); + assert(data1.newsStories.pageInfo.hasNextPage); + + const page2 = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { + first: 2, + after: data1.newsStories.pageInfo.endCursor, + }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(page2.errors, undefined); + const data2 = page2.data as unknown as NewsStoriesResult; + assert(data2.newsStories.pageInfo.hasPreviousPage); + + const seen = [...data1.newsStories.edges, ...data2.newsStories.edges] + .map((e) => e.node.url); + assertEquals(new Set(seen).size, 3); + // Highest score (most reactions) first. + assertEquals(seen[0], urls[2]); + assertEquals(seen[2], urls[0]); + }); + }, +}); + +Deno.test({ + name: "newsStories rejects pages larger than the cap", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const tooBig = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { first: 101 }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assert(tooBig.errors != null && tooBig.errors.length > 0); + assertEquals(tooBig.errors[0].extensions?.code, "PAGINATION_ERROR"); + }); + }, +}); + +Deno.test({ + name: "PostLink exposes sharingPosts and sourceBreakdown", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const local = await insertAccountWithActor(tx, { + username: "gqllocal", + name: "GQL Local", + email: "gqllocal@example.com", + }); + const remote = await insertRemoteActor(tx, { + username: "gqlremote", + name: "GQL Remote", + host: "mastodon.example", + }); + const bridged = await insertRemoteActor(tx, { + username: "gqlbsky.bsky.social", + name: "GQL Bsky", + host: "bsky.brid.gy", + handleHost: "bsky.brid.gy", + }); + const link = await insertPostLink(tx, { url: "https://example.com/mix" }); + await insertNotePost(tx, { + account: local.account, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: local.account, + actorId: remote.id, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: local.account, + actorId: bridged.id, + link: { id: link.id, url: link.url }, + }); + await recomputeNewsScores(tx); + + const result = await execute({ + schema, + document: parse(` + query { + newsStories(first: 10) { + edges { + node { + url + postCount + sourceBreakdown { local remote bluesky } + sharingPosts(first: 10) { + edges { node { __typename } } + } + } + } + } + } + `), + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(result.errors, undefined); + const node = (result.data as { + newsStories: { + edges: { + node: { + url: string; + postCount: number; + sourceBreakdown: { + local: number; + remote: number; + bluesky: number; + }; + sharingPosts: { edges: unknown[] }; + }; + }[]; + }; + }).newsStories.edges[0].node; + assertEquals(node.url, link.url); + assertEquals(node.postCount, 3); + assertEquals(node.sourceBreakdown, { local: 1, remote: 1, bluesky: 1 }); + assertEquals(node.sharingPosts.edges.length, 3); + }); + }, +}); + +Deno.test({ + name: "sharingPosts and postCount exclude bot-account shares", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const local = await insertAccountWithActor(tx, { + username: "gqlbothuman", + name: "GQL Human", + email: "gqlbothuman@example.com", + }); + const bot = await insertRemoteActor(tx, { + username: "gqlbot", + name: "GQL Bot", + host: "bots.example", + type: "Application", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/botmix", + }); + await insertNotePost(tx, { + account: local.account, + link: { id: link.id, url: link.url }, + }); + // A bot's share of the same link must not become a discussion root nor + // inflate the public share count. + await insertNotePost(tx, { + account: local.account, + actorId: bot.id, + link: { id: link.id, url: link.url }, + }); + await recomputeNewsScores(tx); + + const result = await execute({ + schema, + document: parse(` + query { + newsStories(first: 10) { + edges { + node { + url + postCount + sourceBreakdown { local remote bluesky } + sharingPosts(first: 10) { + edges { node { __typename } } + } + } + } + } + } + `), + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(result.errors, undefined); + const node = (result.data as { + newsStories: { + edges: { + node: { + url: string; + postCount: number; + sourceBreakdown: { + local: number; + remote: number; + bluesky: number; + }; + sharingPosts: { edges: unknown[] }; + }; + }[]; + }; + }).newsStories.edges[0].node; + assertEquals(node.url, link.url); + assertEquals(node.postCount, 1); + assertEquals(node.sourceBreakdown, { local: 1, remote: 0, bluesky: 0 }); + assertEquals(node.sharingPosts.edges.length, 1); + }); + }, +}); + +Deno.test({ + name: "newsStories omits a link shared only by a bot account", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const host = await insertAccountWithActor(tx, { + username: "gqlbotonly", + name: "GQL Bot Only", + email: "gqlbotonly@example.com", + }); + const bot = await insertRemoteActor(tx, { + username: "gqlonlybot", + name: "GQL Only Bot", + host: "bots.example", + type: "Service", + }); + const humanLink = await insertPostLink(tx, { + url: "https://example.com/gqlhuman", + }); + const botLink = await insertPostLink(tx, { + url: "https://example.com/gqlbotonly", + }); + await insertNotePost(tx, { + account: host.account, + link: { id: humanLink.id, url: humanLink.url }, + }); + await insertNotePost(tx, { + account: host.account, + actorId: bot.id, + link: { id: botLink.id, url: botLink.url }, + }); + await recomputeNewsScores(tx); + + const result = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { first: 10 }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(result.errors, undefined); + const urls = (result.data as unknown as NewsStoriesResult) + .newsStories.edges.map((e) => e.node.url); + assert(urls.includes(humanLink.url)); + assert(!urls.includes(botLink.url)); + }); + }, +}); + +Deno.test({ + name: "PostLink discussionCount counts shares plus public replies and quotes", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const human = await insertAccountWithActor(tx, { + username: "gqldisc", + name: "GQL Disc", + email: "gqldisc@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/gqldisc", + }); + const { post: share } = await insertNotePost(tx, { + account: human.account, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: human.account, + replyTargetId: share.id, + }); + await insertNotePost(tx, { + account: human.account, + quotedPostId: share.id, + }); + // A followers-only reply must not inflate the public count. + await insertNotePost(tx, { + account: human.account, + visibility: "followers", + replyTargetId: share.id, + }); + await recomputeNewsScores(tx); + + const result = await execute({ + schema, + document: parse(` + query Q($id: UUID!) { + newsStory(id: $id) { discussionCount } + } + `), + variableValues: { id: link.id }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(result.errors, undefined); + assertEquals( + (result.data as { newsStory: { discussionCount: number } | null }) + .newsStory?.discussionCount, + 3, + ); + }); + }, +}); + +Deno.test({ + name: "newsStories rejects backward pagination", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const result = await execute({ + schema, + document: parse(` + query { + newsStories(last: 5) { + edges { node { url } } + } + } + `), + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assert(result.errors != null && result.errors.length > 0); + assertEquals(result.errors[0].extensions?.code, "PAGINATION_ERROR"); + }); + }, +}); + +const statusQuery = parse(` + query NewsScoreStatus { + newsScoreStatus { + scoredLinkCount + lastRecomputedAt + } + } +`); + +Deno.test({ + name: "newsStory looks a link up by uuid for the discussion permalink", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "storylookup", + name: "Story Lookup", + email: "storylookup@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/by" }); + await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + await recomputeNewsScores(tx); + + const doc = parse(` + query Story($id: UUID!) { + newsStory(id: $id) { uuid url postCount } + } + `); + const found = await execute({ + schema, + document: doc, + variableValues: { id: link.id }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(found.errors, undefined); + const story = (found.data as { + newsStory: { uuid: string; url: string; postCount: number } | null; + }).newsStory; + assertEquals(story?.uuid, link.id); + assertEquals(story?.url, link.url); + assertEquals(story?.postCount, 1); + + // A well-formed but unknown uuid resolves to null (not an error). + const missing = await execute({ + schema, + document: doc, + variableValues: { id: "00000000-0000-7000-8000-000000000000" }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(missing.errors, undefined); + assertEquals( + (missing.data as { newsStory: unknown }).newsStory, + null, + ); + }); + }, +}); + +Deno.test({ + name: "newsScoreStatus is null for guests and non-moderators", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const guest = await execute({ + schema, + document: statusQuery, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(guest.errors, undefined); + assertEquals( + (guest.data as { newsScoreStatus: unknown }).newsScoreStatus, + null, + ); + + const { account } = await insertAccountWithActor(tx, { + username: "plainviewer", + name: "Plain Viewer", + email: "plainviewer@example.com", + }); + const nonMod = await execute({ + schema, + document: statusQuery, + contextValue: makeUserContext(tx, account), + onError: "NO_PROPAGATE", + }); + assertEquals(nonMod.errors, undefined); + assertEquals( + (nonMod.data as { newsScoreStatus: unknown }).newsScoreStatus, + null, + ); + }); + }, +}); + +const recomputeMutation = parse(` + mutation Recompute { + recomputeNewsScores { + __typename + ... on RecomputeNewsScoresPayload { + linksUpdated + status { scoredLinkCount lastRecomputedAt } + } + } + } +`); + +Deno.test({ + name: "recomputeNewsScores rejects guests and non-moderators", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const guest = await execute({ + schema, + document: recomputeMutation, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(guest.errors, undefined); + assertEquals( + (guest.data as { recomputeNewsScores: { __typename: string } }) + .recomputeNewsScores.__typename, + "NotAuthenticatedError", + ); + + const { account } = await insertAccountWithActor(tx, { + username: "nonmod", + name: "Non Mod", + email: "nonmod@example.com", + }); + const nonMod = await execute({ + schema, + document: recomputeMutation, + contextValue: makeUserContext(tx, account), + onError: "NO_PROPAGATE", + }); + assertEquals(nonMod.errors, undefined); + assertEquals( + (nonMod.data as { recomputeNewsScores: { __typename: string } }) + .recomputeNewsScores.__typename, + "NotAuthorizedError", + ); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores rebuilds scores for a moderator", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const moderator = await makeModerator(tx, { + username: "newsmod", + name: "News Mod", + email: "newsmod@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/mod" }); + await insertNotePost(tx, { + account: moderator, + link: { id: link.id, url: link.url }, + }); + // Note: the incremental write hook already scored the link, but the + // mutation must still report a consistent post-run status. + + const result = await execute({ + schema, + document: recomputeMutation, + contextValue: makeUserContext(tx, moderator), + onError: "NO_PROPAGATE", + }); + assertEquals(result.errors, undefined); + const payload = (result.data as { + recomputeNewsScores: { + __typename: string; + linksUpdated: number; + status: { scoredLinkCount: number; lastRecomputedAt: string | null }; + }; + }).recomputeNewsScores; + assertEquals(payload.__typename, "RecomputeNewsScoresPayload"); + assertEquals(payload.linksUpdated, 1); + assertEquals(payload.status.scoredLinkCount, 1); + assert(payload.status.lastRecomputedAt != null); + + // The feed now lists the recomputed story. + const feed = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { first: 10 }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(feed.errors, undefined); + assertEquals( + (feed.data as unknown as NewsStoriesResult).newsStories.edges[0].node + .url, + link.url, + ); + }); + }, +}); + +// --------------------------------------------------------------------------- +// Moderation: score penalty + URL exclusions +// --------------------------------------------------------------------------- + +const setPenaltyMutation = parse(` + mutation SetPenalty($id: UUID!, $penalty: NewsPenalty!) { + setNewsScorePenalty(id: $id, penalty: $penalty) { + __typename + ... on PostLink { uuid penalty } + ... on NotAuthenticatedError { notAuthenticated } + ... on NotAuthorizedError { notAuthorized } + } + } +`); + +function penaltyTypename(data: unknown): string { + return (data as { setNewsScorePenalty: { __typename: string } }) + .setNewsScorePenalty.__typename; +} + +Deno.test({ + name: "setNewsScorePenalty demotes for a moderator and rejects others", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const moderator = await makeModerator(tx, { + username: "penmod", + name: "Pen Mod", + email: "penmod@example.com", + }); + const a = await insertPostLink(tx, { url: "https://example.com/pena" }); + const b = await insertPostLink(tx, { url: "https://example.com/penb" }); + const at = new Date("2026-05-20T00:00:00.000Z"); + await insertNotePost(tx, { + account: moderator, + published: at, + link: { id: a.id, url: a.url }, + }); + await insertNotePost(tx, { + account: moderator, + published: at, + link: { id: b.id, url: b.url }, + }); + await recomputeNewsScores(tx); + + // Guest and non-moderator are rejected. + const guest = await execute({ + schema, + document: setPenaltyMutation, + variableValues: { id: a.id, penalty: "DEMOTE" }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(guest.errors, undefined); + assertEquals(penaltyTypename(guest.data), "NotAuthenticatedError"); + + const { account: plain } = await insertAccountWithActor(tx, { + username: "penplain", + name: "Pen Plain", + email: "penplain@example.com", + }); + const nonMod = await execute({ + schema, + document: setPenaltyMutation, + variableValues: { id: a.id, penalty: "DEMOTE" }, + contextValue: makeUserContext(tx, plain), + onError: "NO_PROPAGATE", + }); + assertEquals(nonMod.errors, undefined); + assertEquals(penaltyTypename(nonMod.data), "NotAuthorizedError"); + + // A moderator demotes link A. + const set = await execute({ + schema, + document: setPenaltyMutation, + variableValues: { id: a.id, penalty: "DEMOTE" }, + contextValue: makeUserContext(tx, moderator), + onError: "NO_PROPAGATE", + }); + assertEquals(set.errors, undefined); + const payload = (set.data as { + setNewsScorePenalty: { __typename: string; penalty: string }; + }).setNewsScorePenalty; + assertEquals(payload.__typename, "PostLink"); + assertEquals(payload.penalty, "DEMOTE"); + + // The unpenalized peer now ranks above the demoted link in POPULAR. + const feed = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { first: 10 }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals(feed.errors, undefined); + const urls = (feed.data as unknown as NewsStoriesResult).newsStories.edges + .map((e) => e.node.url); + assert(urls.indexOf(b.url) < urls.indexOf(a.url)); + }); + }, +}); + +const addPatternMutation = parse(` + mutation AddPattern($pattern: String!, $note: String) { + addNewsExcludedPattern(pattern: $pattern, note: $note) { + __typename + ... on NewsExcludedPattern { id pattern note } + ... on NotAuthenticatedError { notAuthenticated } + ... on NotAuthorizedError { notAuthorized } + ... on InvalidInputError { inputPath } + } + } +`); +const patternsQuery = parse( + `query { newsExcludedPatterns { id pattern note } }`, +); +const removePatternMutation = parse(` + mutation RemovePattern($id: UUID!) { + removeNewsExcludedPattern(id: $id) { + __typename + ... on RemoveNewsExcludedPatternPayload { removedId } + ... on NotAuthenticatedError { notAuthenticated } + ... on NotAuthorizedError { notAuthorized } + } + } +`); + +Deno.test({ + name: "news exclusion patterns hide links and are moderator-only", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const moderator = await makeModerator(tx, { + username: "exclmod", + name: "Excl Mod", + email: "exclmod@example.com", + }); + const spam = await insertPostLink(tx, { url: "https://spam.example/x" }); + const good = await insertPostLink(tx, { url: "https://good.example/y" }); + await insertNotePost(tx, { + account: moderator, + link: { id: spam.id, url: spam.url }, + }); + await insertNotePost(tx, { + account: moderator, + link: { id: good.id, url: good.url }, + }); + await recomputeNewsScores(tx); + + // Guests cannot add patterns or read the list. + const guestAdd = await execute({ + schema, + document: addPatternMutation, + variableValues: { pattern: "https://spam.example/*", note: null }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals( + (guestAdd.data as { addNewsExcludedPattern: { __typename: string } }) + .addNewsExcludedPattern.__typename, + "NotAuthenticatedError", + ); + const guestList = await execute({ + schema, + document: patternsQuery, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + assertEquals( + (guestList.data as { newsExcludedPatterns: unknown }) + .newsExcludedPatterns, + null, + ); + + // An invalid pattern is an InvalidInputError. + const bad = await execute({ + schema, + document: addPatternMutation, + variableValues: { pattern: "https://example.com/(", note: null }, + contextValue: makeUserContext(tx, moderator), + onError: "NO_PROPAGATE", + }); + assertEquals( + (bad.data as { addNewsExcludedPattern: { __typename: string } }) + .addNewsExcludedPattern.__typename, + "InvalidInputError", + ); + + // A moderator adds a valid pattern; the spam link leaves the feed. + const add = await execute({ + schema, + document: addPatternMutation, + variableValues: { pattern: "https://spam.example/*", note: "spam" }, + contextValue: makeUserContext(tx, moderator), + onError: "NO_PROPAGATE", + }); + const added = (add.data as { + addNewsExcludedPattern: { __typename: string; id: string }; + }).addNewsExcludedPattern; + assertEquals(added.__typename, "NewsExcludedPattern"); + + const feed = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { first: 10 }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + const urls = (feed.data as unknown as NewsStoriesResult).newsStories.edges + .map((e) => e.node.url); + assert(!urls.includes(spam.url)); + assert(urls.includes(good.url)); + + // Removing the pattern restores the link. + const remove = await execute({ + schema, + document: removePatternMutation, + variableValues: { id: added.id }, + contextValue: makeUserContext(tx, moderator), + onError: "NO_PROPAGATE", + }); + assertEquals( + (remove.data as { + removeNewsExcludedPattern: { __typename: string; removedId: string }; + }).removeNewsExcludedPattern.removedId, + added.id, + ); + const feed2 = await execute({ + schema, + document: newsStoriesQuery, + variableValues: { first: 10 }, + contextValue: makeGuestContext(tx), + onError: "NO_PROPAGATE", + }); + const urls2 = (feed2.data as unknown as NewsStoriesResult).newsStories + .edges.map((e) => e.node.url); + assert(urls2.includes(spam.url)); + }); + }, +}); diff --git a/graphql/news.ts b/graphql/news.ts new file mode 100644 index 000000000..76f74acb0 --- /dev/null +++ b/graphql/news.ts @@ -0,0 +1,647 @@ +import { createGraphQLError } from "graphql-yoga"; +import { + addNewsExcludedPattern, + getNewsDiscussionCounts, + getNewsExcludedPatterns, + getNewsPenalizedStories, + getNewsScoreStatus, + getNewsSourceBreakdowns, + getNewsStories, + InvalidNewsPatternError, + NEWS_BOT_ACTOR_TYPES, + NEWS_PENALTY_BURY, + NEWS_PENALTY_DEMOTE, + type NewsOrder as NewsOrderValue, + type NewsStoriesCursor, + recomputeNewsScores, + removeNewsExcludedPattern, + setNewsScorePenalty, +} from "@hackerspub/models/news"; +import { getPostVisibilityFilter } from "@hackerspub/models/post"; +import type { PostLink as PostLinkRow } from "@hackerspub/models/schema"; +import { type Uuid, validateUuid } from "@hackerspub/models/uuid"; +import { builder } from "./builder.ts"; +import { InvalidInputError, NotAuthorizedError } from "./error.ts"; +import { Post, PostLink } from "./post.ts"; +import { NotAuthenticatedError } from "./session.ts"; + +const MAX_NEWS_WINDOW = 100; + +// --------------------------------------------------------------------------- +// Ordering +// --------------------------------------------------------------------------- + +export const NewsOrder = builder.enumType("NewsOrder", { + description: "Ordering for the `newsStories` feed of shared links.", + values: { + POPULAR: { + value: "popular", + description: + "Hacker-News-style score combining weighted engagement mass with " + + "recency. The default; what most readers want.", + }, + NEWEST: { + value: "newest", + description: + "Most recently first-shared links first, ignoring engagement.", + }, + ALL_TIME: { + value: "allTime", + description: + "Highest total weighted engagement mass first, ignoring recency: an " + + "all-time-best view rather than what is hot right now.", + }, + } as const, +}); + +// --------------------------------------------------------------------------- +// Moderator score penalty +// --------------------------------------------------------------------------- + +type NewsPenaltyValue = "none" | "demote" | "bury"; + +export const NewsPenalty = builder.enumType("NewsPenalty", { + description: + "A moderator's score penalty on a news link, demoting it in the " + + "`POPULAR` feed. Set via `setNewsScorePenalty`; only moderators can read " + + "or change it. To remove a link from every order instead, exclude its " + + "URL with a pattern (`addNewsExcludedPattern`).", + values: { + NONE: { value: "none", description: "No penalty." }, + DEMOTE: { + value: "demote", + description: "Push the link well down the popular feed.", + }, + BURY: { + value: "bury", + description: "Sink the link to the bottom of the popular feed.", + }, + } as const, +}); + +const PENALTY_VALUE: Record = { + none: 0, + demote: NEWS_PENALTY_DEMOTE, + bury: NEWS_PENALTY_BURY, +}; + +function penaltyLevel(scorePenalty: number): NewsPenaltyValue { + if (scorePenalty <= 0) return "none"; + if (scorePenalty >= NEWS_PENALTY_BURY) return "bury"; + return "demote"; +} + +// --------------------------------------------------------------------------- +// PostLink scoring fields +// --------------------------------------------------------------------------- + +const NewsSourceBreakdown = builder.simpleObject("NewsSourceBreakdown", { + description: + "How a link's public shares break down by origin. Hackers' Pub posts " + + "carry the most weight, generic remote instances less, and Bluesky-" + + "bridged accounts (`@…@bsky.brid.gy`) the least. Shares authored by bot " + + "accounts (`Service`/`Application` actors) are excluded throughout.", + fields: (t) => ({ + local: t.int({ + description: "Public shares authored by local Hackers' Pub accounts.", + }), + remote: t.int({ + description: + "Public shares from generic remote fediverse instances (Mastodon, " + + "Pleroma, etc.).", + }), + bluesky: t.int({ + description: "Public shares bridged from Bluesky (`@…@bsky.brid.gy`).", + }), + }), +}); + +builder.drizzleObjectFields(PostLink, (t) => ({ + uuid: t.expose("id", { + type: "UUID", + description: + "The link's row UUID. Use this for the stable discussion permalink " + + "`/news/{uuid}`; the opaque Relay `id` is for `node(id:)` lookups.", + }), + score: t.exposeFloat("score", { + description: + "Popularity-over-time rank used by the `POPULAR` feed order: " + + "`log10(max(1, weightedMass)) + recency`. Computed by a batch job and " + + "refreshed incrementally; recompute is idempotent and the value is " + + "time-stable (it changes only when the underlying posts or engagement " + + "change, not as the clock advances). Shares from bot accounts " + + "(`Service`/`Application` actors) never count, so a link shared only by " + + "bots stays at `0`, as do links never shared publicly. When one account " + + "shares the same link repeatedly, each share after the first adds only a " + + "small, gap-dependent fraction of the base weight (and a rapid repeat " + + "does not refresh recency), so re-posting cannot inflate the rank.", + }), + weightedMass: t.exposeFloat("weightedMass", { + description: + "Recency-independent engagement mass: the weighted sum over this " + + "link's public shares (excluding bot `Service`/`Application` accounts) " + + "of source weight times account reputation times (quotes, replies, " + + "reactions). Repeated shares of the same link by the same account add " + + "diminishing base weight (recovering with the gap but always below a " + + "first share). Drives the `ALL_TIME` order.", + }), + postCount: t.exposeInt("postCount", { + description: + "Number of public, non-boost posts across the fediverse that share " + + "this link, excluding shares from bot (`Service`/`Application`) " + + "accounts. Counts every such post, including an account's repeated " + + "shares of the same link (a high count with a modest `score` is " + + "expected, since repeats contribute diminishing weight).", + }), + firstSharedAt: t.expose("firstSharedAt", { + type: "DateTime", + nullable: true, + description: + "When this link was first shared publicly by a non-bot account, or " + + "`null` if it has never been. Drives the `NEWEST` order.", + }), + latestActivityAt: t.expose("latestActivityAt", { + type: "DateTime", + nullable: true, + description: + "Timestamp of the freshest activity on this link's qualifying shares " + + "(the share itself, a reply, a quote, or a reaction); shares are public " + + "and authored by non-bot accounts. A rapid repeat share by the same " + + "account does not refresh this (only a first share, a sufficiently-" + + "gapped re-share, or genuine replies/quotes/reactions do), so re-posting " + + "cannot keep a link pinned at the top. `null` means the link is not a " + + "news story (no qualifying public share); such links are excluded from " + + "the feed.", + }), + sharingPosts: t.relatedConnection("posts", { + type: Post, + description: + "The posts that share this link, most recently published first, " + + "filtered to those visible to the viewer. Shares authored by bot " + + "accounts (`Service`/`Application` actors) are excluded, matching the " + + "scoring. These are the roots of the link's discussion tree.", + query: (_args, ctx) => ({ + where: { + AND: [ + getPostVisibilityFilter(ctx.account?.actor ?? null), + { actor: { type: { notIn: [...NEWS_BOT_ACTOR_TYPES] } } }, + ], + }, + orderBy: { published: "desc" }, + }), + }), + sourceBreakdown: t.loadable({ + type: NewsSourceBreakdown, + description: + "Counts of this link's public shares by origin (local / remote / " + + "Bluesky bridge), excluding shares from bot (`Service`/`Application`) " + + "accounts.", + resolve: (link) => link.id, + load: async (linkIds: Uuid[], ctx) => { + const breakdowns = await getNewsSourceBreakdowns(ctx.db, linkIds); + return linkIds.map((id) => + breakdowns.get(id) ?? { local: 0, remote: 0, bluesky: 0 } + ); + }, + }), + discussionCount: t.loadable({ + type: "Int", + description: + "Size of this link's federated discussion: its non-bot public sharing " + + "posts plus their direct public (`public`/`unlisted`) replies and " + + "quotes. Use this as the count of posts to read in the discussion " + + "(the `/news/{uuid}` page); unlike `postCount` it includes the replies " + + "and quotes, not just the shares. Counts direct children only (deeper " + + "nesting is not traversed) and is viewer-independent (public posts " + + "only).", + resolve: (link) => link.id, + load: async (linkIds: Uuid[], ctx) => { + const counts = await getNewsDiscussionCounts(ctx.db, linkIds); + return linkIds.map((id) => counts.get(id) ?? 0); + }, + }), + penalty: t.field({ + type: NewsPenalty, + nullable: true, + description: + "The moderator score penalty on this link (demoting it in the " + + "`POPULAR` feed). `null` for non-moderators; moderators see `NONE` " + + "when the link is unpenalized. Set it with `setNewsScorePenalty`.", + select: { columns: { scorePenalty: true } }, + resolve(link, _args, ctx) { + if (ctx.session == null || !ctx.account?.moderator) return null; + return penaltyLevel(link.scorePenalty); + }, + }), +})); + +// --------------------------------------------------------------------------- +// Feed query +// --------------------------------------------------------------------------- + +function invalidNewsCursor(): never { + throw createGraphQLError("Invalid news cursor.", { + extensions: { code: "INVALID_CURSOR" }, + }); +} + +function newsWindow(first: number | null | undefined): number { + const window = first ?? 25; + if (window < 1) { + throw createGraphQLError( + "Page size must be at least 1.", + { extensions: { code: "PAGINATION_ERROR" } }, + ); + } + if (window > MAX_NEWS_WINDOW) { + throw createGraphQLError( + `News pages are limited to ${MAX_NEWS_WINDOW} stories.`, + { extensions: { code: "PAGINATION_ERROR" } }, + ); + } + return window; +} + +function cursorScalar(link: PostLinkRow, order: NewsOrderValue): string { + switch (order) { + case "newest": + return link.firstSharedAt?.toISOString() ?? ""; + case "allTime": + return String(link.weightedMass); + case "popular": + return String(link.score); + } +} + +function formatNewsCursor(link: PostLinkRow, order: NewsOrderValue): string { + return `${cursorScalar(link, order)}|${link.id}`; +} + +function parseNewsCursor( + raw: string, + order: NewsOrderValue, +): NewsStoriesCursor { + const i = raw.lastIndexOf("|"); + if (i < 0) invalidNewsCursor(); + const scalar = raw.slice(0, i); + const id = raw.slice(i + 1); + if (!validateUuid(id)) invalidNewsCursor(); + if (order === "newest") { + const value = new Date(scalar); + if (isNaN(value.getTime())) invalidNewsCursor(); + return { value, id: id as Uuid }; + } + const value = Number(scalar); + if (!Number.isFinite(value)) invalidNewsCursor(); + return { value, id: id as Uuid }; +} + +builder.queryField("newsStories", (t) => + t.connection({ + type: PostLink, + description: + "Links shared across the fediverse, ranked as a news feed. Forward " + + "pagination only (`first`/`after`); `last`/`before` raise a " + + "`PAGINATION_ERROR`. Pages are capped at " + + `${MAX_NEWS_WINDOW} stories. No authentication required.`, + args: { + order: t.arg({ + type: NewsOrder, + defaultValue: "popular", + description: "How to rank the feed. Defaults to `POPULAR`.", + }), + }, + async resolve(_, args, ctx) { + if (args.before != null || args.last != null) { + throw createGraphQLError( + "The news feed supports forward pagination only.", + { extensions: { code: "PAGINATION_ERROR" } }, + ); + } + const order = args.order as NewsOrderValue; + const window = newsWindow(args.first); + const after = args.after == null + ? undefined + : parseNewsCursor(args.after, order); + const stories = await getNewsStories(ctx.db, { + order, + limit: window + 1, + after, + }); + const hasNextPage = stories.length > window; + const page = stories.slice(0, window); + return { + pageInfo: { + hasNextPage, + hasPreviousPage: args.after != null, + startCursor: page.length < 1 + ? null + : formatNewsCursor(page[0], order), + endCursor: page.length < 1 + ? null + : formatNewsCursor(page[page.length - 1], order), + }, + edges: page.map((link) => ({ + node: link, + cursor: formatNewsCursor(link, order), + })), + }; + }, + })); + +builder.queryField("newsStory", (t) => + t.drizzleField({ + type: PostLink, + nullable: true, + description: + "Look up a news story (a shared link) by its row UUID, for the " + + "discussion permalink `/news/{uuid}`. Returns `null` for a malformed " + + "id, or for a link that is not a public news story: only links with a " + + "qualifying public share (`latestActivityAt` is not `null`) resolve, so " + + "a link seen only in followers-only or direct posts stays private. A " + + "link hidden from the feed by an exclusion pattern (`excludedFromNews`) " + + "is still reachable here.", + args: { + id: t.arg({ + type: "UUID", + required: true, + description: + "The link's row UUID (`PostLink.uuid`), as embedded in the " + + "`/news/{uuid}` permalink.", + }), + }, + resolve(query, _root, args, ctx) { + if (!validateUuid(args.id)) return null; + return ctx.db.query.postLinkTable.findFirst( + query({ + where: { id: args.id, latestActivityAt: { isNotNull: true } }, + }), + ); + }, + })); + +// --------------------------------------------------------------------------- +// Admin: status + manual recompute +// --------------------------------------------------------------------------- + +const NewsScoreStatus = builder.simpleObject("NewsScoreStatus", { + description: + "A snapshot of news scoring state, for the moderator admin page.", + fields: (t) => ({ + scoredLinkCount: t.int({ + description: + "Number of links currently in the feed (with at least one public " + + "share).", + }), + lastRecomputedAt: t.field({ + type: "DateTime", + nullable: true, + description: "When scores were last recomputed, or `null` if never.", + }), + }), +}); + +builder.queryField("newsScoreStatus", (t) => + t.field({ + type: NewsScoreStatus, + nullable: true, + description: + "Moderator-only news scoring snapshot. Returns `null` when the viewer " + + "is not a moderator; routes should guard with `viewer.moderator`.", + async resolve(_root, _args, ctx) { + if (ctx.session == null) return null; + if (!ctx.account?.moderator) return null; + return await getNewsScoreStatus(ctx.db); + }, + })); + +const RecomputeNewsScoresPayload = builder.simpleObject( + "RecomputeNewsScoresPayload", + { + description: "The result of a full news score recompute.", + fields: (t) => ({ + linksUpdated: t.int({ + description: + "Number of links with at least one qualifying public share that " + + "were (re)scored by this run. Stale links dropped from the feed " + + "(they lost their last public share) are reset to zero but not " + + "counted here.", + }), + recomputedAt: t.field({ + type: "DateTime", + description: "When the recompute ran.", + }), + status: t.field({ + type: NewsScoreStatus, + description: "The scoring status after the run.", + }), + }), + }, +); + +builder.mutationField("recomputeNewsScores", (t) => + t.field({ + type: RecomputeNewsScoresPayload, + description: + "Recompute popularity scores for every news link. Requires a " + + "moderator account. Idempotent: safe to trigger at any time, and " + + "running it twice on unchanged data yields identical scores. Normally " + + "scores stay fresh on their own (incrementally on share, plus a " + + "periodic sweep); this is the manual full rebuild and dev backstop.", + errors: { + types: [NotAuthenticatedError, NotAuthorizedError], + }, + async resolve(_root, _args, ctx) { + if (ctx.session == null) throw new NotAuthenticatedError(); + if (!ctx.account?.moderator) throw new NotAuthorizedError(); + const result = await recomputeNewsScores(ctx.db); + const status = await getNewsScoreStatus(ctx.db); + return { + linksUpdated: result.linksUpdated, + recomputedAt: result.recomputedAt, + status, + }; + }, + })); + +// --------------------------------------------------------------------------- +// Admin: per-link score penalty +// --------------------------------------------------------------------------- + +builder.mutationField("setNewsScorePenalty", (t) => + t.field({ + type: PostLink, + nullable: true, + description: + "Set (or clear with `NONE`) a moderator score penalty on a news link, " + + "demoting it in the `POPULAR` feed. Requires a moderator account. " + + "Returns the updated link, or `null` if no link has that id.", + errors: { types: [NotAuthenticatedError, NotAuthorizedError] }, + args: { + id: t.arg({ + type: "UUID", + required: true, + description: "The link's row UUID (`PostLink.uuid`).", + }), + penalty: t.arg({ + type: NewsPenalty, + required: true, + description: "The penalty level to apply (`NONE` clears it).", + }), + }, + async resolve(_root, args, ctx) { + if (ctx.session == null) throw new NotAuthenticatedError(); + if (!ctx.account?.moderator) throw new NotAuthorizedError(); + const link = await ctx.db.query.postLinkTable.findFirst({ + where: { id: args.id }, + }); + if (link == null) return null; + await setNewsScorePenalty(ctx.db, args.id, PENALTY_VALUE[args.penalty]); + return await ctx.db.query.postLinkTable.findFirst({ + where: { id: args.id }, + }) ?? null; + }, + })); + +builder.queryField("newsPenalizedStories", (t) => + t.field({ + type: [PostLink], + nullable: true, + description: + "Moderator-only list of news links currently carrying a score penalty, " + + "heaviest first, for reviewing/clearing demotions (a buried link is no " + + "longer near the top of the feed). `null` for non-moderators.", + async resolve(_root, _args, ctx) { + if (ctx.session == null) return null; + if (!ctx.account?.moderator) return null; + return await getNewsPenalizedStories(ctx.db); + }, + })); + +// --------------------------------------------------------------------------- +// Admin: URL exclusion patterns +// --------------------------------------------------------------------------- + +const NewsExcludedPattern = builder.simpleObject("NewsExcludedPattern", { + description: + "A moderator-managed `URLPattern` string; links whose URL matches it are " + + "hidden from the news feed list (every sort order). Their discussion " + + "page stays reachable by direct link.", + fields: (t) => ({ + id: t.field({ type: "UUID", description: "The pattern's row UUID." }), + pattern: t.string({ + description: "The `URLPattern` string, e.g. `https://example.com/*` or " + + "`https://*.example.com/*`.", + }), + note: t.string({ + nullable: true, + description: "An optional moderator note explaining the exclusion.", + }), + created: t.field({ + type: "DateTime", + description: "When the pattern was added.", + }), + }), +}); + +builder.queryField("newsExcludedPatterns", (t) => + t.field({ + type: [NewsExcludedPattern], + nullable: true, + description: + "Moderator-only list of news feed exclusion patterns, newest first. " + + "`null` for non-moderators.", + async resolve(_root, _args, ctx) { + if (ctx.session == null) return null; + if (!ctx.account?.moderator) return null; + const rows = await getNewsExcludedPatterns(ctx.db); + return rows.map((row) => ({ + id: row.id, + pattern: row.pattern, + note: row.note, + created: row.created, + })); + }, + })); + +builder.mutationField("addNewsExcludedPattern", (t) => + t.field({ + type: NewsExcludedPattern, + description: + "Add a news feed exclusion pattern (a `URLPattern` string) and hide " + + "matching links from the feed list. Idempotent on the pattern string. " + + "Requires a moderator account. An invalid pattern raises " + + "`InvalidInputError`.", + errors: { + types: [NotAuthenticatedError, NotAuthorizedError, InvalidInputError], + }, + args: { + pattern: t.arg.string({ + required: true, + description: + "The `URLPattern` string to exclude, e.g. `https://example.com/*`.", + }), + note: t.arg.string({ + description: "An optional note explaining the exclusion.", + }), + }, + async resolve(_root, args, ctx) { + if (ctx.session == null) throw new NotAuthenticatedError(); + if (!ctx.account?.moderator) throw new NotAuthorizedError(); + try { + const row = await addNewsExcludedPattern(ctx.db, { + pattern: args.pattern, + note: args.note ?? null, + creatorId: ctx.account.id, + }); + return { + id: row.id, + pattern: row.pattern, + note: row.note, + created: row.created, + }; + } catch (error) { + if (error instanceof InvalidNewsPatternError) { + throw new InvalidInputError("pattern"); + } + throw error; + } + }, + })); + +const RemoveNewsExcludedPatternPayload = builder.simpleObject( + "RemoveNewsExcludedPatternPayload", + { + description: "The result of removing a news feed exclusion pattern.", + fields: (t) => ({ + removedId: t.field({ + type: "UUID", + nullable: true, + description: + "The removed pattern's id, or `null` if no pattern had that id.", + }), + }), + }, +); + +builder.mutationField("removeNewsExcludedPattern", (t) => + t.field({ + type: RemoveNewsExcludedPatternPayload, + description: + "Remove a news feed exclusion pattern by id, un-hiding links it no " + + "longer matches. Requires a moderator account.", + errors: { types: [NotAuthenticatedError, NotAuthorizedError] }, + args: { + id: t.arg({ + type: "UUID", + required: true, + description: "The pattern's row UUID.", + }), + }, + async resolve(_root, args, ctx) { + if (ctx.session == null) throw new NotAuthenticatedError(); + if (!ctx.account?.moderator) throw new NotAuthorizedError(); + const removed = await removeNewsExcludedPattern(ctx.db, args.id); + return { removedId: removed ? args.id : null }; + }, + })); diff --git a/graphql/post.ts b/graphql/post.ts index 73695d08c..60b1b5340 100644 --- a/graphql/post.ts +++ b/graphql/post.ts @@ -1300,7 +1300,7 @@ const MediumUploadHeader = builder.simpleObject("MediumUploadHeader", { }), }); -const PostLink = builder.drizzleNode("postLinkTable", { +export const PostLink = builder.drizzleNode("postLinkTable", { variant: "PostLink", description: "OpenGraph / oEmbed metadata for a link embedded in a post. " + "Populated asynchronously after the post is created; individual " + diff --git a/graphql/schema.graphql b/graphql/schema.graphql index f54a005bf..e0245f209 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -563,6 +563,8 @@ enum ActorType { SERVICE } +union AddNewsExcludedPatternResult = InvalidInputError | NewsExcludedPattern | NotAuthenticatedError | NotAuthorizedError + """ Add an emoji reaction to a post. Sends an ActivityPub `Like` or `EmojiReact` activity. Idempotent: adding the same emoji twice has no effect. Exactly one of `emoji` or `customEmojiId` must be provided. Requires authentication. """ @@ -1367,6 +1369,16 @@ type MentionNotification implements Node & Notification { } type Mutation { + """ + Add a news feed exclusion pattern (a `URLPattern` string) and hide matching links from the feed list. Idempotent on the pattern string. Requires a moderator account. An invalid pattern raises `InvalidInputError`. + """ + addNewsExcludedPattern( + """An optional note explaining the exclusion.""" + note: String + + """The `URLPattern` string to exclude, e.g. `https://example.com/*`.""" + pattern: String! + ): AddNewsExcludedPatternResult! addReactionToPost(input: AddReactionToPostInput!): AddReactionToPostResult! attachArticleDraftMedium(input: AttachArticleDraftMediumInput!): AttachArticleDraftMediumResult! attachArticleSourceMedium(input: AttachArticleSourceMediumInput!): AttachArticleSourceMediumResult! @@ -1502,6 +1514,11 @@ type Mutation { pinHashtag(input: PinHashtagInput!): PinHashtagResult! pinPost(input: PinPostInput!): PinPostResult! publishArticleDraft(input: PublishArticleDraftInput!): PublishArticleDraftResult! + + """ + Recompute popularity scores for every news link. Requires a moderator account. Idempotent: safe to trigger at any time, and running it twice on unchanged data yields identical scores. Normally scores stay fresh on their own (incrementally on share, plus a periodic sweep); this is the manual full rebuild and dev backstop. + """ + recomputeNewsScores: RecomputeNewsScoresResult! redeemInvitationLink(email: Email!, id: UUID!, locale: Locale!, verifyUrl: URITemplate!): RedeemInvitationLinkResult! """ @@ -1524,6 +1541,14 @@ type Mutation { """ registerPushNotificationTarget(input: RegisterPushNotificationTargetInput!): RegisterPushNotificationTargetResult! removeFollower(input: RemoveFollowerInput!): RemoveFollowerResult! + + """ + Remove a news feed exclusion pattern by id, un-hiding links it no longer matches. Requires a moderator account. + """ + removeNewsExcludedPattern( + """The pattern's row UUID.""" + id: UUID! + ): RemoveNewsExcludedPatternResult! removeReactionFromPost(input: RemoveReactionFromPostInput!): RemoveReactionFromPostResult! requestArticleTranslation(input: RequestArticleTranslationInput!): RequestArticleTranslationResult! @@ -1539,6 +1564,17 @@ type Mutation { sessionId: UUID! ): Session saveArticleDraft(input: SaveArticleDraftInput!): SaveArticleDraftResult! + + """ + Set (or clear with `NONE`) a moderator score penalty on a news link, demoting it in the `POPULAR` feed. Requires a moderator account. Returns the updated link, or `null` if no link has that id. + """ + setNewsScorePenalty( + """The link's row UUID (`PostLink.uuid`).""" + id: UUID! + + """The penalty level to apply (`NONE` clears it).""" + penalty: NewsPenalty! + ): SetNewsScorePenaltyResult sharePost(input: SharePostInput!): SharePostResult! startMediumUpload(input: StartMediumUploadInput!): StartMediumUploadResult! unblockActor(input: UnblockActorInput!): UnblockActorResult! @@ -1597,6 +1633,82 @@ type MuteActorPayload { union MuteActorResult = InvalidInputError | MuteActorPayload | NotAuthenticatedError +""" +A moderator-managed `URLPattern` string; links whose URL matches it are hidden from the news feed list (every sort order). Their discussion page stays reachable by direct link. +""" +type NewsExcludedPattern { + """When the pattern was added.""" + created: DateTime! + + """The pattern's row UUID.""" + id: UUID! + + """An optional moderator note explaining the exclusion.""" + note: String + + """ + The `URLPattern` string, e.g. `https://example.com/*` or `https://*.example.com/*`. + """ + pattern: String! +} + +"""Ordering for the `newsStories` feed of shared links.""" +enum NewsOrder { + """ + Highest total weighted engagement mass first, ignoring recency: an all-time-best view rather than what is hot right now. + """ + ALL_TIME + + """Most recently first-shared links first, ignoring engagement.""" + NEWEST + + """ + Hacker-News-style score combining weighted engagement mass with recency. The default; what most readers want. + """ + POPULAR +} + +""" +A moderator's score penalty on a news link, demoting it in the `POPULAR` feed. Set via `setNewsScorePenalty`; only moderators can read or change it. To remove a link from every order instead, exclude its URL with a pattern (`addNewsExcludedPattern`). +""" +enum NewsPenalty { + """Sink the link to the bottom of the popular feed.""" + BURY + + """Push the link well down the popular feed.""" + DEMOTE + + """No penalty.""" + NONE +} + +"""A snapshot of news scoring state, for the moderator admin page.""" +type NewsScoreStatus { + """When scores were last recomputed, or `null` if never.""" + lastRecomputedAt: DateTime + + """ + Number of links currently in the feed (with at least one public share). + """ + scoredLinkCount: Int! +} + +""" +How a link's public shares break down by origin. Hackers' Pub posts carry the most weight, generic remote instances less, and Bluesky-bridged accounts (`@…@bsky.brid.gy`) the least. Shares authored by bot accounts (`Service`/`Application` actors) are excluded throughout. +""" +type NewsSourceBreakdown { + """Public shares bridged from Bluesky (`@…@bsky.brid.gy`).""" + bluesky: Int! + + """Public shares authored by local Hackers' Pub accounts.""" + local: Int! + + """ + Public shares from generic remote fediverse instances (Mastodon, Pleroma, etc.). + """ + remote: Int! +} + interface Node { id: ID! } @@ -2151,12 +2263,62 @@ type PostLink implements Node { author: String creator: Actor description: String + + """ + Size of this link's federated discussion: its non-bot public sharing posts plus their direct public (`public`/`unlisted`) replies and quotes. Use this as the count of posts to read in the discussion (the `/news/{uuid}` page); unlike `postCount` it includes the replies and quotes, not just the shares. Counts direct children only (deeper nesting is not traversed) and is viewer-independent (public posts only). + """ + discussionCount: Int! + + """ + When this link was first shared publicly by a non-bot account, or `null` if it has never been. Drives the `NEWEST` order. + """ + firstSharedAt: DateTime id: ID! image: PostLinkImage + + """ + Timestamp of the freshest activity on this link's qualifying shares (the share itself, a reply, a quote, or a reaction); shares are public and authored by non-bot accounts. A rapid repeat share by the same account does not refresh this (only a first share, a sufficiently-gapped re-share, or genuine replies/quotes/reactions do), so re-posting cannot keep a link pinned at the top. `null` means the link is not a news story (no qualifying public share); such links are excluded from the feed. + """ + latestActivityAt: DateTime + + """ + The moderator score penalty on this link (demoting it in the `POPULAR` feed). `null` for non-moderators; moderators see `NONE` when the link is unpenalized. Set it with `setNewsScorePenalty`. + """ + penalty: NewsPenalty + + """ + Number of public, non-boost posts across the fediverse that share this link, excluding shares from bot (`Service`/`Application`) accounts. Counts every such post, including an account's repeated shares of the same link (a high count with a modest `score` is expected, since repeats contribute diminishing weight). + """ + postCount: Int! + + """ + Popularity-over-time rank used by the `POPULAR` feed order: `log10(max(1, weightedMass)) + recency`. Computed by a batch job and refreshed incrementally; recompute is idempotent and the value is time-stable (it changes only when the underlying posts or engagement change, not as the clock advances). Shares from bot accounts (`Service`/`Application` actors) never count, so a link shared only by bots stays at `0`, as do links never shared publicly. When one account shares the same link repeatedly, each share after the first adds only a small, gap-dependent fraction of the base weight (and a rapid repeat does not refresh recency), so re-posting cannot inflate the rank. + """ + score: Float! + + """ + The posts that share this link, most recently published first, filtered to those visible to the viewer. Shares authored by bot accounts (`Service`/`Application` actors) are excluded, matching the scoring. These are the roots of the link's discussion tree. + """ + sharingPosts(after: String, before: String, first: Int, last: Int): PostLinkSharingPostsConnection! siteName: String + + """ + Counts of this link's public shares by origin (local / remote / Bluesky bridge), excluding shares from bot (`Service`/`Application`) accounts. + """ + sourceBreakdown: NewsSourceBreakdown! title: String type: String url: URL! + + """ + The link's row UUID. Use this for the stable discussion permalink `/news/{uuid}`; the opaque Relay `id` is for `node(id:)` lookups. + """ + uuid: UUID! + + """ + Recency-independent engagement mass: the weighted sum over this link's public shares (excluding bot `Service`/`Application` accounts) of source weight times account reputation times (quotes, replies, reactions). Repeated shares of the same link by the same account add diminishing base weight (recovering with the gap but always below a first share). Drives the `ALL_TIME` order. + """ + weightedMass: Float! } type PostLinkImage { @@ -2168,6 +2330,16 @@ type PostLinkImage { width: Int } +type PostLinkSharingPostsConnection { + edges: [PostLinkSharingPostsConnectionEdge!]! + pageInfo: PageInfo! +} + +type PostLinkSharingPostsConnectionEdge { + cursor: String! + node: Post! +} + """ A media attachment on a post. For local posts this refers to an uploaded `Medium` stored on this instance; for federated posts the `url` points to the remote media URL on the originating instance. """ @@ -2413,6 +2585,44 @@ type Query { """The locale for the Markdown guide.""" locale: Locale! ): Document! + + """ + Moderator-only list of news feed exclusion patterns, newest first. `null` for non-moderators. + """ + newsExcludedPatterns: [NewsExcludedPattern!] + + """ + Moderator-only list of news links currently carrying a score penalty, heaviest first, for reviewing/clearing demotions (a buried link is no longer near the top of the feed). `null` for non-moderators. + """ + newsPenalizedStories: [PostLink!] + + """ + Moderator-only news scoring snapshot. Returns `null` when the viewer is not a moderator; routes should guard with `viewer.moderator`. + """ + newsScoreStatus: NewsScoreStatus + + """ + Links shared across the fediverse, ranked as a news feed. Forward pagination only (`first`/`after`); `last`/`before` raise a `PAGINATION_ERROR`. Pages are capped at 100 stories. No authentication required. + """ + newsStories( + after: String + before: String + first: Int + last: Int + + """How to rank the feed. Defaults to `POPULAR`.""" + order: NewsOrder = POPULAR + ): QueryNewsStoriesConnection! + + """ + Look up a news story (a shared link) by its row UUID, for the discussion permalink `/news/{uuid}`. Returns `null` for a malformed id, or for a link that is not a public news story: only links with a qualifying public share (`latestActivityAt` is not `null`) resolve, so a link seen only in followers-only or direct posts stays private. A link hidden from the feed by an exclusion pattern (`excludedFromNews`) is still reachable here. + """ + newsStory( + """ + The link's row UUID (`PostLink.uuid`), as embedded in the `/news/{uuid}` permalink. + """ + id: UUID! + ): PostLink node(id: ID!): Node nodes(ids: [ID!]!): [Node]! @@ -2540,6 +2750,16 @@ type QueryBookmarksConnectionEdge { node: Post! } +type QueryNewsStoriesConnection { + edges: [QueryNewsStoriesConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryNewsStoriesConnectionEdge { + cursor: String! + node: PostLink! +} + type QueryPersonalTimelineConnection { edges: [QueryPersonalTimelineConnectionEdge!]! pageInfo: PageInfo! @@ -2822,6 +3042,22 @@ type ReactionGroupReactorsConnectionEdge { node: Actor! } +"""The result of a full news score recompute.""" +type RecomputeNewsScoresPayload { + """ + Number of links with at least one qualifying public share that were (re)scored by this run. Stale links dropped from the feed (they lost their last public share) are reset to zero but not counted here. + """ + linksUpdated: Int! + + """When the recompute ran.""" + recomputedAt: DateTime! + + """The scoring status after the run.""" + status: NewsScoreStatus! +} + +union RecomputeNewsScoresResult = NotAuthenticatedError | NotAuthorizedError | RecomputeNewsScoresPayload + enum RedeemEmailError { EMAIL_ALREADY_TAKEN EMAIL_INVALID @@ -3017,6 +3253,14 @@ type RemoveFollowerPayload { union RemoveFollowerResult = InvalidInputError | NotAuthenticatedError | RemoveFollowerPayload +"""The result of removing a news feed exclusion pattern.""" +type RemoveNewsExcludedPatternPayload { + """The removed pattern's id, or `null` if no pattern had that id.""" + removedId: UUID +} + +union RemoveNewsExcludedPatternResult = NotAuthenticatedError | NotAuthorizedError | RemoveNewsExcludedPatternPayload + """ Remove an emoji reaction from a post. Sends an ActivityPub `Undo Like` activity. Idempotent: removing a reaction that doesn't exist returns `success: true`. Exactly one of `emoji` or `customEmojiId` must be provided. Requires authentication. """ @@ -3123,6 +3367,8 @@ type Session { userAgent: String } +union SetNewsScorePenaltyResult = NotAuthenticatedError | NotAuthorizedError | PostLink + """A lowercase hex-encoded SHA-256 digest.""" scalar Sha256 diff --git a/models/actor.ts b/models/actor.ts index 1dc34da51..5a6d7520c 100644 --- a/models/actor.ts +++ b/models/actor.ts @@ -33,6 +33,7 @@ import { toDate } from "./date.ts"; import metadata from "./deno.json" with { type: "json" }; import { persistInstance } from "./instance.ts"; import { renderMarkup } from "./markup.ts"; +import { isNewsBotActorType, refreshNewsScoresForActor } from "./news.ts"; import { isPostObject, persistPost, persistSharedPost } from "./post.ts"; import { type Account, @@ -252,6 +253,13 @@ export async function persistActor( updated: toDate(actor.updated) ?? undefined, published: toDate(actor.published), }; + // Capture the prior type to detect a bot/non-bot transition below. The + // upsert already touches this row by iri, so the extra indexed read is + // negligible against this function's network I/O. + const priorActor = await db.query.actorTable.findFirst({ + where: { iri: actor.id.href }, + columns: { type: true }, + }); const rows = await db.insert(actorTable) .values({ ...values, id: generateUuidV7() }) .onConflictDoUpdate({ @@ -261,6 +269,15 @@ export async function persistActor( }) .returning(); const result = { ...rows[0], instance }; + // If this actor just crossed the bot/non-bot boundary, which of its shares + // count toward News changed; re-score the links it shares. Best-effort + // (swallows its own errors) so it never blocks actor persistence. + if ( + priorActor != null && + isNewsBotActorType(priorActor.type) !== isNewsBotActorType(rows[0].type) + ) { + await refreshNewsScoresForActor(db, rows[0].id); + } const featured = await actor.getFeatured(getterOpts); if (featured != null) { const featuredPosts: Post[] = []; diff --git a/models/deno.json b/models/deno.json index 493225b52..2ab0cfdd7 100644 --- a/models/deno.json +++ b/models/deno.json @@ -25,6 +25,7 @@ "./markup": "./markup.ts", "./medium": "./medium.ts", "./muting": "./muting.ts", + "./news": "./news.ts", "./note": "./note.ts", "./notification": "./notification.ts", "./passkey": "./passkey.ts", diff --git a/models/news.test.ts b/models/news.test.ts new file mode 100644 index 000000000..6e8cbf67b --- /dev/null +++ b/models/news.test.ts @@ -0,0 +1,2048 @@ +import { assert } from "@std/assert/assert"; +import { assertAlmostEquals } from "@std/assert/almost-equals"; +import { assertEquals } from "@std/assert/equals"; +import { assertRejects } from "@std/assert/rejects"; +import { eq, sql } from "drizzle-orm"; +import type { Transaction } from "./db.ts"; +import { + addNewsExcludedPattern, + getNewsDiscussionCounts, + getNewsExcludedPatterns, + getNewsPenalizedStories, + getNewsScoreStatus, + getNewsSourceBreakdowns, + getNewsStories, + InvalidNewsPatternError, + NEWS_EPOCH_SECONDS, + NEWS_PENALTY_BURY, + NEWS_PENALTY_DEMOTE, + NEWS_REPEAT_CAP, + NEWS_REPEAT_FRESH_MIN_SECONDS, + NEWS_REPEAT_RECOVERY_TAU_SECONDS, + NEWS_SOURCE_WEIGHT_BLUESKY, + NEWS_SOURCE_WEIGHT_LOCAL, + NEWS_SOURCE_WEIGHT_REMOTE, + NEWS_TAU_SECONDS, + NEWS_W_QUOTE, + NEWS_W_REACT, + NEWS_W_REPLY, + NEWS_W_SHARE, + recomputeNewsScores, + refreshNewsScores, + refreshNewsScoresForActor, + refreshNewsScoresForPostLinks, + removeNewsExcludedPattern, + setNewsScorePenalty, +} from "./news.ts"; +import { syncPostFromNoteSource } from "./post.ts"; +import { actorTable, instanceTable, postTable } from "./schema.ts"; +import type { Uuid } from "./uuid.ts"; +import { + createFedCtx, + insertAccountWithActor, + insertNotePost, + insertPostLink, + insertReaction, + insertRemoteActor, + withRollback, +} from "../test/postgres.ts"; + +// Independent re-implementation of the scoring formula, used to cross-check +// the SQL in `recomputeNewsScores` against a second source of truth. +function recency(at: Date): number { + return (at.getTime() / 1000 - NEWS_EPOCH_SECONDS) / NEWS_TAU_SECONDS; +} +function mass( + sourceWeight: number, + acctWeight: number, + { quotes = 0, replies = 0, reactions = 0 } = {}, +): number { + return sourceWeight * acctWeight * ( + NEWS_W_SHARE + NEWS_W_QUOTE * quotes + NEWS_W_REPLY * replies + + NEWS_W_REACT * reactions + ); +} +function score(weightedMass: number, latestActivity: Date): number { + return Math.log10(Math.max(1, weightedMass)) + recency(latestActivity); +} +// Base-share multiplier for a repeat share, mirroring the SQL: a first share is +// 1, a repeat recovers toward NEWS_REPEAT_CAP as the gap grows. +function repeatFactor(gapSeconds: number): number { + return NEWS_REPEAT_CAP * + (1 - Math.exp(-gapSeconds / NEWS_REPEAT_RECOVERY_TAU_SECONDS)); +} + +async function readLink(tx: Transaction, id: Uuid) { + const link = await tx.query.postLinkTable.findFirst({ where: { id } }); + assert(link != null); + return link; +} + +Deno.test({ + name: "recomputeNewsScores ignores links with no public share", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "noshare", + name: "No Share", + email: "noshare@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/private", + }); + // Followers-only post: not a qualifying public share. + await insertNotePost(tx, { + account: sharer.account, + visibility: "followers", + link: { id: link.id, url: link.url }, + }); + + const result = await recomputeNewsScores(tx); + assertEquals(result.linksUpdated, 0); + + const row = await readLink(tx, link.id); + assertEquals(row.score, 0); + assertEquals(row.latestActivityAt, null); + assertEquals(row.postCount, 0); + + const stories = await getNewsStories(tx, { + order: "popular", + limit: 10, + }); + assertEquals(stories.find((s) => s.id === link.id), undefined); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores weights local over generic remote shares", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const local = await insertAccountWithActor(tx, { + username: "localsharer", + name: "Local Sharer", + email: "localsharer@example.com", + }); + const remote = await insertRemoteActor(tx, { + username: "remotesharer", + name: "Remote Sharer", + host: "mastodon.example", + }); + const localLink = await insertPostLink(tx, { + url: "https://example.com/local", + }); + const remoteLink = await insertPostLink(tx, { + url: "https://example.com/remote", + }); + await insertNotePost(tx, { + account: local.account, + link: { id: localLink.id, url: localLink.url }, + }); + await insertNotePost(tx, { + account: local.account, // satisfies the source-account type + actorId: remote.id, + link: { id: remoteLink.id, url: remoteLink.url }, + }); + + await recomputeNewsScores(tx); + + const localRow = await readLink(tx, localLink.id); + const remoteRow = await readLink(tx, remoteLink.id); + assertAlmostEquals( + localRow.weightedMass, + mass(NEWS_SOURCE_WEIGHT_LOCAL, 1), + 1e-9, + ); + assertAlmostEquals( + remoteRow.weightedMass, + mass(NEWS_SOURCE_WEIGHT_REMOTE, 1), + 1e-9, + ); + assertAlmostEquals( + localRow.weightedMass / remoteRow.weightedMass, + NEWS_SOURCE_WEIGHT_LOCAL / NEWS_SOURCE_WEIGHT_REMOTE, + 1e-9, + ); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores down-weights Bluesky bridge shares", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const host = await insertAccountWithActor(tx, { + username: "bskyhost", + name: "Host", + email: "bskyhost@example.com", + }); + const bridged = await insertRemoteActor(tx, { + username: "alice.bsky.social", + name: "Alice", + host: "bsky.brid.gy", + handleHost: "bsky.brid.gy", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/bsky", + }); + await insertNotePost(tx, { + account: host.account, + actorId: bridged.id, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + assertAlmostEquals( + row.weightedMass, + mass(NEWS_SOURCE_WEIGHT_BLUESKY, 1), + 1e-9, + ); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores ranks quotes over replies over reactions", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "weights", + name: "Weights", + email: "weights@example.com", + }); + const quoteLink = await insertPostLink(tx, { + url: "https://example.com/q", + }); + const replyLink = await insertPostLink(tx, { + url: "https://example.com/r", + }); + const reactLink = await insertPostLink(tx, { + url: "https://example.com/x", + }); + const { post: quoteShare } = await insertNotePost(tx, { + account: sharer.account, + link: { id: quoteLink.id, url: quoteLink.url }, + }); + const { post: replyShare } = await insertNotePost(tx, { + account: sharer.account, + link: { id: replyLink.id, url: replyLink.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + reactionsCounts: { "❤️": 1 }, + link: { id: reactLink.id, url: reactLink.url }, + }); + // A public quote of the quote-link's share, and a public reply of the + // reply-link's share, drive the mass (the denormalized counts would also + // include private posts, which must not count). + await insertNotePost(tx, { + account: sharer.account, + quotedPostId: quoteShare.id, + }); + await insertNotePost(tx, { + account: sharer.account, + replyTargetId: replyShare.id, + }); + + await recomputeNewsScores(tx); + + const q = await readLink(tx, quoteLink.id); + const r = await readLink(tx, replyLink.id); + const x = await readLink(tx, reactLink.id); + assertAlmostEquals(q.weightedMass, mass(1, 1, { quotes: 1 }), 1e-9); + assertAlmostEquals(r.weightedMass, mass(1, 1, { replies: 1 }), 1e-9); + assertAlmostEquals(x.weightedMass, mass(1, 1, { reactions: 1 }), 1e-9); + assert(q.weightedMass > r.weightedMass); + assert(r.weightedMass > x.weightedMass); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores excludes non-public replies and quotes", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "privacy", + name: "Privacy", + email: "privacy@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/pv" }); + const { post: share } = await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + // One public reply + one public quote count toward the score… + await insertNotePost(tx, { + account: sharer.account, + replyTargetId: share.id, + }); + await insertNotePost(tx, { + account: sharer.account, + quotedPostId: share.id, + }); + // …but followers-only and direct replies/quotes must not (they would + // otherwise leak private discussion volume into a public score). + await insertNotePost(tx, { + account: sharer.account, + visibility: "followers", + replyTargetId: share.id, + }); + await insertNotePost(tx, { + account: sharer.account, + visibility: "direct", + quotedPostId: share.id, + }); + + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + assertAlmostEquals( + row.weightedMass, + mass(1, 1, { quotes: 1, replies: 1 }), + 1e-9, + ); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores adds a recency term anchored to a fixed epoch", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "recency", + name: "Recency", + email: "recency@example.com", + }); + const older = await insertPostLink(tx, { url: "https://example.com/o" }); + const newer = await insertPostLink(tx, { url: "https://example.com/n" }); + const olderAt = new Date("2026-04-15T00:00:00.000Z"); + const newerAt = new Date("2026-04-16T00:00:00.000Z"); // +24h + await insertNotePost(tx, { + account: sharer.account, + published: olderAt, + link: { id: older.id, url: older.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: newerAt, + link: { id: newer.id, url: newer.url }, + }); + + await recomputeNewsScores(tx); + + const o = await readLink(tx, older.id); + const n = await readLink(tx, newer.id); + assertEquals(o.latestActivityAt?.getTime(), olderAt.getTime()); + assertEquals(n.latestActivityAt?.getTime(), newerAt.getTime()); + assertAlmostEquals(o.score, score(mass(1, 1), olderAt), 1e-6); + assertAlmostEquals(n.score, score(mass(1, 1), newerAt), 1e-6); + // 24h apart => exactly 86400 / TAU difference in the recency term. + assertAlmostEquals(n.score - o.score, 86400 / NEWS_TAU_SECONDS, 1e-6); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores lifts an old link with a recent reaction", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "fresh", + name: "Fresh", + email: "fresh@example.com", + }); + const reactor = await insertAccountWithActor(tx, { + username: "reactor", + name: "Reactor", + email: "reactor@example.com", + }); + const sharedAt = new Date("2025-01-01T00:00:00.000Z"); + const reactionAt = new Date("2026-05-29T00:00:00.000Z"); + + const fresh = await insertPostLink(tx, { url: "https://example.com/f" }); + const stale = await insertPostLink(tx, { url: "https://example.com/s" }); + const { post: freshPost } = await insertNotePost(tx, { + account: sharer.account, + published: sharedAt, + link: { id: fresh.id, url: fresh.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: sharedAt, + link: { id: stale.id, url: stale.url }, + }); + // A reaction created long after the share bumps `fresh`'s activity. + await insertReaction(tx, { + postId: freshPost.id, + actorId: reactor.actor.id, + created: reactionAt, + }); + + await recomputeNewsScores(tx); + + const f = await readLink(tx, fresh.id); + const s = await readLink(tx, stale.id); + // The reaction timestamp wins over the (older) share publish time. + assertEquals(f.latestActivityAt?.getTime(), reactionAt.getTime()); + assertEquals(s.latestActivityAt?.getTime(), sharedAt.getTime()); + // Same mass, but the fresh reaction lifts the old link far above the + // otherwise-identical stale one. + assert(f.score > s.score); + + const popular = await getNewsStories(tx, { order: "popular", limit: 10 }); + const freshIdx = popular.findIndex((l) => l.id === fresh.id); + const staleIdx = popular.findIndex((l) => l.id === stale.id); + assert(freshIdx >= 0 && staleIdx >= 0); + assert(freshIdx < staleIdx); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores is idempotent", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "idem", + name: "Idem", + email: "idem@example.com", + }); + const a = await insertPostLink(tx, { url: "https://example.com/a" }); + const b = await insertPostLink(tx, { url: "https://example.com/b" }); + const { post: aShare } = await insertNotePost(tx, { + account: sharer.account, + reactionsCounts: { "❤️": 3 }, + published: new Date("2026-03-01T00:00:00.000Z"), + link: { id: a.id, url: a.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + quotedPostId: aShare.id, + published: new Date("2026-03-01T00:00:00.000Z"), + }); + await insertNotePost(tx, { + account: sharer.account, + replyTargetId: aShare.id, + published: new Date("2026-03-01T00:00:00.000Z"), + }); + await insertNotePost(tx, { + account: sharer.account, + published: new Date("2026-03-02T00:00:00.000Z"), + link: { id: b.id, url: b.url }, + }); + + await recomputeNewsScores(tx); + const a1 = await readLink(tx, a.id); + const b1 = await readLink(tx, b.id); + await recomputeNewsScores(tx); + const a2 = await readLink(tx, a.id); + const b2 = await readLink(tx, b.id); + + for (const [first, second] of [[a1, a2], [b1, b2]] as const) { + assertEquals(first.score, second.score); + assertEquals(first.weightedMass, second.weightedMass); + assertEquals(first.recencyComponent, second.recencyComponent); + assertEquals(first.postCount, second.postCount); + assertEquals( + first.firstSharedAt?.getTime(), + second.firstSharedAt?.getTime(), + ); + assertEquals( + first.latestActivityAt?.getTime(), + second.latestActivityAt?.getTime(), + ); + } + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores can target a subset of links", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "subset", + name: "Subset", + email: "subset@example.com", + }); + const a = await insertPostLink(tx, { url: "https://example.com/sa" }); + const b = await insertPostLink(tx, { url: "https://example.com/sb" }); + await insertNotePost(tx, { + account: sharer.account, + link: { id: a.id, url: a.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + link: { id: b.id, url: b.url }, + }); + + const result = await recomputeNewsScores(tx, { linkIds: [a.id] }); + assertEquals(result.linksUpdated, 1); + assert((await readLink(tx, a.id)).latestActivityAt != null); + assertEquals((await readLink(tx, b.id)).latestActivityAt, null); + + await recomputeNewsScores(tx); + assert((await readLink(tx, b.id)).latestActivityAt != null); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores activeSince picks up fresh activity on old links", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "sweep", + name: "Sweep", + email: "sweep@example.com", + }); + const reactor = await insertAccountWithActor(tx, { + username: "sweepreactor", + name: "Sweep Reactor", + email: "sweepreactor@example.com", + }); + const sharedAt = new Date("2025-06-01T00:00:00.000Z"); + const active = await insertPostLink(tx, { + url: "https://example.com/sw1", + }); + const idle = await insertPostLink(tx, { url: "https://example.com/sw2" }); + const { post: activePost } = await insertNotePost(tx, { + account: sharer.account, + published: sharedAt, + link: { id: active.id, url: active.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: sharedAt, + link: { id: idle.id, url: idle.url }, + }); + + await recomputeNewsScores(tx); + assertEquals( + (await readLink(tx, active.id)).latestActivityAt?.getTime(), + sharedAt.getTime(), + ); + + // A reaction arrives long after the initial scoring. + const reactionAt = new Date("2026-05-29T00:00:00.000Z"); + await insertReaction(tx, { + postId: activePost.id, + actorId: reactor.actor.id, + created: reactionAt, + }); + + // The sweep targets only links with activity since the cutoff, derived + // from source timestamps (not the stale stored latestActivityAt). + const result = await recomputeNewsScores(tx, { + activeSince: new Date("2026-01-01T00:00:00.000Z"), + }); + assertEquals(result.linksUpdated, 1); + assertEquals( + (await readLink(tx, active.id)).latestActivityAt?.getTime(), + reactionAt.getTime(), + ); + // The idle link had no fresh activity, so the sweep left it untouched. + assertEquals( + (await readLink(tx, idle.id)).latestActivityAt?.getTime(), + sharedAt.getTime(), + ); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores drops a link that lost its last public share", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "dropout", + name: "Dropout", + email: "dropout@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/d" }); + const { post } = await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + assert((await readLink(tx, link.id)).latestActivityAt != null); + + // The only public share becomes followers-only. + await tx.update(postTable).set({ visibility: "followers" }).where( + eq(postTable.id, post.id), + ); + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + assertEquals(row.score, 0); + assertEquals(row.latestActivityAt, null); + assertEquals(row.postCount, 0); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores activeSince picks up a federated count update", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "sweepupdate", + name: "Sweep Update", + email: "sweepupdate@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/swu" }); + const { post } = await insertNotePost(tx, { + account: sharer.account, + published: new Date("2025-06-01T00:00:00.000Z"), + link: { id: link.id, url: link.url }, + }); + await recomputeNewsScores(tx); + const before = await readLink(tx, link.id); + + // A federated Update bumps `updated` and revises the reaction totals + // without creating any local reply/quote/reaction row. + await tx.execute( + sql`update post + set updated = '2026-05-29T00:00:00Z', + reactions_counts = '{"❤️": 5}'::jsonb + where id = ${post.id}`, + ); + const result = await recomputeNewsScores(tx, { + activeSince: new Date("2026-01-01T00:00:00.000Z"), + }); + + assertEquals(result.linksUpdated, 1); + const after = await readLink(tx, link.id); + assert(after.weightedMass > before.weightedMass); + }); + }, +}); + +Deno.test({ + name: + "recomputeNewsScores activeSince still drops a link that lost its share", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "sweepdrop", + name: "Sweep Drop", + email: "sweepdrop@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/sd" }); + const { post } = await insertNotePost(tx, { + account: sharer.account, + published: new Date("2026-05-20T00:00:00.000Z"), + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + assert((await readLink(tx, link.id)).latestActivityAt != null); + + // The only public share becomes followers-only, then a sweep runs. The + // sweep's zeroing scopes by the stored latestActivityAt, so it still + // resets the dropped-out link even though it is no longer "active". + await tx.update(postTable).set({ visibility: "followers" }).where( + eq(postTable.id, post.id), + ); + await recomputeNewsScores(tx, { + activeSince: new Date("2026-05-01T00:00:00.000Z"), + }); + + const row = await readLink(tx, link.id); + assertEquals(row.score, 0); + assertEquals(row.latestActivityAt, null); + assertEquals(row.postCount, 0); + }); + }, +}); + +Deno.test({ + name: "getNewsStories diverges between popular and allTime order", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "diverge", + name: "Diverge", + email: "diverge@example.com", + }); + const heavyOld = await insertPostLink(tx, { + url: "https://example.com/heavy", + }); + const lightNew = await insertPostLink(tx, { + url: "https://example.com/light", + }); + // Heavy engagement but shared a year ago. + await insertNotePost(tx, { + account: sharer.account, + reactionsCounts: { "❤️": 150 }, + published: new Date("2025-05-30T00:00:00.000Z"), + link: { id: heavyOld.id, url: heavyOld.url }, + }); + // Light engagement but shared recently. + await insertNotePost(tx, { + account: sharer.account, + published: new Date("2026-05-30T00:00:00.000Z"), + link: { id: lightNew.id, url: lightNew.url }, + }); + + await recomputeNewsScores(tx); + + const byAllTime = await getNewsStories(tx, { + order: "allTime", + limit: 10, + }); + const byPopular = await getNewsStories(tx, { + order: "popular", + limit: 10, + }); + assertEquals(byAllTime[0].id, heavyOld.id); + assertEquals(byPopular[0].id, lightNew.id); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores aggregates postCount and firstSharedAt", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const link = await insertPostLink(tx, { url: "https://example.com/agg" }); + const times = [ + new Date("2026-05-10T00:00:00.000Z"), + new Date("2026-05-12T00:00:00.000Z"), + new Date("2026-05-11T00:00:00.000Z"), + ]; + // Distinct accounts so each is a full-weight first share (this exercises + // aggregation, not the same-account repeat damping). + for (let i = 0; i < times.length; i++) { + const sharer = await insertAccountWithActor(tx, { + username: `aggr${i}`, + name: `Aggr ${i}`, + email: `aggr${i}@example.com`, + }); + await insertNotePost(tx, { + account: sharer.account, + published: times[i], + link: { id: link.id, url: link.url }, + }); + } + + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + assertEquals(row.postCount, 3); + assertEquals( + row.firstSharedAt?.getTime(), + new Date("2026-05-10T00:00:00.000Z").getTime(), + ); + assertEquals( + row.latestActivityAt?.getTime(), + new Date("2026-05-12T00:00:00.000Z").getTime(), + ); + }); + }, +}); + +Deno.test({ + name: "getNewsStories paginates by keyset without gaps or overlaps", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "pager", + name: "Pager", + email: "pager@example.com", + }); + const ids: Uuid[] = []; + for (let i = 0; i < 5; i++) { + const link = await insertPostLink(tx, { + url: `https://example.com/page-${i}`, + }); + await insertNotePost(tx, { + account: sharer.account, + // Distinct published times => distinct firstSharedAt order. + published: new Date(Date.UTC(2026, 4, 10 + i)), + link: { id: link.id, url: link.url }, + }); + ids.push(link.id); + } + + await recomputeNewsScores(tx); + + const page1 = await getNewsStories(tx, { order: "newest", limit: 2 }); + assertEquals(page1.length, 2); + const last = page1[page1.length - 1]; + const page2 = await getNewsStories(tx, { + order: "newest", + limit: 2, + after: { value: last.firstSharedAt!, id: last.id }, + }); + assertEquals(page2.length, 2); + + const seen = [...page1, ...page2].map((l) => l.id); + assertEquals(new Set(seen).size, seen.length); // no overlaps + // newest-first: published descending. + assertEquals(seen[0], ids[4]); + assertEquals(seen[1], ids[3]); + assertEquals(seen[2], ids[2]); + assertEquals(seen[3], ids[1]); + }); + }, +}); + +Deno.test({ + name: "getNewsStories newest pagination keeps sub-millisecond-close links", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "micropage", + name: "Micro Page", + email: "micropage@example.com", + }); + const a = await insertPostLink(tx, { url: "https://example.com/ua" }); + const b = await insertPostLink(tx, { url: "https://example.com/ub" }); + const { post: postA } = await insertNotePost(tx, { + account: sharer.account, + link: { id: a.id, url: a.url }, + }); + const { post: postB } = await insertNotePost(tx, { + account: sharer.account, + link: { id: b.id, url: b.url }, + }); + // Same millisecond, different microseconds: the precision a JS-Date + // cursor cannot represent. + await tx.execute( + sql`update post set published = '2026-05-20T00:00:00.000800Z' + where id = ${postA.id}`, + ); + await tx.execute( + sql`update post set published = '2026-05-20T00:00:00.000300Z' + where id = ${postB.id}`, + ); + await recomputeNewsScores(tx); + + // Walk the feed one story at a time using the encoded cursor. + const seen: Uuid[] = []; + let after: { value: number | Date; id: Uuid } | undefined; + for (let i = 0; i < 3; i++) { + const page = await getNewsStories(tx, { + order: "newest", + limit: 1, + after, + }); + if (page.length < 1) break; + const link = page[0]; + seen.push(link.id); + after = { value: link.firstSharedAt!, id: link.id }; + } + assertEquals(new Set(seen).size, 2); + assert(seen.includes(a.id)); + assert(seen.includes(b.id)); + }); + }, +}); + +Deno.test({ + name: "getNewsScoreStatus reports scored link count and last recompute", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "status", + name: "Status", + email: "status@example.com", + }); + const before = await getNewsScoreStatus(tx); + assertEquals(before.scoredLinkCount, 0); + + const link = await insertPostLink(tx, { url: "https://example.com/st" }); + await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + await recomputeNewsScores(tx); + + const after = await getNewsScoreStatus(tx); + assertEquals(after.scoredLinkCount, 1); + assert(after.lastRecomputedAt != null); + }); + }, +}); + +Deno.test({ + name: "refreshNewsScores scores a newly shared link without a batch run", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "hookshare", + name: "Hook Share", + email: "hookshare@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/hk" }); + await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + + assertEquals((await readLink(tx, link.id)).latestActivityAt, null); + await refreshNewsScores(tx, [link.id]); + const row = await readLink(tx, link.id); + assert(row.latestActivityAt != null); + assert(row.score > 0); + }); + }, +}); + +Deno.test({ + name: "refreshNewsScores drops a link whose share is no longer public", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "hookdrop", + name: "Hook Drop", + email: "hookdrop@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/hd" }); + const { post } = await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + await refreshNewsScores(tx, [link.id]); + assert((await readLink(tx, link.id)).latestActivityAt != null); + + // The edit removes the only public share; refreshing the (previous) link + // drops it from the feed. + await tx.update(postTable).set({ visibility: "followers" }).where( + eq(postTable.id, post.id), + ); + await refreshNewsScores(tx, [link.id]); + assertEquals((await readLink(tx, link.id)).latestActivityAt, null); + assertEquals((await readLink(tx, link.id)).score, 0); + }); + }, +}); + +Deno.test({ + name: "syncPostFromNoteSource clears a removed link and drops the story", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const fedCtx = createFedCtx(tx); + const author = await insertAccountWithActor(tx, { + username: "editor", + name: "Editor", + email: "editor@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/edited", + }); + // The note carries a link on the row but its content has none, so a + // re-sync renders no link and must clear `link_id`. + const { noteSourceId } = await insertNotePost(tx, { + account: author.account, + content: "Hello world", + link: { id: link.id, url: link.url }, + }); + await refreshNewsScores(tx, [link.id]); + assert((await readLink(tx, link.id)).latestActivityAt != null); + + const noteSource = await tx.query.noteSourceTable.findFirst({ + where: { id: noteSourceId }, + with: { + account: { + with: { avatarMedium: true, emails: true, links: true }, + }, + media: { with: { medium: true } }, + }, + }); + assert(noteSource != null); + + const updated = await syncPostFromNoteSource(fedCtx, noteSource); + assert(updated != null); + // The link is cleared (not left as the stale previous value)... + assertEquals(updated.linkId, null); + // ...and the incremental refresh of the previous link drops the story. + assertEquals((await readLink(tx, link.id)).latestActivityAt, null); + assertEquals((await readLink(tx, link.id)).score, 0); + }); + }, +}); + +Deno.test({ + name: "getNewsSourceBreakdowns counts NULL-software instances as remote", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const local = await insertAccountWithActor(tx, { + username: "brklocal", + name: "Brk Local", + email: "brklocal@example.com", + }); + // A remote instance whose `software` is unknown (NULL). + await tx.insert(instanceTable).values({ + host: "unknown.example", + software: null, + softwareVersion: null, + }); + const unknownRemote = await insertRemoteActor(tx, { + username: "brkunknown", + name: "Brk Unknown", + host: "unknown.example", + }); + const bridged = await insertRemoteActor(tx, { + username: "brkbsky.bsky.social", + name: "Brk Bsky", + host: "bsky.brid.gy", + handleHost: "bsky.brid.gy", + }); + const link = await insertPostLink(tx, { url: "https://example.com/brk" }); + await insertNotePost(tx, { + account: local.account, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: local.account, + actorId: unknownRemote.id, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: local.account, + actorId: bridged.id, + link: { id: link.id, url: link.url }, + }); + + const breakdowns = await getNewsSourceBreakdowns(tx, [link.id]); + // The NULL-software actor must land in `remote`, not vanish. + assertEquals(breakdowns.get(link.id), { + local: 1, + remote: 1, + bluesky: 1, + }); + }); + }, +}); + +Deno.test({ + name: "refreshNewsScoresForPostLinks reflects a deleted public reply", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "deletereply", + name: "Delete Reply", + email: "deletereply@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/dr" }); + const { post: share } = await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + const { post: reply } = await insertNotePost(tx, { + account: sharer.account, + replyTargetId: share.id, + }); + await recomputeNewsScores(tx); + assertAlmostEquals( + (await readLink(tx, link.id)).weightedMass, + mass(1, 1, { replies: 1 }), + 1e-9, + ); + + // Delete the reply, then refresh through the destructive-path helper: + // the parent link's mass drops back to just the share. + await tx.delete(postTable).where(eq(postTable.id, reply.id)); + await refreshNewsScoresForPostLinks(tx, reply); + assertAlmostEquals( + (await readLink(tx, link.id)).weightedMass, + mass(1, 1), + 1e-9, + ); + }); + }, +}); + +Deno.test({ + name: "refreshNewsScoresForPostLinks drops a link when its share is deleted", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "deleteshare", + name: "Delete Share", + email: "deleteshare@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/ds" }); + const { post: share } = await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + await recomputeNewsScores(tx); + assert((await readLink(tx, link.id)).latestActivityAt != null); + + await tx.delete(postTable).where(eq(postTable.id, share.id)); + await refreshNewsScoresForPostLinks(tx, share); + const row = await readLink(tx, link.id); + assertEquals(row.score, 0); + assertEquals(row.latestActivityAt, null); + assertEquals(row.postCount, 0); + }); + }, +}); + +Deno.test({ + name: "refreshNewsScores ignores null/empty link ids", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + await refreshNewsScores(tx, []); + await refreshNewsScores(tx, [null, undefined]); + const status = await getNewsScoreStatus(tx); + assertEquals(status.scoredLinkCount, 0); + }); + }, +}); + +// --------------------------------------------------------------------------- +// Bot exclusion: shares authored by Service/Application actors (automated link +// feeds) must not surface a link as news. Replies/quotes/reactions are not +// filtered by author; only the *sharing* post's actor type matters. +// --------------------------------------------------------------------------- + +Deno.test({ + name: "recomputeNewsScores excludes a Service-actor (bot) share", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const host = await insertAccountWithActor(tx, { + username: "bothost1", + name: "Bot Host", + email: "bothost1@example.com", + }); + const bot = await insertRemoteActor(tx, { + username: "feedbot", + name: "Feed Bot", + host: "bots.example", + type: "Service", + }); + const link = await insertPostLink(tx, { url: "https://example.com/svc" }); + await insertNotePost(tx, { + account: host.account, + actorId: bot.id, + link: { id: link.id, url: link.url }, + }); + + const result = await recomputeNewsScores(tx); + assertEquals(result.linksUpdated, 0); + const row = await readLink(tx, link.id); + assertEquals(row.score, 0); + assertEquals(row.latestActivityAt, null); + assertEquals(row.postCount, 0); + const stories = await getNewsStories(tx, { order: "popular", limit: 10 }); + assertEquals(stories.find((s) => s.id === link.id), undefined); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores excludes an Application-actor (bot) share", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const host = await insertAccountWithActor(tx, { + username: "bothost2", + name: "Bot Host 2", + email: "bothost2@example.com", + }); + const bot = await insertRemoteActor(tx, { + username: "appbot", + name: "App Bot", + host: "bots.example", + type: "Application", + }); + const link = await insertPostLink(tx, { url: "https://example.com/app" }); + await insertNotePost(tx, { + account: host.account, + actorId: bot.id, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + const row = await readLink(tx, link.id); + assertEquals(row.latestActivityAt, null); + assertEquals(row.postCount, 0); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores ignores a bot share when a human also shares", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const human = await insertAccountWithActor(tx, { + username: "humanmix", + name: "Human Mix", + email: "humanmix@example.com", + }); + const bot = await insertRemoteActor(tx, { + username: "mixbot", + name: "Mix Bot", + host: "bots.example", + type: "Service", + }); + const link = await insertPostLink(tx, { url: "https://example.com/mix" }); + const baseline = await insertPostLink(tx, { + url: "https://example.com/base", + }); + // The link gets one human share and one bot share; the baseline gets only + // the same human share. The bot share must contribute nothing, so the two + // links end up with identical mass and score. + await insertNotePost(tx, { + account: human.account, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: human.account, + actorId: bot.id, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: human.account, + link: { id: baseline.id, url: baseline.url }, + }); + + await recomputeNewsScores(tx); + + const linkRow = await readLink(tx, link.id); + const baselineRow = await readLink(tx, baseline.id); + assertEquals(linkRow.postCount, 1); + assertAlmostEquals(linkRow.weightedMass, baselineRow.weightedMass, 1e-9); + assertAlmostEquals(linkRow.score, baselineRow.score, 1e-9); + assertEquals( + linkRow.latestActivityAt?.getTime(), + baselineRow.latestActivityAt?.getTime(), + ); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores still scores a Group-actor share", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const host = await insertAccountWithActor(tx, { + username: "grouphost", + name: "Group Host", + email: "grouphost@example.com", + }); + // Group/Organization accounts are not bots: their shares still count. + const group = await insertRemoteActor(tx, { + username: "guppe", + name: "Guppe Group", + host: "a.gup.pe", + type: "Group", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/group", + }); + await insertNotePost(tx, { + account: host.account, + actorId: group.id, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + const row = await readLink(tx, link.id); + assert(row.latestActivityAt != null); + assertEquals(row.postCount, 1); + }); + }, +}); + +Deno.test({ + name: "refreshNewsScores drops a link left with only a bot share", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const human = await insertAccountWithActor(tx, { + username: "dropbot", + name: "Drop Bot", + email: "dropbot@example.com", + }); + const bot = await insertRemoteActor(tx, { + username: "lingerbot", + name: "Linger Bot", + host: "bots.example", + type: "Service", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/drop", + }); + const { post: humanShare } = await insertNotePost(tx, { + account: human.account, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: human.account, + actorId: bot.id, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + assert((await readLink(tx, link.id)).latestActivityAt != null); + assertEquals((await readLink(tx, link.id)).postCount, 1); + + // Delete the only human share: the bot share remains but does not qualify, + // so the incremental refresh drops the link from the feed. + await tx.delete(postTable).where(eq(postTable.id, humanShare.id)); + await refreshNewsScores(tx, [link.id]); + + const row = await readLink(tx, link.id); + assertEquals(row.score, 0); + assertEquals(row.latestActivityAt, null); + assertEquals(row.postCount, 0); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores activeSince skips a bot-only link", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const host = await insertAccountWithActor(tx, { + username: "sweepbot", + name: "Sweep Bot Host", + email: "sweepbot@example.com", + }); + const bot = await insertRemoteActor(tx, { + username: "sweepfeedbot", + name: "Sweep Feed Bot", + host: "bots.example", + type: "Service", + }); + const link = await insertPostLink(tx, { url: "https://example.com/swb" }); + await insertNotePost(tx, { + account: host.account, + actorId: bot.id, + published: new Date("2026-05-20T00:00:00.000Z"), + link: { id: link.id, url: link.url }, + }); + + const result = await recomputeNewsScores(tx, { + activeSince: new Date("2026-01-01T00:00:00.000Z"), + }); + assertEquals(result.linksUpdated, 0); + assertEquals((await readLink(tx, link.id)).latestActivityAt, null); + }); + }, +}); + +Deno.test({ + name: "getNewsSourceBreakdowns excludes bot shares", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const local = await insertAccountWithActor(tx, { + username: "brkbotlocal", + name: "Brk Bot Local", + email: "brkbotlocal@example.com", + }); + const bot = await insertRemoteActor(tx, { + username: "brkbot", + name: "Brk Bot", + host: "mastodon.example", + type: "Service", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/brkbot", + }); + await insertNotePost(tx, { + account: local.account, + link: { id: link.id, url: link.url }, + }); + // A remote Service share would otherwise be counted as `remote`. + await insertNotePost(tx, { + account: local.account, + actorId: bot.id, + link: { id: link.id, url: link.url }, + }); + + const breakdowns = await getNewsSourceBreakdowns(tx, [link.id]); + assertEquals(breakdowns.get(link.id), { + local: 1, + remote: 0, + bluesky: 0, + }); + }); + }, +}); + +// --------------------------------------------------------------------------- +// Discussion count: the size of a link's federated conversation = its non-bot +// public sharing posts plus their direct public replies and quotes. +// --------------------------------------------------------------------------- + +Deno.test({ + name: "getNewsDiscussionCounts counts shares plus public replies and quotes", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const human = await insertAccountWithActor(tx, { + username: "disc", + name: "Disc", + email: "disc@example.com", + }); + const bot = await insertRemoteActor(tx, { + username: "discbot", + name: "Disc Bot", + host: "bots.example", + type: "Service", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/disc", + }); + const { post: share } = await insertNotePost(tx, { + account: human.account, + link: { id: link.id, url: link.url }, + }); + // A bot's share of the same link must not count (excluded root). + await insertNotePost(tx, { + account: human.account, + actorId: bot.id, + link: { id: link.id, url: link.url }, + }); + // A public reply and a public quote of the human share count… + await insertNotePost(tx, { + account: human.account, + replyTargetId: share.id, + }); + await insertNotePost(tx, { + account: human.account, + quotedPostId: share.id, + }); + // …but a followers-only reply does not. + await insertNotePost(tx, { + account: human.account, + visibility: "followers", + replyTargetId: share.id, + }); + + const counts = await getNewsDiscussionCounts(tx, [link.id]); + // 1 human share + 1 public reply + 1 public quote = 3. + assertEquals(counts.get(link.id), 3); + }); + }, +}); + +Deno.test({ + name: "getNewsDiscussionCounts counts a share-only link and ignores unknowns", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const a = await insertAccountWithActor(tx, { + username: "disca", + name: "Disc A", + email: "disca@example.com", + }); + const b = await insertAccountWithActor(tx, { + username: "discb", + name: "Disc B", + email: "discb@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/disc2", + }); + await insertNotePost(tx, { + account: a.account, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: b.account, + link: { id: link.id, url: link.url }, + }); + + const counts = await getNewsDiscussionCounts(tx, [link.id]); + // Two shares, no replies/quotes. + assertEquals(counts.get(link.id), 2); + + // An unknown link id is simply absent from the map. + const empty = await getNewsDiscussionCounts(tx, [ + "00000000-0000-7000-8000-000000000000" as Uuid, + ]); + assertEquals(empty.size, 0); + }); + }, +}); + +Deno.test({ + name: "getNewsDiscussionCounts counts a reply-and-quote post once", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const a = await insertAccountWithActor(tx, { + username: "dedup", + name: "Dedup", + email: "dedup@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/dedup", + }); + const { post: share1 } = await insertNotePost(tx, { + account: a.account, + link: { id: link.id, url: link.url }, + }); + const { post: share2 } = await insertNotePost(tx, { + account: a.account, + link: { id: link.id, url: link.url }, + }); + // One post that both replies to share1 and quotes share2 must count once, + // not twice, matching the deduplicated discussion tree. + await insertNotePost(tx, { + account: a.account, + replyTargetId: share1.id, + quotedPostId: share2.id, + }); + + const counts = await getNewsDiscussionCounts(tx, [link.id]); + // 2 shares + 1 distinct child = 3 (not 4). + assertEquals(counts.get(link.id), 3); + }); + }, +}); + +Deno.test({ + name: "refreshNewsScoresForActor re-scores links across a bot transition", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const host = await insertAccountWithActor(tx, { + username: "transhost", + name: "Trans Host", + email: "transhost@example.com", + }); + // The actor starts as a Person, so its share counts. + const actor = await insertRemoteActor(tx, { + username: "flipper", + name: "Flipper", + host: "mastodon.example", + type: "Person", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/flip", + }); + await insertNotePost(tx, { + account: host.account, + actorId: actor.id, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + assert((await readLink(tx, link.id)).latestActivityAt != null); + + // The actor toggles Mastodon's bot flag, federating as a Service: its + // share no longer qualifies, and refreshing by actor drops the link. + await tx.update(actorTable).set({ type: "Service" }).where( + eq(actorTable.id, actor.id), + ); + await refreshNewsScoresForActor(tx, actor.id); + const botted = await readLink(tx, link.id); + assertEquals(botted.score, 0); + assertEquals(botted.latestActivityAt, null); + assertEquals(botted.postCount, 0); + + // Turning the bot flag back off re-scores the link. + await tx.update(actorTable).set({ type: "Person" }).where( + eq(actorTable.id, actor.id), + ); + await refreshNewsScoresForActor(tx, actor.id); + assert((await readLink(tx, link.id)).latestActivityAt != null); + }); + }, +}); + +// --------------------------------------------------------------------------- +// Repeated-share damping: the same account re-sharing the same link adds little +// extra base weight (recovering with the gap, capped below a first share) and a +// rapid repeat does not refresh freshness. Different accounts are independent, +// and engagement on a repeat post is never discounted. +// --------------------------------------------------------------------------- + +Deno.test({ + name: "recomputeNewsScores damps a rapid repeat share's base mass", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "rapid", + name: "Rapid", + email: "rapid@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/rapid", + }); + const t0 = new Date("2026-05-20T00:00:00.000Z"); + const t1 = new Date("2026-05-20T01:00:00.000Z"); // +1h = 3600s + await insertNotePost(tx, { + account: sharer.account, + published: t0, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: t1, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + assertEquals(row.postCount, 2); + // The second share contributes only repeatFactor(3600) of a base share. + assertAlmostEquals( + row.weightedMass, + NEWS_W_SHARE * (1 + repeatFactor(3600)), + 1e-9, + ); + // Far below two independent shares. + assert(row.weightedMass < 2 * NEWS_W_SHARE); + }); + }, +}); + +Deno.test({ + name: + "recomputeNewsScores lets a long-gap repeat recover, but below a fresh share", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "recover", + name: "Recover", + email: "recover@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/recover", + }); + const t0 = new Date("2026-02-19T00:00:00.000Z"); + const t1 = new Date("2026-05-20T00:00:00.000Z"); // +90 days + const gap = (t1.getTime() - t0.getTime()) / 1000; + await insertNotePost(tx, { + account: sharer.account, + published: t0, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: t1, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + assertAlmostEquals( + row.weightedMass, + NEWS_W_SHARE * (1 + repeatFactor(gap)), + 1e-9, + ); + // The long gap recovers more than a rapid repeat would, yet a repeat is + // always lighter than a fresh share. + assert(repeatFactor(gap) > repeatFactor(3600)); + assert(repeatFactor(gap) < 1); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores does not damp shares from different accounts", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const a = await insertAccountWithActor(tx, { + username: "distincta", + name: "Distinct A", + email: "distincta@example.com", + }); + const b = await insertAccountWithActor(tx, { + username: "distinctb", + name: "Distinct B", + email: "distinctb@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/distinct", + }); + // Same link, same instant, but two different accounts: both full weight. + await insertNotePost(tx, { + account: a.account, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: b.account, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + assertEquals(row.postCount, 2); + assertAlmostEquals(row.weightedMass, 2 * NEWS_W_SHARE, 1e-9); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores keeps a rapid repeat from refreshing freshness", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "nopin", + name: "No Pin", + email: "nopin@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/nopin", + }); + const t0 = new Date("2026-05-20T00:00:00.000Z"); + const t1 = new Date("2026-05-21T00:00:00.000Z"); // +1 day < FRESH_MIN + assert( + (t1.getTime() - t0.getTime()) / 1000 < NEWS_REPEAT_FRESH_MIN_SECONDS, + ); + await insertNotePost(tx, { + account: sharer.account, + published: t0, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: t1, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + assertEquals(row.postCount, 2); + // The rapid second share does not count as fresh activity. + assertEquals(row.latestActivityAt?.getTime(), t0.getTime()); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores lets a long-gap repeat refresh freshness", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "resurface", + name: "Resurface", + email: "resurface@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/resurface", + }); + const t0 = new Date("2026-01-01T00:00:00.000Z"); + const t1 = new Date("2026-05-21T00:00:00.000Z"); // well past FRESH_MIN + await insertNotePost(tx, { + account: sharer.account, + published: t0, + link: { id: link.id, url: link.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: t1, + link: { id: link.id, url: link.url }, + }); + + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + assertEquals(row.latestActivityAt?.getTime(), t1.getTime()); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores still refreshes freshness from a repeat's replies", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "repreply", + name: "Repeat Reply", + email: "repreply@example.com", + }); + const link = await insertPostLink(tx, { + url: "https://example.com/repreply", + }); + const t0 = new Date("2026-05-20T00:00:00.000Z"); + const t1 = new Date("2026-05-21T00:00:00.000Z"); // gap < FRESH_MIN + await insertNotePost(tx, { + account: sharer.account, + published: t0, + link: { id: link.id, url: link.url }, + }); + const { post: repeat } = await insertNotePost(tx, { + account: sharer.account, + published: t1, + link: { id: link.id, url: link.url }, + }); + // A public reply to the rapid repeat, published at t1. + await insertNotePost(tx, { + account: sharer.account, + published: t1, + replyTargetId: repeat.id, + }); + + await recomputeNewsScores(tx); + + const row = await readLink(tx, link.id); + // The bare repeat share would not refresh freshness (gap < FRESH_MIN), but + // its genuine reply does, so the link is fresh as of t1. + assertEquals(row.latestActivityAt?.getTime(), t1.getTime()); + }); + }, +}); + +// --------------------------------------------------------------------------- +// Moderation: score penalties + URL exclusions +// --------------------------------------------------------------------------- + +Deno.test({ + name: "setNewsScorePenalty demotes a link in the popular feed", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "penalty", + name: "Penalty", + email: "penalty@example.com", + }); + const a = await insertPostLink(tx, { url: "https://example.com/pa" }); + const b = await insertPostLink(tx, { url: "https://example.com/pb" }); + const at = new Date("2026-05-20T00:00:00.000Z"); + await insertNotePost(tx, { + account: sharer.account, + published: at, + link: { id: a.id, url: a.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + published: at, + link: { id: b.id, url: b.url }, + }); + await recomputeNewsScores(tx); + + const baseA = (await readLink(tx, a.id)).score; + const baseB = (await readLink(tx, b.id)).score; + assertAlmostEquals(baseA, baseB, 1e-9); // identical base scores + + await setNewsScorePenalty(tx, a.id, NEWS_PENALTY_DEMOTE); + const penalized = await readLink(tx, a.id); + assertEquals(penalized.scorePenalty, NEWS_PENALTY_DEMOTE); + assertAlmostEquals(penalized.score, baseA - NEWS_PENALTY_DEMOTE, 1e-6); + + // The unpenalized peer now ranks above the demoted link. + const popular = await getNewsStories(tx, { order: "popular", limit: 10 }); + const ai = popular.findIndex((l) => l.id === a.id); + const bi = popular.findIndex((l) => l.id === b.id); + assert(ai >= 0 && bi >= 0 && bi < ai); + + // Clearing the penalty restores the score. + await setNewsScorePenalty(tx, a.id, 0); + assertAlmostEquals((await readLink(tx, a.id)).score, baseA, 1e-6); + }); + }, +}); + +Deno.test({ + name: "getNewsStories excludes links matching an exclusion pattern", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "excl", + name: "Excl", + email: "excl@example.com", + }); + const spam = await insertPostLink(tx, { url: "https://spam.example/a" }); + const good = await insertPostLink(tx, { url: "https://good.example/b" }); + await insertNotePost(tx, { + account: sharer.account, + link: { id: spam.id, url: spam.url }, + }); + await insertNotePost(tx, { + account: sharer.account, + link: { id: good.id, url: good.url }, + }); + await recomputeNewsScores(tx); + const before = (await getNewsStories(tx, { order: "popular", limit: 10 })) + .map((l) => l.id); + assert(before.includes(spam.id) && before.includes(good.id)); + + await addNewsExcludedPattern(tx, { pattern: "https://spam.example/*" }); + + // Excluded from every sort order, but the row remains (reachable by id). + for (const order of ["popular", "newest", "allTime"] as const) { + const got = (await getNewsStories(tx, { order, limit: 10 })) + .map((l) => l.id); + assert(!got.includes(spam.id), `${order} must exclude the spam link`); + assert(got.includes(good.id), `${order} must keep the good link`); + } + assertEquals((await readLink(tx, spam.id)).excludedFromNews, true); + assertEquals((await readLink(tx, good.id)).excludedFromNews, false); + + // Removing the pattern un-flags and restores the link. + const [pattern] = await getNewsExcludedPatterns(tx); + await removeNewsExcludedPattern(tx, pattern.id); + assertEquals((await readLink(tx, spam.id)).excludedFromNews, false); + const after = (await getNewsStories(tx, { order: "popular", limit: 10 })) + .map((l) => l.id); + assert(after.includes(spam.id)); + }); + }, +}); + +Deno.test({ + name: "recomputeNewsScores flags a newly-shared link matching a pattern", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "exclnew", + name: "Excl New", + email: "exclnew@example.com", + }); + await addNewsExcludedPattern(tx, { + pattern: "https://blocked.example/*", + }); + const link = await insertPostLink(tx, { + url: "https://blocked.example/post", + }); + await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + await recomputeNewsScores(tx); + + assertEquals((await readLink(tx, link.id)).excludedFromNews, true); + const ids = (await getNewsStories(tx, { order: "popular", limit: 10 })) + .map((l) => l.id); + assert(!ids.includes(link.id)); + }); + }, +}); + +Deno.test({ + name: "addNewsExcludedPattern rejects an invalid URLPattern", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + await assertRejects( + () => addNewsExcludedPattern(tx, { pattern: "https://example.com/(" }), + InvalidNewsPatternError, + ); + }); + }, +}); + +Deno.test({ + name: "getNewsPenalizedStories lists links carrying a penalty", + sanitizeOps: false, + sanitizeResources: false, + async fn() { + await withRollback(async (tx) => { + const sharer = await insertAccountWithActor(tx, { + username: "penlist", + name: "Pen List", + email: "penlist@example.com", + }); + const link = await insertPostLink(tx, { url: "https://example.com/pl" }); + await insertNotePost(tx, { + account: sharer.account, + link: { id: link.id, url: link.url }, + }); + await recomputeNewsScores(tx); + assertEquals((await getNewsPenalizedStories(tx)).length, 0); + + await setNewsScorePenalty(tx, link.id, NEWS_PENALTY_BURY); + const penalized = await getNewsPenalizedStories(tx); + assertEquals(penalized.length, 1); + assertEquals(penalized[0].id, link.id); + + await setNewsScorePenalty(tx, link.id, 0); + assertEquals((await getNewsPenalizedStories(tx)).length, 0); + }); + }, +}); diff --git a/models/news.ts b/models/news.ts new file mode 100644 index 000000000..f987551f6 --- /dev/null +++ b/models/news.ts @@ -0,0 +1,1009 @@ +import { getLogger } from "@logtape/logtape"; +import { + and, + count, + desc, + eq, + gt, + inArray, + isNotNull, + lt, + or, + type SQL, + sql, +} from "drizzle-orm"; +import type { Database, Transaction } from "./db.ts"; +import { + type ActorType, + type NewsExcludedPattern, + newsExcludedPatternTable, + type PostLink, + postLinkTable, +} from "./schema.ts"; +import { generateUuidV7, type Uuid } from "./uuid.ts"; + +const logger = getLogger(["hackerspub", "models", "news"]); + +// --------------------------------------------------------------------------- +// Scoring constants +// +// A link's rank is a Reddit/Hacker-News-style additive-log score, recomputed +// in an idempotent batch. These are deliberately plain code constants (not +// runtime config); tune them here and redeploy. See `recomputeNewsScores` +// for how they combine. +// --------------------------------------------------------------------------- + +/** Weight for a sharing post authored by a local Hackers' Pub account. */ +export const NEWS_SOURCE_WEIGHT_LOCAL = 1.0; +/** Weight for a sharing post from a generic remote fediverse instance. */ +export const NEWS_SOURCE_WEIGHT_REMOTE = 0.8; +/** Weight for a sharing post bridged from Bluesky (`@…@bsky.brid.gy`). */ +export const NEWS_SOURCE_WEIGHT_BLUESKY = 0.5; + +/** Base contribution every public sharing post adds, before engagement. */ +export const NEWS_W_SHARE = 1.0; +/** Weight of each quote of a sharing post (heaviest engagement signal). */ +export const NEWS_W_QUOTE = 3.0; +/** Weight of each reply to a sharing post. */ +export const NEWS_W_REPLY = 2.0; +/** Weight of each reaction to a sharing post (lightest engagement signal). */ +export const NEWS_W_REACT = 0.5; + +/** Maximum additive reputation bonus a single sharer can contribute. */ +export const NEWS_ACCOUNT_WEIGHT_CAP = 2.0; +/** Scales the follower/following reputation term before the cap. */ +export const NEWS_ACCOUNT_RATIO_FACTOR = 0.5; + +/** + * Fixed epoch (2024-01-01T00:00:00Z, in seconds) the recency term is measured + * against. Anchoring to a constant (not `now()`) is what makes the score + * idempotent and time-stable: a link's score is a pure function of the posts + * and engagement around it, so sorting by it never goes stale as the wall + * clock advances; it only changes when the underlying data changes. + */ +export const NEWS_EPOCH_SECONDS = 1_704_067_200; +/** + * Time constant (seconds) for the additive recency term. Smaller values make + * recency dominate engagement mass more strongly. + */ +export const NEWS_TAU_SECONDS = 50_000; + +// --------------------------------------------------------------------------- +// Repeated-share damping +// +// The same account re-sharing the same link should not pile up score: from its +// second share of a given link onward, the extra base-share weight is heavily +// discounted, recovering with the gap since that account's previous share of +// the link but never reaching a first share's weight. A short-gap repeat also +// does not refresh the link's freshness, so rapid re-sharing cannot pin a link +// at the top (genuine replies/quotes/reactions still do). +// --------------------------------------------------------------------------- + +/** + * Maximum base-share weight a *repeat* share of the same link by the same + * account can contribute, as a fraction of a first share. Strictly `< 1`, so a + * repeat is always lighter than the first share however long the gap. + */ +export const NEWS_REPEAT_CAP = 0.5; +/** + * Time constant (seconds) over which a repeat share's base weight recovers + * toward `NEWS_REPEAT_CAP` as the gap since the account's previous share of the + * same link grows. Larger values mean the weight recovers more slowly. + */ +export const NEWS_REPEAT_RECOVERY_TAU_SECONDS = 2_592_000; // 30 days +/** + * Minimum gap (seconds) since an account's previous share of the same link for + * a repeat share to refresh the link's freshness (`latestActivityAt`). Below + * this, a repeat is not treated as fresh activity (so rapid re-sharing cannot + * pin a link at the top); genuine replies/quotes/reactions still refresh it. + */ +export const NEWS_REPEAT_FRESH_MIN_SECONDS = 604_800; // 7 days + +// --------------------------------------------------------------------------- +// Moderator score penalties +// +// A moderator demotes a link by subtracting a penalty from its `score` (the +// `POPULAR` order). Presets, not free numbers, since the raw score scale is +// recency-dominated and unintuitive. Tunable. +// --------------------------------------------------------------------------- + +/** + * "Demote": push a link well down the popular feed (roughly a month of recency + * worth of score), without removing it. + */ +export const NEWS_PENALTY_DEMOTE = 50; +/** + * "Bury": sink a link to the very bottom of the popular feed. Large enough that + * the score goes negative regardless of engagement/recency. (To remove a link + * from every order, use an exclusion pattern instead.) + */ +export const NEWS_PENALTY_BURY = 100_000; + +// Detects a Bluesky-bridged actor (`@…@bsky.brid.gy`). References the actor +// alias `a` and instance alias `i`, which both source queries below join under +// those names, so the classification stays identical wherever it is reused. +// `coalesce(... , false)` keeps the predicate boolean-total: `instance.software` +// can be `null`, and a bare `null ilike …` would make `NOT condition` evaluate +// to `null` (not `true`), which would drop NULL-software remote actors out of +// the `remote` count while the recompute still scores them as remote. +const blueskyBridgeCondition: SQL = sql`( + coalesce(i.software ilike '%bsky%', false) or a.handle_host = 'bsky.brid.gy' +)`; + +/** + * Actor types treated as bots, whose *shares* are excluded from News: a link + * shared only by `Service`/`Application` actors (automated link feeds) must not + * surface as a news story. `Person`, `Group`, and `Organization` stay + * eligible. Replies/quotes/reactions are not filtered by author; only the + * sharing post's actor type matters. Exported so the GraphQL `sharingPosts` + * filter stays in lockstep with this SQL. + */ +export const NEWS_BOT_ACTOR_TYPES: readonly ActorType[] = [ + "Service", + "Application", +]; + +/** + * Whether an actor `type` is treated as a bot for News purposes (its shares are + * excluded). Use this to detect when a federated actor crosses the bot/non-bot + * boundary so the links it shares can be re-scored. + */ +export function isNewsBotActorType(type: ActorType): boolean { + return NEWS_BOT_ACTOR_TYPES.includes(type); +} + +// A qualifying sharing post must be authored by a non-bot actor. References the +// actor alias `a`, which every source query below joins under that name. Cast +// `a.type` to text so the bound type list compares cleanly (an enum column has +// no implicit operator against a bound text parameter), keeping a single source +// of truth with `NEWS_BOT_ACTOR_TYPES`. +const nonBotSharerCondition: SQL = sql`a.type::text not in (${ + sql.join(NEWS_BOT_ACTOR_TYPES.map((t) => sql`${t}`), sql`, `) +})`; + +// --------------------------------------------------------------------------- +// Recompute +// --------------------------------------------------------------------------- + +export interface RecomputeNewsScoresOptions { + /** + * Restrict the recompute to these link ids. Used by the incremental + * single-link refresh on post write. Omit to recompute every link. + */ + readonly linkIds?: readonly Uuid[]; + /** + * Restrict the recompute to links whose `latestActivityAt` is at or after + * this instant. Used by the periodic sweep to bound cost to recently + * active stories. Ignored when `linkIds` is given. + */ + readonly activeSince?: Date; +} + +export interface RecomputeNewsScoresResult { + /** Number of links that have at least one qualifying public share. */ + readonly linksUpdated: number; + /** When the recompute ran. */ + readonly recomputedAt: Date; +} + +function isTransaction(db: Database): db is Transaction { + return "rollback" in db; +} + +/** + * Recompute popularity scores for news links and write them onto + * `post_link`. Idempotent: running it repeatedly on unchanged data yields + * identical `score`/`weightedMass`/`firstSharedAt`/`latestActivityAt` + * (`scoreUpdated` aside). + * + * A "sharing post" is a publicly visible (`public`/`unlisted`), non-boost post + * whose `linkId` points at the link. `weightedMass` sums each sharing post's + * source weight times account-reputation weight times its weighted engagement + * counts; `latestActivityAt` is the freshest of the share, its reactions, and + * its direct replies/quotes. Links that have lost their last qualifying share + * are reset to a zero score and dropped from the feed. + */ +export async function recomputeNewsScores( + db: Database, + options: RecomputeNewsScoresOptions = {}, +): Promise { + const recomputedAt = new Date(); + const scope = resolveScope(options); + + // An explicit but empty link set has nothing to do; skip the round trip. + if (scope.kind === "links" && scope.ids.length < 1) { + return { linksUpdated: 0, recomputedAt }; + } + + const run = async (tx: Database): Promise => { + const linksUpdated = await recomputeAggregate(tx, scope); + await zeroStaleLinks(tx, scope); + // Flag newly-scored (or rescored) links that match an exclusion pattern. + // Scope the pass to the links this recompute touched so the periodic + // `activeSince` sweep stays O(active links) instead of re-testing every + // scored link on each run; a full (`all`) recompute re-evaluates all of + // them, which is what it is for. + const exclusionScope = scope.kind === "links" + ? scope.ids + : scope.kind === "activeSince" + ? await activeLinkIds(tx, scope.since) + : undefined; + await applyNewsExclusions(tx, exclusionScope); + return linksUpdated; + }; + + const linksUpdated = isTransaction(db) + ? await run(db) + : await db.transaction(run); + logger.debug( + "Recomputed news scores for {linksUpdated} link(s).", + { linksUpdated }, + ); + return { linksUpdated, recomputedAt }; +} + +/** + * Best-effort incremental refresh of specific links' scores, for the write + * paths (a link is shared, unshared, or its sharing post changes link). Nulls + * are dropped and deduped; an empty set is a no-op. + * + * Isolation: the recompute runs in its own (sub)transaction so a scoring + * failure rolls back only itself and is swallowed (logged) rather than + * propagating. This matters when `db` is already a transaction: without the + * savepoint a Postgres error would poison the caller's transaction and block + * the post write even though the exception is caught. Engagement-driven + * re-ranking (new replies, quotes, reactions on a story) is left to the + * periodic sweep, which derives its target set from source timestamps and so + * sees correct, settled counts. + */ +export async function refreshNewsScores( + db: Database, + linkIds: ReadonlyArray, +): Promise { + const ids = [...new Set(linkIds.filter((id): id is Uuid => id != null))]; + if (ids.length < 1) return; + try { + await db.transaction((tx) => recomputeNewsScores(tx, { linkIds: ids })); + } catch (error) { + logger.error( + "Failed to refresh news scores for {linkIds}: {error}", + { linkIds: ids, error }, + ); + } +} + +/** + * Refresh the news score for the link a post shares, given only the post's id. + * For destructive paths (e.g. a federated reaction undo) that hold the post id + * but not its row, and which the source-derived sweep cannot detect. + */ +export async function refreshNewsScoresForPostId( + db: Database, + postId: Uuid, +): Promise { + const post = await db.query.postTable.findFirst({ + where: { id: postId }, + columns: { linkId: true }, + }); + if (post?.linkId != null) await refreshNewsScores(db, [post.linkId]); +} + +/** + * Refresh the links a removed or changed post affects: its own shared link, + * plus the link of any post it replied to or quoted (whose public reply/quote + * count just changed). Used by the destructive paths (delete, quote revoke), + * which the source-derived periodic sweep cannot detect, so without this a + * deleted share or reply could keep a link in the feed or inflate its mass + * until a manual full recompute. + */ +export async function refreshNewsScoresForPostLinks( + db: Database, + post: { + readonly linkId: Uuid | null; + readonly replyTargetId: Uuid | null; + readonly quotedPostId: Uuid | null; + }, +): Promise { + const linkIds = new Set(); + if (post.linkId != null) linkIds.add(post.linkId); + const parentIds = [post.replyTargetId, post.quotedPostId].filter( + (id): id is Uuid => id != null, + ); + if (parentIds.length > 0) { + const parents = await db.query.postTable.findMany({ + where: { id: { in: parentIds } }, + columns: { linkId: true }, + }); + for (const parent of parents) { + if (parent.linkId != null) linkIds.add(parent.linkId); + } + } + await refreshNewsScores(db, [...linkIds]); +} + +/** + * Refresh every link an actor shares, for when the actor's `type` crosses the + * bot/non-bot boundary (e.g. a remote `Person` toggles Mastodon's bot flag and + * federates as a `Service`). Such a transition silently changes which of the + * actor's shares qualify, and the periodic sweep cannot detect it (its active + * set is keyed on recent *qualifying* activity, which the just-(un)botted share + * may no longer have), so the caller must trigger this explicitly. + */ +export async function refreshNewsScoresForActor( + db: Database, + actorId: Uuid, +): Promise { + const shares = await db.query.postTable.findMany({ + where: { + actorId, + linkId: { isNotNull: true }, + sharedPostId: { isNull: true }, + }, + columns: { linkId: true }, + }); + await refreshNewsScores(db, shares.map((s) => s.linkId)); +} + +/** Which links a recompute targets. */ +type RecomputeScope = + | { readonly kind: "all" } + | { readonly kind: "links"; readonly ids: readonly Uuid[] } + | { readonly kind: "activeSince"; readonly since: Date }; + +function resolveScope(options: RecomputeNewsScoresOptions): RecomputeScope { + if (options.linkIds != null) return { kind: "links", ids: options.linkIds }; + if (options.activeSince != null) { + return { kind: "activeSince", since: options.activeSince }; + } + return { kind: "all" }; +} + +/** + * A `AND …` predicate that narrows a statement to the scope's + * links, or empty SQL for a full recompute. The set always stays inside SQL + * (an explicit id list is bound as one `uuid[]` array; the `activeSince` set is + * a subquery) so a large sweep never expands into thousands of bind + * parameters. + */ +function scopeFilter(scope: RecomputeScope, linkIdColumn: SQL): SQL { + switch (scope.kind) { + case "all": + return sql``; + case "links": { + // UUIDs contain no array-literal metacharacters, so a plain `{a,b}` + // literal bound as a single string parameter is safe. + const literal = `{${scope.ids.join(",")}}`; + return sql` and ${linkIdColumn} = any(${literal}::uuid[])`; + } + case "activeSince": + return sql` and ${linkIdColumn} in (${ + activeLinkIdsSubquery(scope.since) + })`; + } +} + +/** + * The companion of `scopeFilter` for the stale-link reset. A link that just + * lost its last public share is, by definition, *absent* from + * `activeLinkIdsSubquery` (which requires a qualifying share), so the sweep + * must scope its zeroing by the stored `latest_activity_at` instead: reset + * recently-scored links that no longer qualify. `all`/`links` scopes match + * `scopeFilter`. + */ +function staleScopeFilter(scope: RecomputeScope): SQL { + switch (scope.kind) { + case "all": + return sql``; + case "links": { + const literal = `{${scope.ids.join(",")}}`; + return sql` and pl.id = any(${literal}::uuid[])`; + } + case "activeSince": + return sql` and pl.latest_activity_at >= ${scope.since.toISOString()}::timestamptz`; + } +} + +/** + * Subquery of link ids with any engagement at or after `activeSince`: a + * qualifying share published since then, a reaction created since then, or a + * public direct reply/quote published since then. The periodic sweep scopes + * to this (rather than the stored `latestActivityAt`) so a fresh reaction on an + * *older* story is still picked up: that story's stored `latestActivityAt` is + * still old, but its underlying reaction is new. + */ +function activeLinkIdsSubquery(activeSince: Date): SQL { + // Raw `sql` does not bind a JS `Date`; pass an ISO string cast to + // `timestamptz`. + const since = sql`${activeSince.toISOString()}::timestamptz`; + return sql` + select s.link_id + from post s + join actor a on a.id = s.actor_id + where s.link_id is not null + and s.visibility in ('public', 'unlisted') + and s.shared_post_id is null + and ${nonBotSharerCondition} + and ( + s.published >= ${since} + -- A federated Update to a share (e.g. its replies/likes totals) bumps + -- the updated column, so this catches remote engagement-count changes + -- that create no local reaction/reply/quote row. + or s.updated >= ${since} + or exists ( + select 1 from reaction r + where r.post_id = s.id and r.created >= ${since} + ) + or exists ( + select 1 from post c + where (c.reply_target_id = s.id or c.quoted_post_id = s.id) + and c.visibility in ('public', 'unlisted') + and c.published >= ${since} + ) + ) + `; +} + +/** + * Materialize the `activeSince` link set so the exclusion pass can be scoped to + * the same links the recompute touched, rather than re-testing every scored + * link. Deduped, since the subquery yields one row per qualifying share. + */ +async function activeLinkIds(db: Database, activeSince: Date): Promise { + const rows = await db.execute( + activeLinkIdsSubquery(activeSince), + ) as unknown as { link_id: Uuid }[]; + return [...new Set(rows.map((row) => row.link_id))]; +} + +/** + * The single set-based aggregation: derive per-link mass and activity from the + * sharing posts and their engagement, then write the score. Returns the + * number of links written. + */ +async function recomputeAggregate( + db: Database, + scope: RecomputeScope, +): Promise { + const result = await db.execute(sql` + with shares as ( + select + p.link_id as link_id, + p.id as post_id, + p.published as published, + p.reactions_count as reactions_count, + case + when a.account_id is not null then ${NEWS_SOURCE_WEIGHT_LOCAL}::double precision + when ${blueskyBridgeCondition} + then ${NEWS_SOURCE_WEIGHT_BLUESKY}::double precision + else ${NEWS_SOURCE_WEIGHT_REMOTE}::double precision + end as source_weight, + (1 + least( + ${NEWS_ACCOUNT_WEIGHT_CAP}::double precision, + log((1 + greatest(a.followers_count, 0))::double precision) + * (1 + greatest(a.followers_count, 0)::double precision + / (greatest(a.followees_count, 0) + 1)) + * ${NEWS_ACCOUNT_RATIO_FACTOR}::double precision + )) as account_weight, + -- Seconds since this account's previous share of this same link (NULL + -- for its first share), to damp repeated re-shares below. + extract(epoch from ( + p.published - lag(p.published) over ( + partition by p.actor_id, p.link_id order by p.published, p.id + ) + )) as gap_seconds + from post p + join actor a on a.id = p.actor_id + join instance i on i.host = a.instance_host + where p.link_id is not null + and p.visibility in ('public', 'unlisted') + and p.shared_post_id is null + and ${nonBotSharerCondition}${scopeFilter(scope, sql`p.link_id`)} + ), + -- Count only public/unlisted replies and quotes: the denormalized + -- replies_count/quotes_count include followers-only and direct posts, + -- which must not influence (or leak through) a public news score. + reply_counts as ( + select s.post_id as post_id, count(*) as cnt + from shares s + join post c on c.reply_target_id = s.post_id + and c.visibility in ('public', 'unlisted') + group by s.post_id + ), + quote_counts as ( + select s.post_id as post_id, count(*) as cnt + from shares s + join post c on c.quoted_post_id = s.post_id + and c.visibility in ('public', 'unlisted') + group by s.post_id + ), + child_activity as ( + select s.link_id as link_id, max(c.published) as latest + from shares s + join post c + on (c.reply_target_id = s.post_id or c.quoted_post_id = s.post_id) + and c.visibility in ('public', 'unlisted') + group by s.link_id + ), + reaction_activity as ( + select s.link_id as link_id, max(r.created) as latest + from shares s + join reaction r on r.post_id = s.post_id + group by s.link_id + ), + agg as ( + select + s.link_id as link_id, + count(*) as post_count, + min(s.published) as first_shared_at, + -- Only a first share or a sufficiently-gapped re-share refreshes the + -- link's freshness, so rapid re-sharing cannot pin it at the top. + -- (Genuine replies/quotes/reactions still refresh it via the activity + -- CTEs below.) Every account's first share has a NULL gap, so this is + -- non-NULL whenever the link has any share. + max(s.published) filter ( + where s.gap_seconds is null + or s.gap_seconds >= ${NEWS_REPEAT_FRESH_MIN_SECONDS}::double precision + ) as latest_share, + sum( + s.source_weight * s.account_weight * ( + -- Damp only the base share weight of a repeat: it recovers toward + -- NEWS_REPEAT_CAP as the gap since this account's previous share of + -- this link grows, but never reaches a first share's weight. The + -- per-post engagement below is never discounted. + ${NEWS_W_SHARE}::double precision * ( + case + when s.gap_seconds is null then 1::double precision + else ${NEWS_REPEAT_CAP}::double precision * ( + 1 - exp( + -s.gap_seconds + / ${NEWS_REPEAT_RECOVERY_TAU_SECONDS}::double precision + ) + ) + end + ) + + ${NEWS_W_QUOTE}::double precision * coalesce(qc.cnt, 0) + + ${NEWS_W_REPLY}::double precision * coalesce(rc.cnt, 0) + + ${NEWS_W_REACT}::double precision * s.reactions_count + ) + ) as weighted_mass + from shares s + left join reply_counts rc on rc.post_id = s.post_id + left join quote_counts qc on qc.post_id = s.post_id + group by s.link_id + ), + final as ( + select + agg.link_id as link_id, + agg.post_count as post_count, + agg.first_shared_at as first_shared_at, + greatest(agg.latest_share, ca.latest, ra.latest) as latest_activity_at, + agg.weighted_mass as weighted_mass + from agg + left join child_activity ca on ca.link_id = agg.link_id + left join reaction_activity ra on ra.link_id = agg.link_id + ) + update post_link pl set + post_count = final.post_count, + -- Truncate to milliseconds so the stored value matches the precision of + -- the JS-Date-derived NEWEST feed cursor (toISOString is ms-only); + -- otherwise sub-millisecond rows after a cursor would be skipped. + first_shared_at = date_trunc('milliseconds', final.first_shared_at), + latest_activity_at = final.latest_activity_at, + weighted_mass = final.weighted_mass, + recency_component = + (extract(epoch from final.latest_activity_at) - ${NEWS_EPOCH_SECONDS}::double precision) + / ${NEWS_TAU_SECONDS}::double precision, + score = + log(greatest(1::double precision, final.weighted_mass)) + + (extract(epoch from final.latest_activity_at) - ${NEWS_EPOCH_SECONDS}::double precision) + / ${NEWS_TAU_SECONDS}::double precision + - pl.score_penalty, + score_updated = now() + from final + where pl.id = final.link_id + returning pl.id + `); + return result.length; +} + +/** + * Reset links that were scored before but no longer have any qualifying public + * share, so they drop out of the feed indexes. + */ +async function zeroStaleLinks( + db: Database, + scope: RecomputeScope, +): Promise { + await db.execute(sql` + update post_link pl set + score = 0, + weighted_mass = 0, + recency_component = 0, + post_count = 0, + first_shared_at = null, + latest_activity_at = null, + score_updated = now() + where pl.latest_activity_at is not null${staleScopeFilter(scope)} + and not exists ( + select 1 from post p + join actor a on a.id = p.actor_id + where p.link_id = pl.id + and p.visibility in ('public', 'unlisted') + and p.shared_post_id is null + and ${nonBotSharerCondition} + ) + `); +} + +// --------------------------------------------------------------------------- +// Feed reads +// --------------------------------------------------------------------------- + +export type NewsOrder = "popular" | "newest" | "allTime"; + +export interface NewsStoriesCursor { + /** The active order's sort scalar of the last row on the previous page. */ + readonly value: number | Date; + /** The id of the last row on the previous page (keyset tiebreaker). */ + readonly id: Uuid; +} + +export interface GetNewsStoriesOptions { + readonly order: NewsOrder; + readonly limit: number; + readonly after?: NewsStoriesCursor; +} + +/** + * Read a page of ranked news links (newest/most-popular first). Keyset + * pagination on `(sortKey, id)` matching the partial feed indexes; pass the + * previous page's last row as `after`. Only links with at least one public + * share (`latestActivityAt IS NOT NULL`) are returned. + */ +export async function getNewsStories( + db: Database, + options: GetNewsStoriesOptions, +): Promise { + const sortColumn = options.order === "newest" + ? postLinkTable.firstSharedAt + : options.order === "allTime" + ? postLinkTable.weightedMass + : postLinkTable.score; + + const conditions: SQL[] = [ + isNotNull(postLinkTable.latestActivityAt), + eq(postLinkTable.excludedFromNews, false), + ]; + if (options.after != null) { + const { value, id } = options.after; + conditions.push( + or( + lt(sortColumn, value), + and(eq(sortColumn, value), lt(postLinkTable.id, id)), + )!, + ); + } + + return await db + .select() + .from(postLinkTable) + .where(and(...conditions)) + .orderBy(desc(sortColumn), desc(postLinkTable.id)) + .limit(options.limit); +} + +// --------------------------------------------------------------------------- +// Status (admin) +// --------------------------------------------------------------------------- + +export interface NewsScoreStatus { + /** Links currently in the feed (with at least one public share). */ + readonly scoredLinkCount: number; + /** When scores were last recomputed, or `null` if never. */ + readonly lastRecomputedAt: Date | null; +} + +/** Snapshot of news scoring state for the moderator admin page. */ +export async function getNewsScoreStatus( + db: Database, +): Promise { + const [counts] = await db + .select({ scoredLinkCount: count() }) + .from(postLinkTable) + .where(isNotNull(postLinkTable.latestActivityAt)); + // Read the column directly (ordered, not aggregated) so drizzle applies the + // timestamptz -> Date mapping; `max()` would hand back a raw Postgres string + // the GraphQL `DateTime` scalar then refuses to serialize. + const [latest] = await db + .select({ scoreUpdated: postLinkTable.scoreUpdated }) + .from(postLinkTable) + .where(isNotNull(postLinkTable.scoreUpdated)) + .orderBy(desc(postLinkTable.scoreUpdated)) + .limit(1); + return { + scoredLinkCount: Number(counts?.scoredLinkCount ?? 0), + lastRecomputedAt: latest?.scoreUpdated ?? null, + }; +} + +// --------------------------------------------------------------------------- +// Source breakdown +// --------------------------------------------------------------------------- + +export interface NewsSourceBreakdown { + /** Public sharing posts from local Hackers' Pub accounts. */ + readonly local: number; + /** Public sharing posts from generic remote fediverse instances. */ + readonly remote: number; + /** Public sharing posts bridged from Bluesky (`@…@bsky.brid.gy`). */ + readonly bluesky: number; +} + +/** + * Count, per link, how many of its public sharing posts come from local, + * generic-remote, and Bluesky-bridged accounts. Batched for the GraphQL + * `PostLink.sourceBreakdown` loader; links with no public share are absent + * from the returned map. + */ +export async function getNewsSourceBreakdowns( + db: Database, + linkIds: readonly Uuid[], +): Promise> { + const result = new Map(); + const ids = [...new Set(linkIds)]; + if (ids.length < 1) return result; + const literal = `{${ids.join(",")}}`; + const rows = await db.execute< + { link_id: Uuid; local: string; remote: string; bluesky: string } + >(sql` + select + p.link_id as link_id, + count(*) filter (where a.account_id is not null) as local, + count(*) filter ( + where a.account_id is null and ${blueskyBridgeCondition} + ) as bluesky, + count(*) filter ( + where a.account_id is null and not ${blueskyBridgeCondition} + ) as remote + from post p + join actor a on a.id = p.actor_id + join instance i on i.host = a.instance_host + where p.link_id = any(${literal}::uuid[]) + and p.visibility in ('public', 'unlisted') + and p.shared_post_id is null + and ${nonBotSharerCondition} + group by p.link_id + `); + for (const row of rows) { + result.set(row.link_id, { + local: Number(row.local), + remote: Number(row.remote), + bluesky: Number(row.bluesky), + }); + } + return result; +} + +// --------------------------------------------------------------------------- +// Discussion count +// --------------------------------------------------------------------------- + +/** + * Count, per link, the size of its federated discussion: the non-bot public + * sharing posts plus their direct public (`public`/`unlisted`) replies and + * quotes. Batched for the GraphQL `PostLink.discussionCount` loader; links + * with no qualifying share are absent from the returned map. Counts direct + * children only (deeper nesting is not traversed); replies/quotes are not + * author-filtered, matching the discussion the link's page renders. + */ +export async function getNewsDiscussionCounts( + db: Database, + linkIds: readonly Uuid[], +): Promise> { + const result = new Map(); + const ids = [...new Set(linkIds)]; + if (ids.length < 1) return result; + const literal = `{${ids.join(",")}}`; + // Collect each link's distinct posts as `(link_id, post_id)` pairs (the + // sharing posts plus their direct public replies and quotes), then count per + // link. `union` (not `union all`) deduplicates, so a single post that is + // both a reply and a quote of the link's shares (or replies to one share and + // quotes another) is counted once, matching the deduplicated discussion tree. + const rows = await db.execute<{ link_id: Uuid; cnt: string | number }>(sql` + with shares as ( + select p.id as post_id, p.link_id as link_id + from post p + join actor a on a.id = p.actor_id + where p.link_id = any(${literal}::uuid[]) + and p.visibility in ('public', 'unlisted') + and p.shared_post_id is null + and ${nonBotSharerCondition} + ) + select link_id, count(*) as cnt + from ( + select link_id, post_id from shares + union + select s.link_id, c.id as post_id + from shares s + join post c on c.reply_target_id = s.post_id + and c.visibility in ('public', 'unlisted') + union + select s.link_id, c.id as post_id + from shares s + join post c on c.quoted_post_id = s.post_id + and c.visibility in ('public', 'unlisted') + ) posts + group by link_id + `); + for (const row of rows) { + result.set(row.link_id, Number(row.cnt)); + } + return result; +} + +// --------------------------------------------------------------------------- +// Moderation: exclusion patterns + score penalties +// --------------------------------------------------------------------------- + +/** Thrown when a news exclusion pattern is not a valid `URLPattern`. */ +export class InvalidNewsPatternError extends Error {} + +/** + * Re-evaluate `excludedFromNews` for the given links (or every scored link when + * omitted) against the current exclusion patterns: a link is excluded when its + * URL matches any pattern (Web-standard `URLPattern`). Invalid stored patterns + * are skipped (and logged). Only rows whose flag actually changes are written. + */ +export async function applyNewsExclusions( + db: Database, + linkIds?: readonly Uuid[], +): Promise { + if (linkIds != null && linkIds.length < 1) return; + const scope = linkIds != null + ? inArray(postLinkTable.id, [...linkIds]) + : isNotNull(postLinkTable.latestActivityAt); + + const patternRows = await db + .select({ pattern: newsExcludedPatternTable.pattern }) + .from(newsExcludedPatternTable); + const patterns: URLPattern[] = []; + for (const { pattern } of patternRows) { + try { + patterns.push(new URLPattern(pattern)); + } catch (error) { + logger.warn( + "Skipping invalid news exclusion pattern {pattern}: {error}", + { pattern, error }, + ); + } + } + + const links = await db + .select({ id: postLinkTable.id, url: postLinkTable.url }) + .from(postLinkTable) + .where(scope); + const excludedIds = links + .filter((link) => patterns.some((p) => p.test(link.url))) + .map((link) => link.id); + const literal = `{${excludedIds.join(",")}}`; + const target = sql`${postLinkTable.id} = any(${literal}::uuid[])`; + // `is distinct from` skips no-op writes so a periodic full pass does not churn + // every scored row. + await db + .update(postLinkTable) + .set({ excludedFromNews: target }) + .where( + and( + scope, + sql`${postLinkTable.excludedFromNews} is distinct from ${target}`, + ), + ); +} + +/** List all exclusion patterns, newest first (for the admin page). */ +export function getNewsExcludedPatterns( + db: Database, +): Promise { + return db + .select() + .from(newsExcludedPatternTable) + .orderBy(desc(newsExcludedPatternTable.created)); +} + +/** + * Add an exclusion pattern (idempotent on the pattern string) and re-flag every + * scored link against it. Throws `InvalidNewsPatternError` if the pattern is + * not a valid `URLPattern`. + */ +export async function addNewsExcludedPattern( + db: Database, + values: { pattern: string; note?: string | null; creatorId?: Uuid | null }, +): Promise { + const pattern = values.pattern.trim(); + if (pattern.length < 1) { + throw new InvalidNewsPatternError("Pattern must not be empty."); + } + try { + new URLPattern(pattern); + } catch (error) { + throw new InvalidNewsPatternError( + `Invalid URLPattern: ${pattern} (${error})`, + ); + } + const run = async (tx: Database): Promise => { + const inserted = await tx + .insert(newsExcludedPatternTable) + .values({ + id: generateUuidV7(), + pattern, + note: values.note?.trim() || null, + creatorId: values.creatorId ?? null, + }) + .onConflictDoNothing({ target: newsExcludedPatternTable.pattern }) + .returning(); + let row = inserted[0]; + if (row == null) { + [row] = await tx + .select() + .from(newsExcludedPatternTable) + .where(eq(newsExcludedPatternTable.pattern, pattern)) + .limit(1); + } + if (row == null) throw new Error("Failed to persist exclusion pattern."); + await applyNewsExclusions(tx); + return row; + }; + return isTransaction(db) ? await run(db) : await db.transaction(run); +} + +/** Remove an exclusion pattern and un-flag links it no longer matches. */ +export async function removeNewsExcludedPattern( + db: Database, + id: Uuid, +): Promise { + const run = async (tx: Database): Promise => { + const rows = await tx + .delete(newsExcludedPatternTable) + .where(eq(newsExcludedPatternTable.id, id)) + .returning(); + if (rows.length < 1) return false; + await applyNewsExclusions(tx); + return true; + }; + return isTransaction(db) ? await run(db) : await db.transaction(run); +} + +/** + * Set a link's moderator score penalty (subtracted from its `score`) and + * recompute that link so the feed reflects it. + */ +export async function setNewsScorePenalty( + db: Database, + linkId: Uuid, + penalty: number, +): Promise { + // Guard against a negative penalty (which would *boost* the score) or a + // non-finite value poisoning the ranking column. + if (!Number.isFinite(penalty) || penalty < 0) { + throw new RangeError(`Invalid news score penalty: ${penalty}`); + } + const run = async (tx: Database): Promise => { + await tx + .update(postLinkTable) + .set({ scorePenalty: penalty }) + .where(eq(postLinkTable.id, linkId)); + await recomputeNewsScores(tx, { linkIds: [linkId] }); + }; + if (isTransaction(db)) await run(db); + else await db.transaction(run); +} + +/** Links currently carrying a moderator penalty, heaviest first (admin review). */ +export function getNewsPenalizedStories(db: Database): Promise { + return db + .select() + .from(postLinkTable) + .where(gt(postLinkTable.scorePenalty, 0)) + .orderBy(desc(postLinkTable.scorePenalty), desc(postLinkTable.id)) + .limit(100); +} diff --git a/models/post.ts b/models/post.ts index 6aa5e0c6e..e30c57c93 100644 --- a/models/post.ts +++ b/models/post.ts @@ -50,6 +50,7 @@ import type { Database, RelationsFilter, Transaction } from "./db.ts"; import { extractExternalLinks } from "./html.ts"; import { getMissingArticleMediumLabel, renderMarkup } from "./markup.ts"; import { persistPostMedium } from "./medium.ts"; +import { refreshNewsScores, refreshNewsScoresForPostLinks } from "./news.ts"; import { createQuotedPostUpdatedNotification, createSharedPostUpdatedNotification, @@ -585,6 +586,7 @@ export async function syncPostFromNoteSource( quotedPostId: true, quoteAuthorizationIri: true, quoteTargetState: true, + linkId: true, }, where: { noteSourceId: noteSource.id }, }); @@ -642,9 +644,13 @@ export async function syncPostFromNoteSource( }`, ]), ), - linkId: link?.id, + // Use explicit `null` (not `undefined`) so editing a note to remove its + // link actually clears `link_id` on update: drizzle drops `undefined` from + // the update set, which would otherwise leave the old link attached (and + // keep a dropped story in the news feed). Matches `persistPost`. + linkId: link?.id ?? null, linkUrl: link == null - ? undefined + ? null : externalLinks[0].hash === "" ? link.url : new URL(externalLinks[0].hash, link.url).href, @@ -742,6 +748,9 @@ export async function syncPostFromNoteSource( with: { actor: true }, }) ?? null; const quoteRequestTarget = quoteRequestRequired ? quotedPost ?? null : null; + // Score the link this note now shares; also refresh the previous link when + // an edit changed or removed it, so the old story can drop out. + await refreshNewsScores(db, [post.linkId, existingPost?.linkId]); return { ...post, actor, @@ -964,7 +973,13 @@ export async function persistPost( ); let quoteAuthorizationIri = post.quoteAuthorizationId?.href; const existingPost = await db.query.postTable.findFirst({ - columns: { id: true, name: true, contentHtml: true, quotedPostId: true }, + columns: { + id: true, + name: true, + contentHtml: true, + quotedPostId: true, + linkId: true, + }, where: { iri: post.id.href }, }); if (quoteAuthorizationIri != null && quotedPost != null) { @@ -1286,6 +1301,11 @@ export async function persistPost( if (post instanceof vocab.Question) { poll = await persistPoll(db, post, persistedPost.id); } + // Only refresh at the top level: recursive reply backfill (depth > 0) would + // re-score the same story once per reply; the periodic sweep covers those. + if (depth === 0) { + await refreshNewsScores(db, [persistedPost.linkId, existingPost?.linkId]); + } return { ...persistedPost, replyTarget: replyTarget ?? null, @@ -1934,12 +1954,21 @@ export async function deletePersistedPost( ).returning(); if (deletedPosts.length < 1) return false; const [deletedPost] = deletedPosts; - if (deletedPost.replyTargetId == null) return true; - const replyTarget = await db.query.postTable.findFirst({ - where: { id: deletedPost.replyTargetId }, - }); - if (replyTarget == null) return true; - await updateRepliesCount(db, replyTarget, -1); + if (deletedPost.replyTargetId != null) { + const replyTarget = await db.query.postTable.findFirst({ + where: { id: deletedPost.replyTargetId }, + }); + if (replyTarget != null) await updateRepliesCount(db, replyTarget, -1); + } + if (deletedPost.quotedPostId != null) { + const quotedPost = await db.query.postTable.findFirst({ + where: { id: deletedPost.quotedPostId }, + }); + if (quotedPost != null) await updateQuotesCount(db, quotedPost, -1); + } + // Re-score the link this post shared and the links of the posts it replied + // to / quoted (their public reply/quote count dropped). + await refreshNewsScoresForPostLinks(db, deletedPost); return true; } @@ -2360,6 +2389,8 @@ export async function revokeQuote( } } await updateQuotesCount(db, quotedPost, -1); + // The quoted post lost a public quote, so re-score its link. + await refreshNewsScores(db, [quotedPost.linkId]); return updatedPost; } @@ -2579,6 +2610,33 @@ export async function deletePost( } } } + // Re-score every link affected by this cascade: the link each deleted post + // shared (this post plus its bulk-deleted replies/quotes/boosts, any of which + // may itself be a sharing post), and the links of the posts this post replied + // to / quoted (whose public reply/quote count dropped). + const affectedLinkIds = new Set(); + const parentIds = new Set(); + for (const deleted of interactions) { + if (deleted.linkId != null) affectedLinkIds.add(deleted.linkId); + // A bulk-deleted interaction may reply to or quote a story other than this + // post (e.g. a post that quoted this one while also replying to a different + // story); that story's public reply/quote count just dropped too. + if (deleted.replyTargetId != null) parentIds.add(deleted.replyTargetId); + if (deleted.quotedPostId != null) parentIds.add(deleted.quotedPostId); + } + for (const original of originalPosts) { + if (original.linkId != null) affectedLinkIds.add(original.linkId); + } + if (parentIds.size > 0) { + const parents = await db.query.postTable.findMany({ + where: { id: { in: [...parentIds] } }, + columns: { linkId: true }, + }); + for (const parent of parents) { + if (parent.linkId != null) affectedLinkIds.add(parent.linkId); + } + } + await refreshNewsScores(db, [...affectedLinkIds]); const noteSourceIds = interactions .filter((i) => i.noteSourceId != null) .map((i) => i.noteSourceId!); diff --git a/models/reaction.ts b/models/reaction.ts index 24f513662..82df122d6 100644 --- a/models/reaction.ts +++ b/models/reaction.ts @@ -7,6 +7,7 @@ import { getPersistedActor, persistActor } from "./actor.ts"; import type { ContextData } from "./context.ts"; import type { Database } from "./db.ts"; import { DEFAULT_REACTION_EMOJI, type ReactionEmoji } from "./emoji.ts"; +import { refreshNewsScores } from "./news.ts"; import { createReactNotification, deleteReactNotification, @@ -305,6 +306,9 @@ export async function undoReaction( .returning(); if (rows.length < 1) return undefined; await updateReactionsCounts(db, post.id); + // If the reacted post shares a link, its reaction total just dropped; the + // periodic sweep can't see a removed reaction, so re-score the link here. + await refreshNewsScores(db, [post.linkId]); if (post.actor.accountId != null && post.actorId !== account.actor.id) { let notifEmoji: string | CustomEmoji = emoji ?? DEFAULT_REACTION_EMOJI; if (emoji == null && customEmojiId != null) { diff --git a/models/schema.ts b/models/schema.ts index cd733d25a..166ca2cad 100644 --- a/models/schema.ts +++ b/models/schema.ts @@ -5,6 +5,7 @@ import { boolean, bytea, check, + doublePrecision, foreignKey, index, integer, @@ -1115,6 +1116,20 @@ export const postLinkTable = pgTable( creatorId: uuid("creator_id") .$type() .references((): AnyPgColumn => actorTable.id, { onDelete: "set null" }), + score: doublePrecision().notNull().default(0), + weightedMass: doublePrecision("weighted_mass").notNull().default(0), + recencyComponent: doublePrecision("recency_component").notNull().default(0), + postCount: integer("post_count").notNull().default(0), + firstSharedAt: timestamp("first_shared_at", { withTimezone: true }), + latestActivityAt: timestamp("latest_activity_at", { withTimezone: true }), + scoreUpdated: timestamp("score_updated", { withTimezone: true }), + // Moderator-applied penalty subtracted from `score` to demote a link in the + // feed. Persisted across recomputes (the recompute reads and re-applies it). + scorePenalty: doublePrecision("score_penalty").notNull().default(0), + // Set when the link's URL matches a `news_excluded_pattern`; excludes it + // from the feed list (every sort order) while leaving its discussion page + // reachable. Recomputed from the patterns, not edited directly. + excludedFromNews: boolean("excluded_from_news").notNull().default(false), created: timestamp({ withTimezone: true }) .notNull() .default(currentTimestamp), @@ -1164,12 +1179,52 @@ export const postLinkTable = pgTable( `, ), index().on(table.creatorId), + // News feed sorts. The partial predicate `latest_activity_at IS NOT NULL` + // is the canonical "has at least one public, non-boost sharing post" flag, + // so scraped-but-never-publicly-shared links stay out of every feed query. + // Every index carries the `id DESC` tiebreaker so it fully covers the + // `(sortKey, id)` keyset pagination order (scores tie at 0 before/between + // batch runs, and `first_shared_at` timestamps can collide). + index("idx_post_link_score") + .on(desc(table.score), desc(table.id)) + .where(isNotNull(table.latestActivityAt)), + index("idx_post_link_first_shared") + .on(desc(table.firstSharedAt), desc(table.id)) + .where(isNotNull(table.latestActivityAt)), + index("idx_post_link_weighted_mass") + .on(desc(table.weightedMass), desc(table.id)) + .where(isNotNull(table.latestActivityAt)), ], ); export type PostLink = typeof postLinkTable.$inferSelect; export type NewPostLink = typeof postLinkTable.$inferInsert; +// Moderator-managed URL patterns; a link whose URL matches any of these is +// excluded from the News feed list. Patterns are Web-standard `URLPattern` +// strings (e.g. `https://example.com/*`, `https://*.example.com/*`). +export const newsExcludedPatternTable = pgTable( + "news_excluded_pattern", + { + id: uuid().$type().primaryKey(), + pattern: text().notNull().unique(), + note: text(), + creatorId: uuid("creator_id") + .$type() + .references((): AnyPgColumn => accountTable.id, { onDelete: "set null" }), + created: timestamp({ withTimezone: true }) + .notNull() + .default(currentTimestamp), + }, + (table) => [ + index().on(table.creatorId), + ], +); + +export type NewsExcludedPattern = typeof newsExcludedPatternTable.$inferSelect; +export type NewNewsExcludedPattern = + typeof newsExcludedPatternTable.$inferInsert; + export const pollTable = pgTable( "poll", { diff --git a/test/postgres.ts b/test/postgres.ts index 533b5d778..a257f3a21 100644 --- a/test/postgres.ts +++ b/test/postgres.ts @@ -9,11 +9,15 @@ import { accountEmailTable, accountTable, actorTable, + type ActorType, instanceTable, mentionTable, type NewPost, noteSourceTable, + type PostLink, + postLinkTable, postTable, + reactionTable, } from "@hackerspub/models/schema"; import { generateUuidV7 } from "@hackerspub/models/uuid"; import type { Uuid } from "@hackerspub/models/uuid"; @@ -61,6 +65,26 @@ export async function seedLocalInstance( }).onConflictDoNothing(); } +/** + * Seed (or override) an instance row with a specific `software` value, e.g. + * `"mastodon"` or `"bsky.brid.gy"`, so news source-weight tests can control + * how a remote actor's instance is classified. + */ +export async function seedInstance( + tx: Transaction, + host: string, + software: string, +): Promise { + await tx.insert(instanceTable).values({ + host, + software, + softwareVersion: "test", + }).onConflictDoUpdate({ + target: instanceTable.host, + set: { software, softwareVersion: "test" }, + }); +} + export async function insertAccountWithActor( tx: Transaction, values: { @@ -70,6 +94,9 @@ export async function insertAccountWithActor( iri?: string; inboxUrl?: string; host?: string; + type?: ActorType; + followersCount?: number; + followeesCount?: number; }, ): Promise<{ account: AuthenticatedAccount; @@ -103,12 +130,14 @@ export async function insertAccountWithActor( await tx.insert(actorTable).values({ id: actorId, iri: values.iri ?? `http://${host}/@${values.username}`, - type: "Person", + type: values.type ?? "Person", username: values.username, instanceHost: host, handleHost: host, accountId, name: values.name, + followersCount: values.followersCount ?? 0, + followeesCount: values.followeesCount ?? 0, inboxUrl: values.inboxUrl ?? `http://${host}/@${values.username}/inbox`, sharedInboxUrl: `http://${host}/inbox`, created: timestamp, @@ -143,6 +172,10 @@ export async function insertRemoteActor( iri?: string; inboxUrl?: string; url?: string; + handleHost?: string; + type?: ActorType; + followersCount?: number; + followeesCount?: number; }, ) { const actorId = generateUuidV7(); @@ -153,11 +186,13 @@ export async function insertRemoteActor( await tx.insert(actorTable).values({ id: actorId, iri: values.iri ?? `https://${values.host}/users/${values.username}`, - type: "Person", + type: values.type ?? "Person", username: values.username, instanceHost: values.host, - handleHost: values.host, + handleHost: values.handleHost ?? values.host, name: values.name, + followersCount: values.followersCount ?? 0, + followeesCount: values.followeesCount ?? 0, inboxUrl: values.inboxUrl ?? `https://${values.host}/users/${values.username}/inbox`, sharedInboxUrl: `https://${values.host}/inbox`, @@ -184,9 +219,13 @@ export async function insertNotePost( quotePolicy?: "everyone" | "followers" | "self"; quoteRequestPolicy?: "everyone" | "followers" | "self"; reactionsCounts?: Record; + repliesCount?: number; + quotesCount?: number; + sharesCount?: number; replyTargetId?: Uuid; quotedPostId?: Uuid; sharedPostId?: Uuid; + link?: { id: Uuid; url: string }; published?: Date; updated?: Date; }, @@ -227,10 +266,15 @@ export async function insertNotePost( sharedPostId: values.sharedPostId, replyTargetId: values.replyTargetId, quotedPostId: values.quotedPostId, + linkId: values.link?.id, + linkUrl: values.link?.url, contentHtml: values.contentHtml ?? `

${values.content ?? "Hello world"}

`, language: values.language ?? "en", reactionsCounts: values.reactionsCounts ?? {}, + repliesCount: values.repliesCount, + quotesCount: values.quotesCount, + sharesCount: values.sharesCount, url: `http://localhost/@${values.account.username}/${noteSourceId}`, published: timestamp, updated, @@ -255,11 +299,16 @@ export async function insertRemotePost( visibility?: "public" | "unlisted" | "followers" | "direct" | "none"; quotePolicy?: "everyone" | "followers" | "self"; quoteRequestPolicy?: "everyone" | "followers" | "self"; + reactionsCounts?: Record; + repliesCount?: number; + quotesCount?: number; + sharesCount?: number; published?: Date; updated?: Date; replyTargetId?: Uuid; quotedPostId?: Uuid; sharedPostId?: Uuid; + link?: { id: Uuid; url: string }; }, ) { const timestamp = values.published ?? new Date("2026-04-15T00:00:00.000Z"); @@ -281,9 +330,14 @@ export async function insertRemotePost( sharedPostId: values.sharedPostId, replyTargetId: values.replyTargetId, quotedPostId: values.quotedPostId, + linkId: values.link?.id, + linkUrl: values.link?.url, contentHtml: values.contentHtml ?? "

Remote post

", language: values.language ?? "en", - reactionsCounts: {}, + reactionsCounts: values.reactionsCounts ?? {}, + repliesCount: values.repliesCount, + quotesCount: values.quotesCount, + sharesCount: values.sharesCount, published: timestamp, updated, }; @@ -302,6 +356,35 @@ export async function insertMention( await tx.insert(mentionTable).values(values); } +export async function insertPostLink( + tx: Transaction, + values: { url: string; title?: string; creatorId?: Uuid }, +): Promise { + const id = generateUuidV7(); + await tx.insert(postLinkTable).values({ + id, + url: values.url, + title: values.title, + creatorId: values.creatorId, + }); + const link = await tx.query.postLinkTable.findFirst({ where: { id } }); + assert(link != null); + return link; +} + +export async function insertReaction( + tx: Transaction, + values: { postId: Uuid; actorId: Uuid; emoji?: string; created?: Date }, +) { + await tx.insert(reactionTable).values({ + iri: `http://localhost/reactions/${generateUuidV7()}`, + postId: values.postId, + actorId: values.actorId, + emoji: values.emoji ?? "❤️", + created: values.created ?? new Date("2026-04-15T00:00:00.000Z"), + }); +} + export function createTestKv(): TestKv { const store = new Map(); diff --git a/web-next/src/components/AppSidebar.tsx b/web-next/src/components/AppSidebar.tsx index 0938b1311..78cef95a2 100644 --- a/web-next/src/components/AppSidebar.tsx +++ b/web-next/src/components/AppSidebar.tsx @@ -221,6 +221,29 @@ export function AppSidebar(props: AppSidebarProps) { {t`Timeline`} + + + + + + {t`News`} + + + + + + + + {t`News`} + + diff --git a/web-next/src/components/NewsDiscussion.tsx b/web-next/src/components/NewsDiscussion.tsx new file mode 100644 index 000000000..0a669b72c --- /dev/null +++ b/web-next/src/components/NewsDiscussion.tsx @@ -0,0 +1,94 @@ +import { graphql } from "relay-runtime"; +import { For, Match, Show, Switch } from "solid-js"; +import { createPaginationFragment } from "solid-relay"; +import { NewsDiscussionComposer } from "~/components/NewsDiscussionComposer.tsx"; +import { NewsDiscussionThread } from "~/components/NewsDiscussionThread.tsx"; +import { useLingui } from "~/lib/i18n/macro.d.ts"; +import type { NewsDiscussion_story$key } from "./__generated__/NewsDiscussion_story.graphql.ts"; + +export interface NewsDiscussionProps { + $story: NewsDiscussion_story$key; + targetUuid?: string | null; +} + +export function NewsDiscussion(props: NewsDiscussionProps) { + const { t } = useLingui(); + // Shared across the whole tree so each post renders in exactly one place, + // even if it is both a root sharing post and a reply/quote elsewhere. + const rendered = new Set(); + const story = createPaginationFragment( + graphql` + fragment NewsDiscussion_story on PostLink + @refetchable(queryName: "NewsDiscussionQuery") + @argumentDefinitions( + cursor: { type: "String" } + count: { type: "Int", defaultValue: 20 } + ) + { + url + sharingPosts(after: $cursor, first: $count) + @connection(key: "NewsDiscussion__sharingPosts") + { + edges { + node { + id + ...NewsDiscussionThread_post + } + } + pageInfo { + hasNextPage + } + } + } + `, + () => props.$story, + ); + + return ( + + {(data) => ( + <> + story.refetch({}, { fetchPolicy: "network-only" })} + /> +
+ + {(edge) => ( +
+ +
+ )} +
+ + + + +
+ {t`No one has shared this link in a public post yet.`} +
+
+
+ + )} +
+ ); +} diff --git a/web-next/src/components/NewsDiscussionComposer.tsx b/web-next/src/components/NewsDiscussionComposer.tsx new file mode 100644 index 000000000..40909e27e --- /dev/null +++ b/web-next/src/components/NewsDiscussionComposer.tsx @@ -0,0 +1,50 @@ +import { A, useLocation } from "@solidjs/router"; +import { Match, Switch } from "solid-js"; +import { NoteComposer } from "~/components/NoteComposer.tsx"; +import { Button } from "~/components/ui/button.tsx"; +import { useViewer } from "~/contexts/ViewerContext.tsx"; +import { buildSignInHref } from "~/lib/authGate.ts"; +import { useLingui } from "~/lib/i18n/macro.d.ts"; + +export interface NewsDiscussionComposerProps { + /** The discussed link's URL; appended to a posted opinion if absent. */ + url: string; + /** Called after a note is posted, to refresh the discussion roots. */ + onPosted: () => void; +} + +export function NewsDiscussionComposer(props: NewsDiscussionComposerProps) { + const { t } = useLingui(); + const { isAuthenticated, isLoaded } = useViewer(); + const location = useLocation(); + const signInHref = () => + buildSignInHref(location.pathname + location.search + location.hash); + + return ( + + +
+ +

+ {t`The link to this story is added to your post automatically.`} +

+
+
+ +
+

+ {t`Join the discussion about this story.`} +

+ +
+
+
+ ); +} diff --git a/web-next/src/components/NewsDiscussionThread.tsx b/web-next/src/components/NewsDiscussionThread.tsx new file mode 100644 index 000000000..8bc3ef92b --- /dev/null +++ b/web-next/src/components/NewsDiscussionThread.tsx @@ -0,0 +1,446 @@ +import { fetchQuery, graphql } from "relay-runtime"; +import { + createMemo, + createSignal, + For, + getOwner, + onCleanup, + onMount, + runWithOwner, + Show, +} from "solid-js"; +import { createFragment, useRelayEnvironment } from "solid-relay"; +import { ActorHoverCard } from "~/components/ActorHoverCard.tsx"; +import { InternalLink } from "~/components/InternalLink.tsx"; +import { PostAvatar } from "~/components/PostAvatar.tsx"; +import { PostEngagementBar } from "~/components/PostEngagementBar.tsx"; +import { Timestamp } from "~/components/Timestamp.tsx"; +import { useNoteCompose } from "~/contexts/NoteComposeContext.tsx"; +import { useContentLinkInterceptor } from "~/lib/contentLinkInterceptor.ts"; +import { useLingui } from "~/lib/i18n/macro.d.ts"; +import { useMentionHoverCards } from "~/lib/mentionHoverCards.tsx"; +import type { + NewsDiscussionThread_post$data, + NewsDiscussionThread_post$key, +} from "./__generated__/NewsDiscussionThread_post.graphql.ts"; +import type { NewsDiscussionThreadChildrenQuery } from "./__generated__/NewsDiscussionThreadChildrenQuery.graphql.ts"; + +// Auto-expand replies/quotes down to this depth; deeper levels load only when +// the reader asks (so a busy thread does not fetch everything up front). +export const NEWS_DISCUSSION_AUTO_DEPTH = 3; +// Following a deep link auto-expands the target's ancestors past +// NEWS_DISCUSSION_AUTO_DEPTH, but only down to this depth and only this many +// reply pages per node, so a hash link cannot fan out into fetching the entire +// tree at once. +const NEWS_DISCUSSION_TARGET_MAX_DEPTH = 8; +const NEWS_DISCUSSION_TARGET_MAX_PAGES = 5; + +const childrenQuery = graphql` + query NewsDiscussionThreadChildrenQuery( + $id: ID! + $cursor: String + $quoteCursor: String + $loadReplies: Boolean! + $loadQuotes: Boolean! + ) { + node(id: $id) { + ... on Post { + replies(after: $cursor, first: 10) @include(if: $loadReplies) { + edges { node { id ...NewsDiscussionThread_post } } + pageInfo { hasNextPage endCursor } + } + quotes(after: $quoteCursor, first: 20) @include(if: $loadQuotes) { + edges { node { id ...NewsDiscussionThread_post } } + pageInfo { hasNextPage endCursor } + } + } + } + } +`; + +interface Child { + readonly id: string; + readonly key: NewsDiscussionThread_post$key; +} + +type LoadMode = "initial" | "replies" | "quotes"; + +export interface NewsDiscussionThreadProps { + $post: NewsDiscussionThread_post$key; + depth: number; + /** UUID from the URL hash; ancestors of it auto-expand and it is scrolled to. */ + targetUuid?: string | null; + /** Post ids already rendered up the ancestor chain, to break cycles. */ + visited?: ReadonlySet; + /** + * Discussion-wide set of post ids already rendered anywhere in the tree. A + * post can legitimately be both a root sharing post and a reply/quote under + * another post (or a child of two branches); this set ensures each renders in + * exactly one place, so there are no duplicate `post-` anchors. The + * first node to claim an id wins; it releases the claim on unmount. + */ + rendered: Set; +} + +export function NewsDiscussionThread(props: NewsDiscussionThreadProps) { + const { t } = useLingui(); + const { onNoteCreated } = useNoteCompose(); + const environment = useRelayEnvironment(); + const owner = getOwner(); + const post = createFragment( + graphql` + fragment NewsDiscussionThread_post on Post { + id + uuid + content + language + url + iri + published + engagementStats { + replies + quotes + } + actor { + name + handle + username + local + url + iri + ...PostAvatar_actor + } + ...PostEngagementBar_post + } + `, + () => props.$post, + ); + + // Claim this post id for the whole discussion (synchronously, before render): + // a duplicate occurrence elsewhere in the tree renders nothing. Released on + // unmount so a reload/refetch of the same post can re-claim it. + const ownId = post()?.id; + if (ownId != null) { + if (props.rendered.has(ownId)) return null; + props.rendered.add(ownId); + onCleanup(() => props.rendered.delete(ownId)); + } + + const [replyChildren, setReplyChildren] = createSignal([]); + const [quoteChildren, setQuoteChildren] = createSignal([]); + const [expanded, setExpanded] = createSignal(false); + const [loadState, setLoadState] = createSignal< + "idle" | "loading" | "errored" + >("idle"); + const [replyCursor, setReplyCursor] = createSignal(null); + const [replyHasMore, setReplyHasMore] = createSignal(false); + const [quoteCursor, setQuoteCursor] = createSignal(null); + const [quoteHasMore, setQuoteHasMore] = createSignal(false); + // Dedup across pages and against replies that are also quotes of this post. + const seen = new Set(); + // `fetchQuery` is one-shot (next then complete), and the `loadState` guard + // below prevents overlapping loads, so the subscription needs no manual + // unsubscribe (unsubscribing an in-flight request throws `AbortError`). This + // flag just stops late callbacks from touching state after unmount. + let disposed = false; + // Bounds the deep-link auto-pagination per connection; reset on each fresh + // load. + let autoReplyPages = 0; + let autoQuotePages = 0; + // The last load kind, so the error-retry button repeats it. + let lastMode: LoadMode = "initial"; + onCleanup(() => disposed = true); + + const childCount = () => { + const p = post(); + return p == null ? 0 : p.engagementStats.replies + p.engagementStats.quotes; + }; + + function loadChildren(mode: LoadMode = "initial") { + const p = post(); + if (p == null || loadState() === "loading") return; + lastMode = mode; + if (mode === "initial") { + seen.clear(); + autoReplyPages = 0; + autoQuotePages = 0; + setReplyChildren([]); + setQuoteChildren([]); + setReplyCursor(null); + setQuoteCursor(null); + setReplyHasMore(false); + setQuoteHasMore(false); + } + setExpanded(true); + setLoadState("loading"); + // Only fetch the connection this load touches; the other would just be + // refetched and discarded. + fetchQuery( + environment(), + childrenQuery, + { + id: p.id, + cursor: mode === "replies" ? replyCursor() : null, + quoteCursor: mode === "quotes" ? quoteCursor() : null, + loadReplies: mode !== "quotes", + loadQuotes: mode !== "replies", + }, + ).subscribe({ + next(data) { + if (disposed) return; + runWithOwner(owner, () => { + const node = data.node; + const collect = ( + edges: + | ReadonlyArray<{ node: { id: string } & Child["key"] } | null> + | null + | undefined, + ): Child[] => { + const out: Child[] = []; + for (const edge of edges ?? []) { + const n = edge?.node; + if (n == null) continue; + if (n.id === p.id) continue; // never re-render self + if (props.visited?.has(n.id)) continue; // ancestor already shows it + if (seen.has(n.id)) continue; // already loaded under this node + seen.add(n.id); + out.push({ id: n.id, key: n }); + } + return out; + }; + // A reply that also quotes this post is collected as a reply (below, + // first) and deduped out of the quotes, so it renders exactly once. + if (mode !== "quotes") { + const replies = collect(node?.replies?.edges); + setReplyChildren((prev) => + mode === "replies" ? [...prev, ...replies] : replies + ); + setReplyCursor(node?.replies?.pageInfo?.endCursor ?? null); + setReplyHasMore(node?.replies?.pageInfo?.hasNextPage ?? false); + } + if (mode !== "replies") { + const quotes = collect(node?.quotes?.edges); + setQuoteChildren((prev) => + mode === "quotes" ? [...prev, ...quotes] : quotes + ); + setQuoteCursor(node?.quotes?.pageInfo?.endCursor ?? null); + setQuoteHasMore(node?.quotes?.pageInfo?.hasNextPage ?? false); + } + setLoadState("idle"); + maybeAutoPaginate(); + }); + }, + error() { + if (disposed) return; + runWithOwner(owner, () => setLoadState("errored")); + }, + }); + } + + // While following a deep link, keep paginating toward the buried target one + // connection at a time (replies first, then quotes), each capped by page + // count and by depth so the expansion stays bounded. A deep-linked quote + // past the first page is reached this way, not just a deep-linked reply. + function maybeAutoPaginate() { + if ( + props.targetUuid == null || + props.depth >= NEWS_DISCUSSION_TARGET_MAX_DEPTH + ) { + return; + } + if (replyHasMore() && autoReplyPages < NEWS_DISCUSSION_TARGET_MAX_PAGES) { + autoReplyPages++; + loadChildren("replies"); + } else if ( + quoteHasMore() && autoQuotePages < NEWS_DISCUSSION_TARGET_MAX_PAGES + ) { + autoQuotePages++; + loadChildren("quotes"); + } + } + + onMount(() => { + // Refresh this node's loaded children when the viewer composes a reply. + onCleanup(onNoteCreated(() => { + if (expanded()) loadChildren(); + })); + if ( + childCount() > 0 && + (props.depth < NEWS_DISCUSSION_AUTO_DEPTH || + (props.targetUuid != null && + props.depth < NEWS_DISCUSSION_TARGET_MAX_DEPTH)) + ) { + loadChildren(); + } + }); + + const isTarget = createMemo(() => + props.targetUuid != null && post()?.uuid === props.targetUuid + ); + const childVisited = createMemo(() => { + const p = post(); + const set = new Set(props.visited ?? []); + if (p != null) set.add(p.id); + return set; + }); + + let articleRef: HTMLElement | undefined; + onMount(() => { + if (isTarget() && articleRef != null) { + requestAnimationFrame(() => + articleRef?.scrollIntoView({ block: "center" }) + ); + } + }); + + const [proseRef, setProseRef] = createSignal(); + useMentionHoverCards(proseRef); + useContentLinkInterceptor(proseRef); + + const engagementBase = (p: NewsDiscussionThread_post$data) => { + if (!p.actor.local || p.url == null) return null; + try { + return new URL(p.url).pathname; + } catch { + return null; + } + }; + + return ( + + {(p) => ( +
+
0, + }} + > +
+ +
+
+ + + + + + {p.actor.handle} + + + + + +
+
+ +
+
+
+ + 0}> +
+ loadChildren()} + class="block w-full cursor-pointer px-4 py-2 text-left text-sm text-muted-foreground transition-colors hover:text-primary" + > + {t`Show ${childCount()} more in this thread`} + + } + > + + {(child) => ( + + )} + + + + + + {(child) => ( + + )} + + + + + + + + +
+
+
+ )} +
+ ); +} diff --git a/web-next/src/components/NewsList.tsx b/web-next/src/components/NewsList.tsx new file mode 100644 index 000000000..1af1be7f1 --- /dev/null +++ b/web-next/src/components/NewsList.tsx @@ -0,0 +1,157 @@ +import { A } from "@solidjs/router"; +import { graphql } from "relay-runtime"; +import { + createEffect, + For, + Match, + on, + onCleanup, + onMount, + Show, + Switch, +} from "solid-js"; +import { createPaginationFragment } from "solid-relay"; +import { NewsStoryCard } from "~/components/NewsStoryCard.tsx"; +import { useNoteCompose } from "~/contexts/NoteComposeContext.tsx"; +import { useLingui } from "~/lib/i18n/macro.d.ts"; +import type { NewsSort } from "~/lib/useNewsSort.ts"; +import type { NewsList_stories$key } from "./__generated__/NewsList_stories.graphql.ts"; + +export interface NewsListProps { + $stories: NewsList_stories$key; + activeSort: () => NewsSort; + buildHref: (sort: NewsSort) => string; +} + +export function NewsList(props: NewsListProps) { + const { t } = useLingui(); + const { onNoteCreated } = useNoteCompose(); + const stories = createPaginationFragment( + graphql` + fragment NewsList_stories on Query + @refetchable(queryName: "NewsListQuery") + @argumentDefinitions( + cursor: { type: "String" } + count: { type: "Int", defaultValue: 25 } + order: { type: "NewsOrder", defaultValue: POPULAR } + ) + { + __id + viewer { + moderator + } + newsStories(after: $cursor, first: $count, order: $order) + @connection(key: "NewsList__newsStories") + { + __id + edges { + __id + cursor + node { + ...NewsStoryCard_story + } + } + pageInfo { + hasNextPage + } + } + } + `, + () => props.$stories, + ); + + // Refetch at the fragment level when the sort pill changes after mount, so + // the subtree stays mounted (no flash). The top-level query carries the + // initial sort for SSR. + createEffect(on( + () => props.activeSort(), + (order) => { + stories.refetch({ order }); + }, + { defer: true }, + )); + + onMount(() => { + // Stale-while-revalidate, and refresh when the viewer shares a link (the + // new share re-scores it, so it should surface immediately). + stories.refetch({ order: props.activeSort() }); + onCleanup(onNoteCreated(() => { + stories.refetch({ order: props.activeSort() }); + })); + }); + + function onLoadMore() { + stories.loadNext(25); + } + + const sortPills: { value: NewsSort; label: () => string }[] = [ + { value: "POPULAR", label: () => t`Popular` }, + { value: "NEWEST", label: () => t`Newest` }, + { value: "ALL_TIME", label: () => t`All-time` }, + ]; + + const pillClass = (active: boolean) => + [ + "rounded-full border px-3 py-1.5 text-sm transition-colors", + active + ? "border-primary bg-primary text-primary-foreground" + : "border-input text-muted-foreground hover:bg-accent hover:text-accent-foreground", + ].join(" "); + + return ( + <> +
+ + {(pill) => ( + + {pill.label()} + + )} + +
+
+ + {(data) => ( + <> + + {(edge) => ( + + stories.refetch( + { order: props.activeSort() }, + { fetchPolicy: "network-only" }, + )} + /> + )} + + + + + +
+ {t`No shared links yet. Once links start circulating across the fediverse, they will appear here.`} +
+
+ + )} +
+
+ + ); +} diff --git a/web-next/src/components/NewsStoryCard.tsx b/web-next/src/components/NewsStoryCard.tsx new file mode 100644 index 000000000..424aeb31d --- /dev/null +++ b/web-next/src/components/NewsStoryCard.tsx @@ -0,0 +1,296 @@ +import { A } from "@solidjs/router"; +import { graphql } from "relay-runtime"; +import { createMemo, Show } from "solid-js"; +import { createFragment, createMutation } from "solid-relay"; +import { Timestamp } from "~/components/Timestamp.tsx"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu.tsx"; +import { showToast } from "~/components/ui/toast.tsx"; +import { useNoteCompose } from "~/contexts/NoteComposeContext.tsx"; +import { msg, plural, useLingui } from "~/lib/i18n/macro.d.ts"; +import type { NewsStoryCard_story$key } from "./__generated__/NewsStoryCard_story.graphql.ts"; +import type { NewsStoryCard_setPenalty_Mutation } from "./__generated__/NewsStoryCard_setPenalty_Mutation.graphql.ts"; + +const setPenaltyMutation = graphql` + mutation NewsStoryCard_setPenalty_Mutation( + $id: UUID! + $penalty: NewsPenalty! + ) { + setNewsScorePenalty(id: $id, penalty: $penalty) { + __typename + ... on PostLink { + id + penalty + score + } + ... on NotAuthenticatedError { + notAuthenticated + } + ... on NotAuthorizedError { + notAuthorized + } + } + } +`; + +export interface NewsStoryCardProps { + $story: NewsStoryCard_story$key; + /** Whether the viewer is a moderator (shows the demote/bury control). */ + isModerator?: boolean; + /** Called after a penalty change, to refetch the feed so it reorders. */ + onPenaltyChanged?: () => void; +} + +export function NewsStoryCard(props: NewsStoryCardProps) { + const { t, i18n } = useLingui(); + const { openWithContent } = useNoteCompose(); + const story = createFragment( + graphql` + fragment NewsStoryCard_story on PostLink { + uuid + url + title + siteName + description + discussionCount + latestActivityAt + penalty + image { + url + alt + width + height + } + } + `, + () => props.$story, + ); + + const [setPenalty] = createMutation( + setPenaltyMutation, + ); + + function applyPenalty( + uuid: `${string}-${string}-${string}-${string}-${string}`, + penalty: "NONE" | "DEMOTE" | "BURY", + ) { + setPenalty({ + variables: { id: uuid, penalty }, + onCompleted(response) { + if (response.setNewsScorePenalty?.__typename === "PostLink") { + props.onPenaltyChanged?.(); + } else { + showToast({ title: t`Failed to update penalty.`, variant: "error" }); + } + }, + onError() { + showToast({ title: t`Failed to update penalty.`, variant: "error" }); + }, + }); + } + + const host = createMemo(() => { + const s = story(); + if (s == null) return ""; + try { + return new URL(s.url).host.replace(/^www\./, ""); + } catch { + return s.url; + } + }); + + const opinionsText = (count: number) => + i18n._(msg`${plural(count, { one: "# opinion", other: "# opinions" })}`); + + return ( + + {(s) => ( +
+ {/* Discussion count, doubling as the link into the conversation. */} + + + + {s.discussionCount} + + + +
+

+ + {s.title || host()} + + +

+

+ {host()} + + · {s.siteName} + +

+ +

+ {s.description} +

+
+
+ + + {(at) => ( + <> + + + {t`Last active`}{" "} + + + + )} + + + + + + {s.penalty === "BURY" ? t`Buried` : t`Demoted`} + + + + + ) => ( + + )} + /> + + applyPenalty(s.uuid, "DEMOTE")} + > + {t`Demote`} + + applyPenalty(s.uuid, "BURY")} + > + {t`Bury`} + + applyPenalty(s.uuid, "NONE")} + > + {t`Clear penalty`} + + + + +
+
+ + + {(img) => ( + + )} + +
+ )} +
+ ); +} diff --git a/web-next/src/components/NewsStoryHeader.tsx b/web-next/src/components/NewsStoryHeader.tsx new file mode 100644 index 000000000..9edc63f53 --- /dev/null +++ b/web-next/src/components/NewsStoryHeader.tsx @@ -0,0 +1,139 @@ +import { graphql } from "relay-runtime"; +import { createMemo, Show } from "solid-js"; +import { createFragment } from "solid-relay"; +import { Button } from "~/components/ui/button.tsx"; +import { Timestamp } from "~/components/Timestamp.tsx"; +import { useNoteCompose } from "~/contexts/NoteComposeContext.tsx"; +import { msg, plural, useLingui } from "~/lib/i18n/macro.d.ts"; +import type { NewsStoryHeader_story$key } from "./__generated__/NewsStoryHeader_story.graphql.ts"; + +export interface NewsStoryHeaderProps { + $story: NewsStoryHeader_story$key; +} + +export function NewsStoryHeader(props: NewsStoryHeaderProps) { + const { t, i18n } = useLingui(); + const { openWithContent } = useNoteCompose(); + const story = createFragment( + graphql` + fragment NewsStoryHeader_story on PostLink { + url + title + siteName + description + discussionCount + firstSharedAt + latestActivityAt + image { + url + alt + width + height + } + sourceBreakdown { + local + remote + bluesky + } + } + `, + () => props.$story, + ); + + const host = createMemo(() => { + const s = story(); + if (s == null) return ""; + try { + return new URL(s.url).host.replace(/^www\./, ""); + } catch { + return s.url; + } + }); + + const opinionsText = (count: number) => + i18n._(msg`${plural(count, { one: "# opinion", other: "# opinions" })}`); + + return ( + + {(s) => ( +
+ + + {(img) => ( +
+ {img.alt +
+ )} +
+
+

+ {host()} + + · {s.siteName} + +

+

+ {s.title || host()} +

+ + {(description) => ( +

+ {description} +

+ )} +
+
+
+
+ + {opinionsText(s.discussionCount)} + + + {(at) => ( + + {t`Last active`} + + )} + + + {(at) => ( + + {t`First shared`} + + )} + + 0}> + {t`${s.sourceBreakdown.local} from Hackers' Pub`} + + 0}> + {t`${s.sourceBreakdown.remote} from the fediverse`} + + 0}> + {t`${s.sourceBreakdown.bluesky} from Bluesky`} + + +
+
+ )} +
+ ); +} diff --git a/web-next/src/components/NoteComposeModal.tsx b/web-next/src/components/NoteComposeModal.tsx index 9ea653553..7976e2b01 100644 --- a/web-next/src/components/NoteComposeModal.tsx +++ b/web-next/src/components/NoteComposeModal.tsx @@ -29,6 +29,7 @@ export function NoteComposeModal() { replyDefaultVisibility, editingNoteId, editInitialData, + initialContent, close, clearQuote, notifyNoteCreated, @@ -108,7 +109,7 @@ export function NoteComposeModal() { defaultVisibility={replyDefaultVisibility() ?? undefined} placeholder={replyTargetId() ? t`Write a reply…` : undefined} editingNoteId={editingNoteId()} - initialContent={editInitialData()?.content} + initialContent={editInitialData()?.content ?? initialContent()} initialLanguage={editInitialData()?.language} initialQuotePolicy={editInitialData()?.quotePolicy} editingVisibility={editInitialData()?.visibility} diff --git a/web-next/src/components/NoteComposer.tsx b/web-next/src/components/NoteComposer.tsx index 7c425ad7d..9f743090a 100644 --- a/web-next/src/components/NoteComposer.tsx +++ b/web-next/src/components/NoteComposer.tsx @@ -11,6 +11,7 @@ import { Show, } from "solid-js"; import { createMutation, useRelayEnvironment } from "solid-relay"; +import { ensureLinkInContent } from "~/lib/composerLink.ts"; import { detectLanguage } from "~/lib/langdet.ts"; import { UploadAbortedError, @@ -254,6 +255,10 @@ export interface NoteComposerProps { replyTargetId?: string | null; defaultVisibility?: PostVisibility | null; showReplyTarget?: boolean; + // When set (new notes only), the URL is appended to the bottom of the + // submitted content unless the author already included it, so the note links + // to (and joins the discussion of) this URL. + ensureLinkUrl?: string | null; // Edit mode: when set, the composer updates an existing note instead of // creating a new one. editingNoteId?: string | null; @@ -266,11 +271,11 @@ export interface NoteComposerProps { export function NoteComposer(props: NoteComposerProps) { const { t, i18n } = useLingui(); const environment = useRelayEnvironment(); - // In edit mode, initialize signals directly from props so the form is - // pre-filled on the first render (avoids an async createEffect lag). - const initialEditContent = props.editingNoteId - ? (props.initialContent ?? "") - : ""; + // Initialize content directly from props so a deliberate pre-fill — an edit's + // body, or a "share this link" URL passed via `initialContent` — is present + // on the first render (avoids an async createEffect lag). Empty for a plain + // compose / reply / quote, where `initialContent` is null. + const initialEditContent = props.initialContent ?? ""; const [content, setContent] = createSignal(initialEditContent); const [visibility, setVisibility] = createSignal( props.defaultVisibility ?? "PUBLIC", @@ -496,8 +501,9 @@ export function NoteComposer(props: NoteComposerProps) { if (!id) { setReplyTargetPost(null); setReplyTargetFetchError(false); - // In edit mode, keep the existing content; don't clear the pre-fill. - if (!props.editingNoteId) { + // Keep a deliberate pre-fill (an edit body, or a "share this link" URL + // via `initialContent`); only clear an auto-filled reply mention. + if (!props.editingNoteId && !props.initialContent) { if (content() === prefillRef) setContent(""); prefillRef = ""; } @@ -862,10 +868,15 @@ export function NoteComposer(props: NoteComposerProps) { }, }); } else { + // Append the discussed link to the bottom unless the author already + // included it, so an inline opinion joins this link's discussion. + const finalContent = props.ensureLinkUrl + ? ensureLinkInContent(noteContent, props.ensureLinkUrl) + : noteContent; createNote({ variables: { input: { - content: noteContent, + content: finalContent, language: language()?.baseName ?? i18n.locale, visibility: visibility(), quotePolicy: effectiveQuotePolicy(), diff --git a/web-next/src/contexts/NoteComposeContext.tsx b/web-next/src/contexts/NoteComposeContext.tsx index 96568fb58..c70e22af8 100644 --- a/web-next/src/contexts/NoteComposeContext.tsx +++ b/web-next/src/contexts/NoteComposeContext.tsx @@ -23,7 +23,9 @@ interface NoteComposeContextValue { replyDefaultVisibility: () => PostVisibility | null; editingNoteId: () => string | null; editInitialData: () => NoteEditInitialData | null; + initialContent: () => string | null; open: () => void; + openWithContent: (content: string) => void; openWithQuote: (quotedPostId: string) => void; openWithReply: ( replyTargetId: string, @@ -49,6 +51,7 @@ export const NoteComposeProvider: ParentComponent = (props) => { const [editInitialData, setEditInitialData] = createSignal< NoteEditInitialData | null >(null); + const [initialContent, setInitialContent] = createSignal(null); const [callbacks, setCallbacks] = createSignal>( new Set(), ); @@ -59,6 +62,16 @@ export const NoteComposeProvider: ParentComponent = (props) => { setReplyDefaultVisibility(null); setEditingNoteId(null); setEditInitialData(null); + setInitialContent(null); + setIsOpen(true); + }; + const openWithContent = (content: string) => { + setQuotedPostId(null); + setReplyTargetId(null); + setReplyDefaultVisibility(null); + setEditingNoteId(null); + setEditInitialData(null); + setInitialContent(content); setIsOpen(true); }; const openWithQuote = (quotedPostId: string) => { @@ -67,6 +80,7 @@ export const NoteComposeProvider: ParentComponent = (props) => { setReplyDefaultVisibility(null); setEditingNoteId(null); setEditInitialData(null); + setInitialContent(null); setIsOpen(true); }; const openWithReply = ( @@ -78,6 +92,7 @@ export const NoteComposeProvider: ParentComponent = (props) => { setReplyDefaultVisibility(defaultVisibility); setEditingNoteId(null); setEditInitialData(null); + setInitialContent(null); setIsOpen(true); }; const openForEdit = (noteId: string, data: NoteEditInitialData) => { @@ -86,6 +101,7 @@ export const NoteComposeProvider: ParentComponent = (props) => { setReplyDefaultVisibility(null); setEditingNoteId(noteId); setEditInitialData(data); + setInitialContent(null); setIsOpen(true); }; const close = () => { @@ -94,6 +110,7 @@ export const NoteComposeProvider: ParentComponent = (props) => { setReplyDefaultVisibility(null); setEditingNoteId(null); setEditInitialData(null); + setInitialContent(null); setIsOpen(false); }; const clearQuote = () => { @@ -125,7 +142,9 @@ export const NoteComposeProvider: ParentComponent = (props) => { replyDefaultVisibility, editingNoteId, editInitialData, + initialContent, open, + openWithContent, openWithQuote, openWithReply, openForEdit, diff --git a/web-next/src/lib/composerLink.test.ts b/web-next/src/lib/composerLink.test.ts new file mode 100644 index 000000000..a7d926ec8 --- /dev/null +++ b/web-next/src/lib/composerLink.test.ts @@ -0,0 +1,29 @@ +import { assertEquals } from "@std/assert"; +import { ensureLinkInContent } from "./composerLink.ts"; + +const URL = "https://example.com/story"; + +Deno.test("ensureLinkInContent appends the URL on a new paragraph when absent", () => { + assertEquals( + ensureLinkInContent("My take on this.", URL), + `My take on this.\n\n${URL}`, + ); +}); + +Deno.test("ensureLinkInContent leaves content unchanged when the URL is already present", () => { + const bare = `Already linked ${URL} here.`; + assertEquals(ensureLinkInContent(bare, URL), bare); +}); + +Deno.test("ensureLinkInContent does not duplicate a URL inside a markdown link", () => { + const md = `See [the story](${URL}) for details.`; + assertEquals(ensureLinkInContent(md, URL), md); +}); + +Deno.test("ensureLinkInContent returns just the URL for empty/whitespace content", () => { + assertEquals(ensureLinkInContent(" ", URL), URL); +}); + +Deno.test("ensureLinkInContent trims surrounding whitespace before appending", () => { + assertEquals(ensureLinkInContent(" spaced ", URL), `spaced\n\n${URL}`); +}); diff --git a/web-next/src/lib/composerLink.ts b/web-next/src/lib/composerLink.ts new file mode 100644 index 000000000..9754bacd9 --- /dev/null +++ b/web-next/src/lib/composerLink.ts @@ -0,0 +1,21 @@ +/** + * Append `url` on its own paragraph at the end of `content`, unless the content + * already contains that URL. + * + * Used by the inline News discussion composer so an opinion gets associated with + * the link being discussed, without duplicating a URL the author already wrote + * or pasted. + * + * Caveat: the server derives a post's `linkId` from the *first* external link in + * the rendered content, so appending at the bottom only joins this link's + * discussion when it ends up first, i.e. the author wrote no other link. A + * plain-text opinion (the common case) works; an opinion that leads with a + * different link attaches to that link instead. The presence check is a plain + * substring match, matching the requested behavior ("don't duplicate a URL the + * author already included"). + */ +export function ensureLinkInContent(content: string, url: string): string { + const trimmed = content.trim(); + if (trimmed.length < 1) return url; + return trimmed.includes(url) ? trimmed : `${trimmed}\n\n${url}`; +} diff --git a/web-next/src/lib/useNewsSort.ts b/web-next/src/lib/useNewsSort.ts new file mode 100644 index 000000000..73968a125 --- /dev/null +++ b/web-next/src/lib/useNewsSort.ts @@ -0,0 +1,43 @@ +import { useLocation, useSearchParams } from "@solidjs/router"; + +export type NewsSort = "POPULAR" | "NEWEST" | "ALL_TIME"; + +const SLUG_TO_SORT: Record = { + popular: "POPULAR", + newest: "NEWEST", + "all-time": "ALL_TIME", +}; + +const SORT_TO_SLUG: Record = { + POPULAR: "popular", + NEWEST: "newest", + ALL_TIME: "all-time", +}; + +function parseSort(value: string | undefined): NewsSort { + return (value != null && SLUG_TO_SORT[value]) || "POPULAR"; +} + +/** + * Encapsulates the `?sort=` search-param handling for the news feed (mirrors + * `useLanguageFilter`). `initialSort` is captured non-reactively so the + * initial preloaded query reference stays stable across sort-pill clicks; + * subsequent changes are handled by a fragment-level refetch inside `NewsList`. + */ +export function useNewsSort(basePath: string) { + const location = useLocation(); + const [searchParams] = useSearchParams<{ sort?: string }>(); + + const initialSort = parseSort(searchParams.sort); + const activeSort = () => parseSort(searchParams.sort); + + const buildHref = (sort: NewsSort) => { + const p = new URLSearchParams(location.search); + if (sort === "POPULAR") p.delete("sort"); + else p.set("sort", SORT_TO_SLUG[sort]); + const qs = p.toString(); + return basePath + (qs ? "?" + qs : ""); + }; + + return { activeSort, initialSort, buildHref }; +} diff --git a/web-next/src/locales/en-US/messages.po b/web-next/src/locales/en-US/messages.po index 532d6d25d..35242334f 100644 --- a/web-next/src/locales/en-US/messages.po +++ b/web-next/src/locales/en-US/messages.po @@ -14,7 +14,7 @@ msgstr "" "Plural-Forms: \n" #. placeholder {0}: article.replies?.edges.length ?? 0 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:991 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:992 msgid "{0, plural, one {# comment} other {# comments}}" msgstr "{0, plural, one {# comment} other {# comments}}" @@ -44,11 +44,16 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, one {# following} other {# following}}" #. placeholder {0}: link.invitationsLeft -#: src/routes/(root)/[handle]/invite/[id].tsx:321 -#: src/routes/(root)/[handle]/settings/invite.tsx:742 +#: src/routes/(root)/[handle]/invite/[id].tsx:326 +#: src/routes/(root)/[handle]/settings/invite.tsx:743 msgid "{0, plural, one {# invitation left} other {# invitations left}}" msgstr "{0, plural, one {# invitation left} other {# invitations left}}" +#. placeholder {0}: status()?.scoredLinkCount ?? 0 +#: src/routes/(root)/admin/news.tsx:350 +msgid "{0, plural, one {# link is currently in the news feed.} other {# links are currently in the news feed.}}" +msgstr "{0, plural, one {# link is currently in the news feed.} other {# links are currently in the news feed.}}" + #. placeholder {0}: count() #: src/routes/(root)/admin/media.tsx:172 msgid "{0, plural, one {# orphan medium can be deleted.} other {# orphan media can be deleted.}}" @@ -77,7 +82,7 @@ msgid "{0, plural, one {# voter} other {# voters}}" msgstr "{0, plural, one {# voter} other {# voters}}" #. placeholder {0}: edge.node.tags.length - 3 -#: src/routes/(root)/[handle]/drafts/index.tsx:293 +#: src/routes/(root)/[handle]/drafts/index.tsx:295 msgid "{0, plural, one {+1 more} other {+# more}}" msgstr "{0, plural, one {+1 more} other {+# more}}" @@ -87,7 +92,7 @@ msgid "{0, plural, one {Deleted # orphan medium.} other {Deleted # orphan media. msgstr "{0, plural, one {Deleted # orphan medium.} other {Deleted # orphan media.}}" #. placeholder {0}: account.invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:311 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" @@ -96,13 +101,18 @@ msgstr "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up msgid "{0, plural, one {Load # more reactor} other {Load # more reactors}}" msgstr "{0, plural, one {Load # more reactor} other {Load # more reactors}}" +#. placeholder {0}: result.linksUpdated! +#: src/routes/(root)/admin/news.tsx:190 +msgid "{0, plural, one {Recomputed # link.} other {Recomputed # links.}}" +msgstr "{0, plural, one {Recomputed # link.} other {Recomputed # links.}}" + #. placeholder {0}: result.accountsAffected! #: src/routes/(root)/admin/invitations.tsx:97 msgid "{0, plural, one {Regenerated invitations for # account.} other {Regenerated invitations for # accounts.}}" msgstr "{0, plural, one {Regenerated invitations for # account.} other {Regenerated invitations for # accounts.}}" #. placeholder {0}: account.inviteesCount.totalCount -#: src/routes/(root)/[handle]/settings/invite.tsx:438 +#: src/routes/(root)/[handle]/settings/invite.tsx:439 msgid "{0, plural, one {You have invited total # person so far.} other {You have invited total # people so far.}}" msgstr "{0, plural, one {You have invited total # person so far.} other {You have invited total # people so far.}}" @@ -172,8 +182,23 @@ msgstr "{0} and {1} others updated a post you shared" msgid "{0} followed you" msgstr "{0} followed you" +#. placeholder {0}: s.sourceBreakdown.bluesky +#: src/components/NewsStoryHeader.tsx:124 +msgid "{0} from Bluesky" +msgstr "{0} from Bluesky" + +#. placeholder {0}: s.sourceBreakdown.local +#: src/components/NewsStoryHeader.tsx:118 +msgid "{0} from Hackers' Pub" +msgstr "{0} from Hackers' Pub" + +#. placeholder {0}: s.sourceBreakdown.remote +#: src/components/NewsStoryHeader.tsx:121 +msgid "{0} from the fediverse" +msgstr "{0} from the fediverse" + #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:361 +#: src/routes/(root)/[handle]/settings/invite.tsx:362 msgid "{0} is already a member of Hackers' Pub." msgstr "{0} is already a member of Hackers' Pub." @@ -229,47 +254,57 @@ msgstr "{0} updated a post you shared" #. placeholder {0}: post.actor.rawName ?? post.actor.username #. placeholder {1}: post.excerpt #. placeholder {1}: title() -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:237 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:283 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:238 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:284 msgid "{0}: {1}" msgstr "{0}: {1}" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/articles.tsx:86 -#: src/routes/(root)/[handle]/(profile)/articles.tsx:90 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:87 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:91 msgid "{0}'s articles" msgstr "{0}'s articles" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/followers.tsx:77 -#: src/routes/(root)/[handle]/(profile)/followers.tsx:80 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:78 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:81 msgid "{0}'s followers" msgstr "{0}'s followers" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/following.tsx:77 -#: src/routes/(root)/[handle]/(profile)/following.tsx:80 +#: src/routes/(root)/[handle]/(profile)/following.tsx:78 +#: src/routes/(root)/[handle]/(profile)/following.tsx:81 msgid "{0}'s following" msgstr "{0}'s following" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/notes.tsx:86 -#: src/routes/(root)/[handle]/(profile)/notes.tsx:90 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:87 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:91 msgid "{0}'s notes" msgstr "{0}'s notes" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/shares.tsx:86 -#: src/routes/(root)/[handle]/(profile)/shares.tsx:90 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:87 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:91 msgid "{0}'s shares" msgstr "{0}'s shares" +#: src/components/NewsStoryCard.tsx:107 +#: src/components/NewsStoryHeader.tsx:54 +msgid "{count, plural, one {# opinion} other {# opinions}}" +msgstr "{count, plural, one {# opinion} other {# opinions}}" + +#: src/components/NewsStoryCard.tsx:51 +#: src/components/NewsStoryHeader.tsx:53 +#~ msgid "{count, plural, one {# share} other {# shares}}" +#~ msgstr "{count, plural, one {# share} other {# shares}}" + #: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:231 #: src/routes/(root)/[handle]/[noteId]/reactions.tsx:253 #~ msgid "+{0} more reactor(s) not shown" #~ msgstr "+{0} more reactor(s) not shown" -#: src/routes/(root)/[handle]/settings/index.tsx:579 +#: src/routes/(root)/[handle]/settings/index.tsx:580 msgid "A name for the link that will be displayed on your profile, e.g., GitHub." msgstr "A name for the link that will be displayed on your profile, e.g., GitHub." @@ -278,11 +313,11 @@ msgid "A sign-in link has been sent to your email. Please check your inbox (or s msgstr "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." #: src/components/admin/AdminAccountsTable.tsx:224 -#: src/components/AppSidebar.tsx:479 +#: src/components/AppSidebar.tsx:502 msgid "Account" msgstr "Account" -#: src/components/AppSidebar.tsx:750 +#: src/components/AppSidebar.tsx:773 #: src/routes/(root)/admin/index.tsx:95 msgid "Accounts" msgstr "Accounts" @@ -292,7 +327,8 @@ msgstr "Accounts" msgid "Actions" msgstr "Actions" -#: src/routes/(root)/[handle]/settings/language.tsx:195 +#: src/routes/(root)/[handle]/settings/language.tsx:196 +#: src/routes/(root)/admin/news.tsx:421 msgid "Add" msgstr "Add" @@ -305,19 +341,23 @@ msgstr "Add {0}" msgid "Add to sidebar" msgstr "Add to sidebar" -#: src/components/AppSidebar.tsx:728 +#: src/routes/(root)/admin/news.tsx:421 +msgid "Adding…" +msgstr "Adding…" + +#: src/components/AppSidebar.tsx:751 msgid "Admin" msgstr "Admin" -#: src/routes/(root)/[handle]/bookmarks.tsx:135 +#: src/routes/(root)/[handle]/bookmarks.tsx:136 msgid "All" msgstr "All" -#: src/components/NoteComposer.tsx:801 +#: src/components/NoteComposer.tsx:807 msgid "All images must finish uploading before posting" msgstr "All images must finish uploading before posting" -#: src/components/NoteComposer.tsx:809 +#: src/components/NoteComposer.tsx:815 msgid "All images require alt text" msgstr "All images require alt text" @@ -329,17 +369,21 @@ msgstr "All languages" msgid "All notifications" msgstr "All notifications" +#: src/components/NewsList.tsx:90 +msgid "All-time" +msgstr "All-time" + #: src/components/article-composer/ArticleComposerPublishFields.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:506 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:507 msgid "Allow automatic translation by AI" msgstr "Allow automatic translation by AI" #. placeholder {0}: index() + 1 -#: src/components/NoteComposer.tsx:1287 +#: src/components/NoteComposer.tsx:1298 msgid "Alt text for image {0}" msgstr "Alt text for image {0}" -#: src/components/NoteComposer.tsx:1299 +#: src/components/NoteComposer.tsx:1310 msgid "Alt text for visually impaired people (required)" msgstr "Alt text for visually impaired people (required)" @@ -347,31 +391,31 @@ msgstr "Alt text for visually impaired people (required)" msgid "An error occurred during signup. Please try again." msgstr "An error occurred during signup. Please try again." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:280 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:281 msgid "An error occurred while registering your passkey." msgstr "An error occurred while registering your passkey." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:328 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:329 msgid "An error occurred while revoking your passkey." msgstr "An error occurred while revoking your passkey." -#: src/routes/(root)/[handle]/settings/preferences.tsx:143 +#: src/routes/(root)/[handle]/settings/preferences.tsx:144 msgid "An error occurred while saving your preferences. Please try again, or contact support if the problem persists." msgstr "An error occurred while saving your preferences. Please try again, or contact support if the problem persists." -#: src/routes/(root)/[handle]/settings/language.tsx:162 +#: src/routes/(root)/[handle]/settings/language.tsx:163 msgid "An error occurred while saving your preferred languages. Please try again, or contact support if the problem persists." msgstr "An error occurred while saving your preferred languages. Please try again, or contact support if the problem persists." -#: src/routes/(root)/[handle]/settings/index.tsx:296 -#: src/routes/(root)/[handle]/settings/index.tsx:337 +#: src/routes/(root)/[handle]/settings/index.tsx:297 +#: src/routes/(root)/[handle]/settings/index.tsx:338 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "An error occurred while saving your settings. Please try again, or contact support if the problem persists." -#: src/routes/(root)/[handle]/invite/[id].tsx:198 -#: src/routes/(root)/[handle]/settings/invite.tsx:270 -#: src/routes/(root)/[handle]/settings/invite.tsx:551 -#: src/routes/(root)/[handle]/settings/invite.tsx:601 +#: src/routes/(root)/[handle]/invite/[id].tsx:203 +#: src/routes/(root)/[handle]/settings/invite.tsx:271 +#: src/routes/(root)/[handle]/settings/invite.tsx:552 +#: src/routes/(root)/[handle]/settings/invite.tsx:602 msgid "An unexpected error occurred. Please try again later." msgstr "An unexpected error occurred. Please try again later." @@ -386,7 +430,7 @@ msgstr "Anyone can quote" msgid "Are you sure you want to block {0} ({1})? They won't be able to follow you or see your posts." msgstr "Are you sure you want to block {0} ({1})? They won't be able to follow you or see your posts." -#: src/routes/(root)/[handle]/drafts/index.tsx:156 +#: src/routes/(root)/[handle]/drafts/index.tsx:157 msgid "Are you sure you want to delete \"{draftTitle}\"? This action cannot be undone." msgstr "Are you sure you want to delete \"{draftTitle}\"? This action cannot be undone." @@ -395,7 +439,7 @@ msgid "Are you sure you want to delete this draft? This action cannot be undone. msgstr "Are you sure you want to delete this draft? This action cannot be undone." #. placeholder {0}: passkeyToRevoke()?.name -#: src/routes/(root)/[handle]/settings/passkeys.tsx:517 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:518 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." msgstr "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." @@ -405,8 +449,8 @@ msgstr "Are you sure you want to revoke passkey {0}? You won't be able to use it msgid "Are you sure you want to unblock {0} ({1})? They will be able to follow you and see your posts." msgstr "Are you sure you want to unblock {0} ({1})? They will be able to follow you and see your posts." -#: src/routes/(root)/[handle]/drafts/index.tsx:243 #: src/routes/(root)/[handle]/drafts/index.tsx:245 +#: src/routes/(root)/[handle]/drafts/index.tsx:247 msgid "Article drafts" msgstr "Article drafts" @@ -414,7 +458,7 @@ msgstr "Article drafts" msgid "Article published" msgstr "Article published" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:333 msgid "Article updated" msgstr "Article updated" @@ -423,21 +467,21 @@ msgid "article-url-slug" msgstr "article-url-slug" #: src/components/ProfileTabs.tsx:51 -#: src/routes/(root)/[handle]/bookmarks.tsx:136 +#: src/routes/(root)/[handle]/bookmarks.tsx:137 msgid "Articles" msgstr "Articles" -#: src/components/AppSidebar.tsx:308 +#: src/components/AppSidebar.tsx:331 msgid "Articles only" msgstr "Articles only" #. placeholder {0}: "CHANGED" -#: src/routes/(root)/[handle]/settings/index.tsx:454 +#: src/routes/(root)/[handle]/settings/index.tsx:455 msgid "As you have already changed it {0}, you can't change it again." msgstr "As you have already changed it {0}, you can't change it again." -#: src/components/NoteComposer.tsx:1173 -#: src/components/NoteComposer.tsx:1174 +#: src/components/NoteComposer.tsx:1184 +#: src/components/NoteComposer.tsx:1185 msgid "Attach image" msgstr "Attach image" @@ -445,20 +489,20 @@ msgstr "Attach image" msgid "Authenticating…" msgstr "Authenticating…" -#: src/components/NoteComposer.tsx:1318 +#: src/components/NoteComposer.tsx:1329 msgid "Auto-fill" msgstr "Auto-fill" -#: src/components/NoteComposer.tsx:1311 -#: src/components/NoteComposer.tsx:1312 +#: src/components/NoteComposer.tsx:1322 +#: src/components/NoteComposer.tsx:1323 msgid "Auto-fill alt text" msgstr "Auto-fill alt text" -#: src/routes/(root)/[handle]/settings/index.tsx:351 +#: src/routes/(root)/[handle]/settings/index.tsx:352 msgid "Avatar" msgstr "Avatar" -#: src/routes/(root)/[handle]/settings/index.tsx:483 +#: src/routes/(root)/[handle]/settings/index.tsx:484 #: src/routes/(root)/sign/up/[token].tsx:403 msgid "Bio" msgstr "Bio" @@ -477,11 +521,11 @@ msgstr "Block" msgid "Block user?" msgstr "Block user?" -#: src/routes/(root)/[handle]/settings/blocks.tsx:98 +#: src/routes/(root)/[handle]/settings/blocks.tsx:99 msgid "Blocked accounts" msgstr "Blocked accounts" -#: src/routes/(root)/[handle]/settings/blocks.tsx:100 +#: src/routes/(root)/[handle]/settings/blocks.tsx:101 msgid "Blocked accounts cannot follow you or see your posts. Unlike muting, blocking is federated to the blocked account's instance." msgstr "Blocked accounts cannot follow you or see your posts. Unlike muting, blocking is federated to the blocked account's instance." @@ -494,8 +538,8 @@ msgstr "Bold" msgid "Bookmark" msgstr "Bookmark" -#: src/components/AppSidebar.tsx:618 -#: src/routes/(root)/[handle]/bookmarks.tsx:125 +#: src/components/AppSidebar.tsx:641 +#: src/routes/(root)/[handle]/bookmarks.tsx:126 msgid "Bookmarks" msgstr "Bookmarks" @@ -524,24 +568,33 @@ msgstr "Browser notifications disabled" msgid "Browser notifications enabled" msgstr "Browser notifications enabled" +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:492 +msgid "Buried" +msgstr "Buried" + +#: src/components/NewsStoryCard.tsx:258 +msgid "Bury" +msgstr "Bury" + #: src/components/article-composer/ArticleComposerActions.tsx:48 -#: src/components/NoteComposer.tsx:1346 -#: src/components/NoteComposer.tsx:1347 -#: src/components/NoteComposer.tsx:1390 +#: src/components/NoteComposer.tsx:1357 +#: src/components/NoteComposer.tsx:1358 +#: src/components/NoteComposer.tsx:1401 #: src/components/PostActionMenu.tsx:367 #: src/components/ProfileActionMenu.tsx:401 #: src/components/ProfileActionMenu.tsx:402 #: src/components/QuotedNoteCard.tsx:248 #: src/components/RemoteFollowButton.tsx:218 #: src/components/RemoteFollowButton.tsx:280 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:522 -#: src/routes/(root)/[handle]/settings/index.tsx:398 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:521 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:399 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:522 #: src/routes/(root)/authorize_interaction.tsx:273 msgid "Cancel" msgstr "Cancel" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:347 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 msgid "Cannot change the language because translations already exist" msgstr "Cannot change the language because translations already exist" @@ -549,11 +602,11 @@ msgstr "Cannot change the language because translations already exist" msgid "Check again" msgstr "Check again" -#: src/routes/(root)/[handle]/invite/[id].tsx:244 +#: src/routes/(root)/[handle]/invite/[id].tsx:249 msgid "Check your email" msgstr "Check your email" -#: src/routes/(root)/[handle]/invite/[id].tsx:246 +#: src/routes/(root)/[handle]/invite/[id].tsx:251 msgid "Check your email to complete sign-up. We've sent a verification link to your email address." msgstr "Check your email to complete sign-up. We've sent a verification link to your email address." @@ -561,7 +614,7 @@ msgstr "Check your email to complete sign-up. We've sent a verification link to msgid "Checking…" msgstr "Checking…" -#: src/routes/(root)/[handle]/settings/invite.tsx:386 +#: src/routes/(root)/[handle]/settings/invite.tsx:387 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "Choose the language your friend prefers. This language will only be used for the invitation." @@ -569,20 +622,25 @@ msgstr "Choose the language your friend prefers. This language will only be used msgid "Choose whether push notifications may include post excerpts. Generic notification text is used when previews are hidden." msgstr "Choose whether push notifications may include post excerpts. Generic notification text is used when previews are hidden." -#: src/routes/(root)/[handle]/invite/[id].tsx:395 +#: src/routes/(root)/[handle]/invite/[id].tsx:400 msgid "Choose your preferred language for the verification email." msgstr "Choose your preferred language for the verification email." #: src/components/admin/AdminAccountsTable.tsx:213 +#: src/routes/(root)/admin/news.tsx:501 msgid "Clear" msgstr "Clear" +#: src/components/NewsStoryCard.tsx:264 +msgid "Clear penalty" +msgstr "Clear penalty" + #: src/components/WebPushNotificationSettings.tsx:395 msgid "Clicking a notification opens your notifications page." msgstr "Clicking a notification opens your notifications page." #: src/components/ImageLightbox.tsx:74 -#: src/routes/(root)/[handle]/settings/invite.tsx:663 +#: src/routes/(root)/[handle]/settings/invite.tsx:664 msgid "Close" msgstr "Close" @@ -594,7 +652,7 @@ msgstr "Closed" msgid "Code" msgstr "Code" -#: src/components/AppSidebar.tsx:977 +#: src/components/AppSidebar.tsx:1023 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/sign/up/[token].tsx:458 msgid "Code of conduct" @@ -604,18 +662,18 @@ msgstr "Code of conduct" #~ msgid "Comments ({0})" #~ msgstr "Comments ({0})" -#: src/components/AppSidebar.tsx:819 +#: src/components/AppSidebar.tsx:865 #: src/components/FloatingComposeButton.tsx:61 msgid "Compose" msgstr "Compose" #: src/components/article-composer/ArticleComposerForm.tsx:65 -#: src/components/NoteComposer.tsx:1122 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:392 +#: src/components/NoteComposer.tsx:1133 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:393 msgid "Content" msgstr "Content" -#: src/components/NoteComposer.tsx:791 +#: src/components/NoteComposer.tsx:797 msgid "Content cannot be empty" msgstr "Content cannot be empty" @@ -627,15 +685,15 @@ msgstr "Continue in browser" msgid "Controls who can quote this article on their timeline." msgstr "Controls who can quote this article on their timeline." -#: src/routes/(root)/[handle]/settings/invite.tsx:611 +#: src/routes/(root)/[handle]/settings/invite.tsx:612 msgid "Copied" msgstr "Copied" -#: src/routes/(root)/[handle]/settings/invite.tsx:705 +#: src/routes/(root)/[handle]/settings/invite.tsx:706 msgid "Copy" msgstr "Copy" -#: src/routes/(root)/[handle]/settings/invite.tsx:618 +#: src/routes/(root)/[handle]/settings/invite.tsx:619 msgid "Could not copy the link to the clipboard." msgstr "Could not copy the link to the clipboard." @@ -655,23 +713,23 @@ msgstr "Could not revoke quote" msgid "Could not vote on this poll" msgstr "Could not vote on this poll" -#: src/components/AppSidebar.tsx:865 +#: src/components/AppSidebar.tsx:911 #: src/components/FloatingComposeButton.tsx:123 msgid "Create article" msgstr "Create article" -#: src/routes/(root)/[handle]/settings/invite.tsx:834 +#: src/routes/(root)/[handle]/settings/invite.tsx:835 msgid "Create invitation link" msgstr "Create invitation link" -#: src/components/AppSidebar.tsx:841 +#: src/components/AppSidebar.tsx:887 #: src/components/FloatingComposeButton.tsx:99 -#: src/components/NoteComposeModal.tsx:72 -#: src/components/NoteComposer.tsx:1407 +#: src/components/NoteComposeModal.tsx:73 +#: src/components/NoteComposer.tsx:1418 msgid "Create note" msgstr "Create note" -#: src/routes/(root)/[handle]/settings/invite.tsx:628 +#: src/routes/(root)/[handle]/settings/invite.tsx:629 msgid "Create shareable invitation links. Each link can be used multiple times until the invitation count runs out or the link expires." msgstr "Create shareable invitation links. Each link can be used multiple times until the invitation count runs out or the link expires." @@ -680,7 +738,7 @@ msgid "Created" msgstr "Created" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:426 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:427 msgid "Created {0}" msgstr "Created {0}" @@ -688,16 +746,16 @@ msgstr "Created {0}" msgid "Creating account…" msgstr "Creating account…" -#: src/components/NoteComposer.tsx:1408 -#: src/routes/(root)/[handle]/settings/invite.tsx:833 +#: src/components/NoteComposer.tsx:1419 +#: src/routes/(root)/[handle]/settings/invite.tsx:834 msgid "Creating…" msgstr "Creating…" -#: src/routes/(root)/[handle]/settings/index.tsx:405 +#: src/routes/(root)/[handle]/settings/index.tsx:406 msgid "Crop" msgstr "Crop" -#: src/routes/(root)/[handle]/settings/index.tsx:378 +#: src/routes/(root)/[handle]/settings/index.tsx:379 msgid "Crop your new avatar" msgstr "Crop your new avatar" @@ -711,22 +769,22 @@ msgstr "Cutoff:" msgid "CW" msgstr "CW" -#: src/routes/(root)/[handle]/settings/preferences.tsx:192 +#: src/routes/(root)/[handle]/settings/preferences.tsx:193 msgid "Default note privacy" msgstr "Default note privacy" -#: src/routes/(root)/[handle]/settings/preferences.tsx:217 +#: src/routes/(root)/[handle]/settings/preferences.tsx:218 msgid "Default quote permission" msgstr "Default quote permission" -#: src/routes/(root)/[handle]/settings/preferences.tsx:204 +#: src/routes/(root)/[handle]/settings/preferences.tsx:205 msgid "Default share privacy" msgstr "Default share privacy" #: src/components/PostActionMenu.tsx:353 #: src/components/PostActionMenu.tsx:373 -#: src/routes/(root)/[handle]/drafts/index.tsx:320 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/drafts/index.tsx:322 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 msgid "Delete" msgstr "Delete" @@ -744,11 +802,20 @@ msgid "Delete post?" msgstr "Delete post?" #: src/components/article-composer/ArticleComposerActions.tsx:21 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 #: src/routes/(root)/admin/media.tsx:187 msgid "Deleting…" msgstr "Deleting…" +#: src/components/NewsStoryCard.tsx:252 +msgid "Demote" +msgstr "Demote" + +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:493 +msgid "Demoted" +msgstr "Demoted" + #: src/components/WebPushNotificationSettings.tsx:431 msgid "Disable" msgstr "Disable" @@ -757,11 +824,11 @@ msgstr "Disable" msgid "Disabling…" msgstr "Disabling…" -#: src/components/NoteComposeModal.tsx:137 +#: src/components/NoteComposeModal.tsx:138 msgid "Discard" msgstr "Discard" -#: src/components/NoteComposeModal.tsx:126 +#: src/components/NoteComposeModal.tsx:127 msgid "Discard draft?" msgstr "Discard draft?" @@ -769,11 +836,15 @@ msgstr "Discard draft?" msgid "Discard unsaved changes - are you sure?" msgstr "Discard unsaved changes - are you sure?" +#: src/components/NewsStoryCard.tsx:145 +#~ msgid "Discussion" +#~ msgstr "Discussion" + #: src/components/WebPushPromptBanner.tsx:269 msgid "Dismiss" msgstr "Dismiss" -#: src/routes/(root)/[handle]/settings/index.tsx:467 +#: src/routes/(root)/[handle]/settings/index.tsx:468 #: src/routes/(root)/sign/up/[token].tsx:377 msgid "Display name" msgstr "Display name" @@ -782,12 +853,12 @@ msgstr "Display name" msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." -#: src/components/NoteComposer.tsx:742 +#: src/components/NoteComposer.tsx:748 msgid "Do you want to quote this link?" msgstr "Do you want to quote this link?" #: src/components/article-composer/ArticleComposerContext.tsx:450 -#: src/routes/(root)/[handle]/drafts/index.tsx:175 +#: src/routes/(root)/[handle]/drafts/index.tsx:176 msgid "Draft deleted" msgstr "Draft deleted" @@ -803,7 +874,7 @@ msgstr "Draft not found" msgid "Draft saved" msgstr "Draft saved" -#: src/routes/(root)/[handle]/settings/index.tsx:381 +#: src/routes/(root)/[handle]/settings/index.tsx:382 msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "Drag to select the area you want to keep, then click “Crop” to update your avatar." @@ -812,25 +883,25 @@ msgid "e.g., @user@mastodon.social" msgstr "e.g., @user@mastodon.social" #: src/components/PostActionMenu.tsx:328 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:695 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:696 msgid "Edit" msgstr "Edit" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:374 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:375 msgid "Edit article" msgstr "Edit article" -#: src/routes/(root)/[handle]/drafts/[id].tsx:73 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/[id].tsx:75 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "Edit draft" msgstr "Edit draft" -#: src/components/NoteComposeModal.tsx:69 +#: src/components/NoteComposeModal.tsx:70 msgid "Edit note" msgstr "Edit note" -#: src/routes/(root)/[handle]/invite/[id].tsx:366 -#: src/routes/(root)/[handle]/settings/invite.tsx:334 +#: src/routes/(root)/[handle]/invite/[id].tsx:371 +#: src/routes/(root)/[handle]/settings/invite.tsx:335 #: src/routes/(root)/sign/up/[token].tsx:311 msgid "Email address" msgstr "Email address" @@ -861,7 +932,7 @@ msgstr "Ends" #~ msgid "Enter article title..." #~ msgstr "Enter article title..." -#: src/routes/(root)/[handle]/invite/[id].tsx:279 +#: src/routes/(root)/[handle]/invite/[id].tsx:284 msgid "Enter your email address below to get started." msgstr "Enter your email address below to get started." @@ -883,47 +954,63 @@ msgstr "Enter your email or username below to sign in." #: src/components/article-composer/ArticleComposerContext.tsx:466 #: src/components/article-composer/ArticleComposerContext.tsx:474 #: src/components/article-composer/ArticleComposerForm.tsx:35 -#: src/components/NoteComposer.tsx:601 -#: src/components/NoteComposer.tsx:648 -#: src/components/NoteComposer.tsx:790 -#: src/components/NoteComposer.tsx:800 -#: src/components/NoteComposer.tsx:808 -#: src/components/NoteComposer.tsx:842 -#: src/components/NoteComposer.tsx:850 -#: src/components/NoteComposer.tsx:858 -#: src/components/NoteComposer.tsx:892 -#: src/components/NoteComposer.tsx:900 -#: src/components/NoteComposer.tsx:908 -#: src/components/NoteComposer.tsx:965 +#: src/components/NoteComposer.tsx:607 +#: src/components/NoteComposer.tsx:654 +#: src/components/NoteComposer.tsx:796 +#: src/components/NoteComposer.tsx:806 +#: src/components/NoteComposer.tsx:814 +#: src/components/NoteComposer.tsx:848 +#: src/components/NoteComposer.tsx:856 +#: src/components/NoteComposer.tsx:864 +#: src/components/NoteComposer.tsx:903 +#: src/components/NoteComposer.tsx:911 +#: src/components/NoteComposer.tsx:919 +#: src/components/NoteComposer.tsx:976 #: src/components/QuotedNoteCard.tsx:270 #: src/components/QuotedNoteCard.tsx:278 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:257 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:345 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:355 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:364 -#: src/routes/(root)/[handle]/drafts/index.tsx:182 -#: src/routes/(root)/[handle]/drafts/index.tsx:191 -#: src/routes/(root)/[handle]/drafts/index.tsx:199 -#: src/routes/(root)/[handle]/invite/[id].tsx:196 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:258 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:346 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/drafts/index.tsx:183 +#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:200 +#: src/routes/(root)/[handle]/invite/[id].tsx:201 #: src/routes/(root)/sign/up/[token].tsx:269 msgid "Error" msgstr "Error" +#: src/routes/(root)/admin/news.tsx:382 +msgid "Excluded URL patterns" +msgstr "Excluded URL patterns" + +#: src/routes/(root)/admin/news.tsx:233 +msgid "Exclusion pattern added." +msgstr "Exclusion pattern added." + +#: src/routes/(root)/admin/news.tsx:268 +msgid "Exclusion pattern removed." +msgstr "Exclusion pattern removed." + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/invite/[id].tsx:334 -#: src/routes/(root)/[handle]/settings/invite.tsx:759 +#: src/routes/(root)/[handle]/invite/[id].tsx:339 +#: src/routes/(root)/[handle]/settings/invite.tsx:760 msgid "Expires {0}" msgstr "Expires {0}" -#: src/routes/(root)/[handle]/settings/invite.tsx:806 +#: src/routes/(root)/[handle]/settings/invite.tsx:807 msgid "Expiry" msgstr "Expiry" -#: src/routes/(root)/[handle]/settings/invite.tsx:391 -#: src/routes/(root)/[handle]/settings/invite.tsx:796 +#: src/routes/(root)/[handle]/settings/invite.tsx:392 +#: src/routes/(root)/[handle]/settings/invite.tsx:797 msgid "Extra message" msgstr "Extra message" +#: src/routes/(root)/admin/news.tsx:252 +msgid "Failed to add exclusion pattern." +msgstr "Failed to add exclusion pattern." + #: src/components/HashtagActionBar.tsx:192 #: src/components/HashtagActionBar.tsx:199 msgid "Failed to add to sidebar" @@ -939,17 +1026,21 @@ msgstr "Failed to block this user" msgid "Failed to bookmark" msgstr "Failed to bookmark" -#: src/routes/(root)/[handle]/settings/invite.tsx:617 +#: src/routes/(root)/admin/news.tsx:310 +msgid "Failed to clear penalty." +msgstr "Failed to clear penalty." + +#: src/routes/(root)/[handle]/settings/invite.tsx:618 msgid "Failed to copy" msgstr "Failed to copy" -#: src/routes/(root)/[handle]/settings/invite.tsx:539 -#: src/routes/(root)/[handle]/settings/invite.tsx:549 +#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:550 msgid "Failed to create invitation link" msgstr "Failed to create invitation link" -#: src/routes/(root)/[handle]/settings/invite.tsx:588 -#: src/routes/(root)/[handle]/settings/invite.tsx:599 +#: src/routes/(root)/[handle]/settings/invite.tsx:589 +#: src/routes/(root)/[handle]/settings/invite.tsx:600 msgid "Failed to delete invitation link" msgstr "Failed to delete invitation link" @@ -983,7 +1074,7 @@ msgstr "Failed to enable browser notifications" msgid "Failed to follow" msgstr "Failed to follow" -#: src/components/NoteComposer.tsx:966 +#: src/components/NoteComposer.tsx:977 msgid "Failed to generate alt text" msgstr "Failed to generate alt text" @@ -999,7 +1090,7 @@ msgstr "Failed to load more articles; click to retry" msgid "Failed to load more bookmarks; click to retry" msgstr "Failed to load more bookmarks; click to retry" -#: src/routes/(root)/[handle]/drafts/index.tsx:345 +#: src/routes/(root)/[handle]/drafts/index.tsx:347 msgid "Failed to load more drafts; click to retry" msgstr "Failed to load more drafts; click to retry" @@ -1011,7 +1102,7 @@ msgstr "Failed to load more followers; click to retry" msgid "Failed to load more following; click to retry" msgstr "Failed to load more following; click to retry" -#: src/routes/(root)/[handle]/settings/invite.tsx:947 +#: src/routes/(root)/[handle]/settings/invite.tsx:948 msgid "Failed to load more invitees; click to retry" msgstr "Failed to load more invitees; click to retry" @@ -1023,7 +1114,7 @@ msgstr "Failed to load more notes; click to retry" msgid "Failed to load more notifications; click to retry" msgstr "Failed to load more notifications; click to retry" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:495 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 msgid "Failed to load more passkeys; click to retry" msgstr "Failed to load more passkeys; click to retry" @@ -1035,8 +1126,8 @@ msgstr "Failed to load more passkeys; click to retry" msgid "Failed to load more posts; click to retry" msgstr "Failed to load more posts; click to retry" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:194 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:201 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:195 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:202 msgid "Failed to load more quotes; click to retry" msgstr "Failed to load more quotes; click to retry" @@ -1044,28 +1135,31 @@ msgstr "Failed to load more quotes; click to retry" msgid "Failed to load more reactors; click to retry" msgstr "Failed to load more reactors; click to retry" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:670 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:671 msgid "Failed to load more replies; click to retry" msgstr "Failed to load more replies; click to retry" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:204 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:213 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:205 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:214 msgid "Failed to load more shares; click to retry" msgstr "Failed to load more shares; click to retry" -#: src/components/BlockedAccountsList.tsx:199 -#: src/components/MutedAccountsList.tsx:196 +#: src/components/AccountListBase.tsx:112 msgid "Failed to load more; click to retry" msgstr "Failed to load more; click to retry" -#: src/components/NoteComposer.tsx:1014 +#: src/components/NoteComposer.tsx:1025 msgid "Failed to load post" msgstr "Failed to load post" -#: src/components/NoteComposer.tsx:1065 +#: src/components/NoteComposer.tsx:1076 msgid "Failed to load quoted post" msgstr "Failed to load quoted post" +#: src/components/NewsDiscussionThread.tsx:408 +msgid "Failed to load replies; click to retry" +msgstr "Failed to load replies; click to retry" + #: src/components/RemoteFollowButton.tsx:126 msgid "Failed to look up user." msgstr "Failed to look up user." @@ -1091,11 +1185,15 @@ msgstr "Failed to pin post" msgid "Failed to react" msgstr "Failed to react" +#: src/routes/(root)/admin/news.tsx:212 +msgid "Failed to recompute news scores." +msgstr "Failed to recompute news scores." + #: src/routes/(root)/admin/invitations.tsx:125 msgid "Failed to regenerate invitations." msgstr "Failed to regenerate invitations." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:277 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:278 msgid "Failed to register passkey" msgstr "Failed to register passkey" @@ -1104,40 +1202,44 @@ msgstr "Failed to register passkey" msgid "Failed to remove bookmark" msgstr "Failed to remove bookmark" +#: src/routes/(root)/admin/news.tsx:281 +msgid "Failed to remove exclusion pattern." +msgstr "Failed to remove exclusion pattern." + #: src/components/HashtagActionBar.tsx:212 #: src/components/HashtagActionBar.tsx:219 msgid "Failed to remove from sidebar" msgstr "Failed to remove from sidebar" #: src/components/MarkdownEditor.tsx:192 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:461 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 msgid "Failed to render preview" msgstr "Failed to render preview" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:319 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:325 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:320 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "Failed to revoke passkey" msgstr "Failed to revoke passkey" -#: src/routes/(root)/[handle]/settings/language.tsx:160 +#: src/routes/(root)/[handle]/settings/language.tsx:161 msgid "Failed to save language preferences" msgstr "Failed to save language preferences" -#: src/routes/(root)/[handle]/settings/preferences.tsx:141 +#: src/routes/(root)/[handle]/settings/preferences.tsx:142 msgid "Failed to save preferences" msgstr "Failed to save preferences" -#: src/routes/(root)/[handle]/settings/index.tsx:293 -#: src/routes/(root)/[handle]/settings/index.tsx:335 +#: src/routes/(root)/[handle]/settings/index.tsx:294 +#: src/routes/(root)/[handle]/settings/index.tsx:336 msgid "Failed to save settings" msgstr "Failed to save settings" -#: src/routes/(root)/[handle]/invite/[id].tsx:185 +#: src/routes/(root)/[handle]/invite/[id].tsx:190 msgid "Failed to send email" msgstr "Failed to send email" -#: src/routes/(root)/[handle]/settings/invite.tsx:248 -#: src/routes/(root)/[handle]/settings/invite.tsx:268 +#: src/routes/(root)/[handle]/settings/invite.tsx:249 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "Failed to send invitation" msgstr "Failed to send invitation" @@ -1151,8 +1253,8 @@ msgstr "Failed to share post" msgid "Failed to sign out: {0}" msgstr "Failed to sign out: {0}" -#: src/components/BlockedAccountsList.tsx:98 -#: src/components/BlockedAccountsList.tsx:104 +#: src/components/BlockedAccountsList.tsx:96 +#: src/components/BlockedAccountsList.tsx:102 #: src/components/ProfileActionMenu.tsx:258 #: src/components/ProfileActionMenu.tsx:264 msgid "Failed to unblock this user" @@ -1164,8 +1266,8 @@ msgstr "Failed to unblock this user" msgid "Failed to unfollow" msgstr "Failed to unfollow" -#: src/components/MutedAccountsList.tsx:97 -#: src/components/MutedAccountsList.tsx:101 +#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:99 #: src/components/ProfileActionMenu.tsx:300 #: src/components/ProfileActionMenu.tsx:308 msgid "Failed to unmute this user" @@ -1181,11 +1283,16 @@ msgstr "Failed to unpin post" msgid "Failed to unshare post" msgstr "Failed to unshare post" +#: src/components/NewsStoryCard.tsx:87 +#: src/components/NewsStoryCard.tsx:91 +msgid "Failed to update penalty." +msgstr "Failed to update penalty." + #: src/components/WebPushNotificationSettings.tsx:354 msgid "Failed to update push notification privacy" msgstr "Failed to update push notification privacy" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:366 msgid "Failed to update the article. Please try again." msgstr "Failed to update the article. Please try again." @@ -1194,8 +1301,8 @@ msgstr "Failed to update the article. Please try again." #~ msgstr "Failed to update the note. Please try again." #: src/components/article-composer/ArticleComposerForm.tsx:38 -#: src/components/NoteComposer.tsx:651 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:260 +#: src/components/NoteComposer.tsx:657 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:261 msgid "Failed to upload image" msgstr "Failed to upload image" @@ -1203,7 +1310,7 @@ msgstr "Failed to upload image" msgid "Failed to vote" msgstr "Failed to vote" -#: src/components/AppSidebar.tsx:354 +#: src/components/AppSidebar.tsx:377 msgid "Fediverse" msgstr "Fediverse" @@ -1211,10 +1318,14 @@ msgstr "Fediverse" msgid "Fediverse handle" msgstr "Fediverse handle" -#: src/components/AppSidebar.tsx:245 +#: src/components/AppSidebar.tsx:268 msgid "Feed" msgstr "Feed" +#: src/components/NewsStoryHeader.tsx:113 +msgid "First shared" +msgstr "First shared" + #: src/components/FollowButton.tsx:195 #: src/components/HashtagActionBar.tsx:237 msgid "Follow" @@ -1257,7 +1368,7 @@ msgstr "Following you" msgid "Formatting" msgstr "Formatting" -#: src/components/NoteComposer.tsx:1337 +#: src/components/NoteComposer.tsx:1348 msgid "Generating…" msgstr "Generating…" @@ -1265,14 +1376,14 @@ msgstr "Generating…" msgid "Get browser notifications" msgstr "Get browser notifications" -#: src/components/AppSidebar.tsx:1015 +#: src/components/AppSidebar.tsx:1061 msgid "GitHub repository" msgstr "GitHub repository" -#: src/routes/(root)/[handle]/bookmarks.tsx:108 -#: src/routes/(root)/[handle]/drafts/[id].tsx:59 -#: src/routes/(root)/[handle]/drafts/index.tsx:225 -#: src/routes/(root)/[handle]/drafts/new.tsx:69 +#: src/routes/(root)/[handle]/bookmarks.tsx:109 +#: src/routes/(root)/[handle]/drafts/[id].tsx:61 +#: src/routes/(root)/[handle]/drafts/index.tsx:227 +#: src/routes/(root)/[handle]/drafts/new.tsx:71 msgid "Go back" msgstr "Go back" @@ -1284,13 +1395,13 @@ msgstr "Go home" msgid "Go to Drafts" msgstr "Go to Drafts" -#: src/routes/(root)/[handle]/bookmarks.tsx:114 +#: src/routes/(root)/[handle]/bookmarks.tsx:115 msgid "Go to my bookmarks" msgstr "Go to my bookmarks" -#: src/routes/(root)/[handle]/drafts/[id].tsx:64 -#: src/routes/(root)/[handle]/drafts/index.tsx:230 -#: src/routes/(root)/[handle]/drafts/new.tsx:74 +#: src/routes/(root)/[handle]/drafts/[id].tsx:66 +#: src/routes/(root)/[handle]/drafts/index.tsx:232 +#: src/routes/(root)/[handle]/drafts/new.tsx:76 msgid "Go to my drafts" msgstr "Go to my drafts" @@ -1298,8 +1409,8 @@ msgstr "Go to my drafts" msgid "Grants one extra invitation to the most active accounts (the top third by post count) since the last regeneration cutoff." msgstr "Grants one extra invitation to the most active accounts (the top third by post count) since the last regeneration cutoff." -#: src/components/AppSidebar.tsx:323 -#: src/components/AppSidebar.tsx:446 +#: src/components/AppSidebar.tsx:346 +#: src/components/AppSidebar.tsx:469 #: src/routes/(root).tsx:134 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/markdown.tsx:40 @@ -1328,6 +1439,19 @@ msgstr "Hackers' Pub: Admin · Invitations" msgid "Hackers' Pub: Admin · Media" msgstr "Hackers' Pub: Admin · Media" +#: src/routes/(root)/admin/news.tsx:320 +msgid "Hackers' Pub: Admin · News" +msgstr "Hackers' Pub: Admin · News" + +#: src/routes/(root)/admin/news.tsx:124 +#~ msgid "Hackers' Pub: Admin · News scores" +#~ msgstr "Hackers' Pub: Admin · News scores" + +#: src/routes/(root)/news/[link_id]/index.tsx:56 +#: src/routes/(root)/news/index.tsx:37 +msgid "Hackers' Pub: News" +msgstr "Hackers' Pub: News" + #: src/routes/(root)/notifications.tsx:47 msgid "Hackers' Pub: Notifications" msgstr "Hackers' Pub: Notifications" @@ -1350,6 +1474,10 @@ msgstr "Heading 3" msgid "Hide" msgstr "Hide" +#: src/routes/(root)/admin/news.tsx:384 +msgid "Hide links matching a URL pattern from the news feed list (every sort order). Their discussion pages stay reachable by direct link. Patterns use the URLPattern syntax, e.g. https://example.com/* or https://*.example.com/*." +msgstr "Hide links matching a URL pattern from the news feed list (every sort order). Their discussion pages stay reachable by direct link. Patterns use the URLPattern syntax, e.g. https://example.com/* or https://*.example.com/*." + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Hide preview" #~ msgstr "Hide preview" @@ -1362,23 +1490,23 @@ msgstr "Hide" msgid "I have read and agree to the Code of conduct." msgstr "I have read and agree to the Code of conduct." -#: src/routes/(root)/[handle]/settings/preferences.tsx:186 +#: src/routes/(root)/[handle]/settings/preferences.tsx:187 msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1013 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1014 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:548 msgid "If you have a fediverse account, you can reply to this article from your own instance. Search {0} on your instance and reply to it." msgstr "If you have a fediverse account, you can reply to this article from your own instance. Search {0} on your instance and reply to it." #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:393 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:394 msgid "If you have a fediverse account, you can reply to this note from your own instance. Search {0} on your instance and reply to it." msgstr "If you have a fediverse account, you can reply to this note from your own instance. Search {0} on your instance and reply to it." #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:471 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:472 msgid "If you have a fediverse account, you can reply to this post from your own instance. Search {0} on your instance and reply to it." msgstr "If you have a fediverse account, you can reply to this post from your own instance. Search {0} on your instance and reply to it." @@ -1401,42 +1529,42 @@ msgstr "Invalid Fediverse handle format." #: src/components/article-composer/ArticleComposerContext.tsx:321 #: src/components/article-composer/ArticleComposerContext.tsx:394 #: src/components/article-composer/ArticleComposerContext.tsx:459 -#: src/components/NoteComposer.tsx:843 -#: src/components/NoteComposer.tsx:893 -#: src/routes/(root)/[handle]/drafts/index.tsx:184 +#: src/components/NoteComposer.tsx:849 +#: src/components/NoteComposer.tsx:904 +#: src/routes/(root)/[handle]/drafts/index.tsx:185 msgid "Invalid input: {0}" msgstr "Invalid input: {0}" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:349 msgid "Invalid input: {inputPath}" msgstr "Invalid input: {inputPath}" #. placeholder {0}: link.inviter.name ?? link.inviter.username -#: src/routes/(root)/[handle]/invite/[id].tsx:237 +#: src/routes/(root)/[handle]/invite/[id].tsx:242 msgid "Invitation from {0}" msgstr "Invitation from {0}" -#: src/routes/(root)/[handle]/settings/invite.tsx:379 +#: src/routes/(root)/[handle]/settings/invite.tsx:380 msgid "Invitation language" msgstr "Invitation language" -#: src/routes/(root)/[handle]/settings/invite.tsx:531 +#: src/routes/(root)/[handle]/settings/invite.tsx:532 msgid "Invitation link created" msgstr "Invitation link created" -#: src/routes/(root)/[handle]/settings/invite.tsx:582 +#: src/routes/(root)/[handle]/settings/invite.tsx:583 msgid "Invitation link deleted" msgstr "Invitation link deleted" -#: src/routes/(root)/[handle]/settings/invite.tsx:626 +#: src/routes/(root)/[handle]/settings/invite.tsx:627 msgid "Invitation links" msgstr "Invitation links" -#: src/routes/(root)/[handle]/settings/invite.tsx:258 +#: src/routes/(root)/[handle]/settings/invite.tsx:259 msgid "Invitation sent" msgstr "Invitation sent" -#: src/components/AppSidebar.tsx:773 +#: src/components/AppSidebar.tsx:796 #: src/routes/(root)/admin/invitations.tsx:148 msgid "Invitations" msgstr "Invitations" @@ -1445,13 +1573,13 @@ msgstr "Invitations" msgid "Invitations left" msgstr "Invitations left" -#: src/components/AppSidebar.tsx:642 +#: src/components/AppSidebar.tsx:665 #: src/components/SettingsTabs.tsx:69 -#: src/routes/(root)/[handle]/settings/invite.tsx:295 +#: src/routes/(root)/[handle]/settings/invite.tsx:296 msgid "Invite" msgstr "Invite" -#: src/routes/(root)/[handle]/settings/invite.tsx:304 +#: src/routes/(root)/[handle]/settings/invite.tsx:305 msgid "Invite a friend" msgstr "Invite a friend" @@ -1467,23 +1595,27 @@ msgstr "Invited by" msgid "Italic" msgstr "Italic" -#: src/routes/(root)/[handle]/settings/index.tsx:474 +#: src/routes/(root)/[handle]/settings/index.tsx:475 msgid "John Doe" msgstr "John Doe" +#: src/components/NewsDiscussionComposer.tsx:41 +msgid "Join the discussion about this story." +msgstr "Join the discussion about this story." + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/settings/invite.tsx:921 +#: src/routes/(root)/[handle]/settings/invite.tsx:922 msgid "Joined on {0}" msgstr "Joined on {0}" -#: src/components/NoteComposeModal.tsx:132 +#: src/components/NoteComposeModal.tsx:133 msgid "Keep editing" msgstr "Keep editing" #: src/components/article-composer/ArticleComposerPublishFields.tsx:53 #: src/components/LanguageList.tsx:33 #: src/components/LanguageSelect.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:489 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:490 msgid "Language" msgstr "Language" @@ -1491,7 +1623,7 @@ msgstr "Language" msgid "Language code" msgstr "Language code" -#: src/routes/(root)/[handle]/settings/language.tsx:82 +#: src/routes/(root)/[handle]/settings/language.tsx:83 msgid "Language settings" msgstr "Language settings" @@ -1499,16 +1631,25 @@ msgstr "Language settings" msgid "Languages" msgstr "Languages" +#: src/components/NewsStoryCard.tsx:205 +#: src/components/NewsStoryHeader.tsx:106 +msgid "Last active" +msgstr "Last active" + #: src/components/admin/AdminAccountsTable.tsx:278 msgid "Last activity" msgstr "Last activity" +#: src/routes/(root)/admin/news.tsx:360 +msgid "Last recomputed:" +msgstr "Last recomputed:" + #: src/routes/(root)/admin/invitations.tsx:160 msgid "Last regenerated:" msgstr "Last regenerated:" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:450 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:451 msgid "Last used {0}" msgstr "Last used {0}" @@ -1524,21 +1665,28 @@ msgstr "Link author:" #~ msgid "Link author: " #~ msgstr "Link author: " -#: src/routes/(root)/[handle]/invite/[id].tsx:255 +#: src/routes/(root)/[handle]/invite/[id].tsx:260 msgid "Link expired" msgstr "Link expired" -#: src/routes/(root)/[handle]/settings/index.tsx:560 +#: src/routes/(root)/[handle]/settings/index.tsx:561 msgid "Link name" msgstr "Link name" +#: src/routes/(root)/admin/news.tsx:465 +msgid "Links a moderator has demoted in the popular feed. Clear a penalty to restore a link's normal ranking." +msgstr "Links a moderator has demoted in the popular feed. Clear a penalty to restore a link's normal ranking." + +#: src/routes/(root)/news/index.tsx:41 +msgid "Links circulating across the fediverse, ranked by how much they are being shared and discussed." +msgstr "Links circulating across the fediverse, ranked by how much they are being shared and discussed." + #: src/components/ui/markdown-editor.tsx:291 msgid "List" msgstr "List" -#: src/components/BlockedAccountsList.tsx:202 -#: src/components/MutedAccountsList.tsx:199 -#: src/routes/(root)/[handle]/drafts/index.tsx:348 +#: src/components/AccountListBase.tsx:115 +#: src/routes/(root)/[handle]/drafts/index.tsx:350 msgid "Load more" msgstr "Load more" @@ -1562,7 +1710,7 @@ msgstr "Load more followers" msgid "Load more following" msgstr "Load more following" -#: src/routes/(root)/[handle]/settings/invite.tsx:950 +#: src/routes/(root)/[handle]/settings/invite.tsx:951 msgid "Load more invitees" msgstr "Load more invitees" @@ -1574,7 +1722,7 @@ msgstr "Load more notes" msgid "Load more notifications" msgstr "Load more notifications" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:497 msgid "Load more passkeys" msgstr "Load more passkeys" @@ -1586,8 +1734,9 @@ msgstr "Load more passkeys" msgid "Load more posts" msgstr "Load more posts" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:197 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:204 +#: src/components/NewsDiscussionThread.tsx:378 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:205 msgid "Load more quotes" msgstr "Load more quotes" @@ -1595,15 +1744,20 @@ msgstr "Load more quotes" #~ msgid "Load more reactors (+{0})" #~ msgstr "Load more reactors (+{0})" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:673 +#: src/components/NewsDiscussionThread.tsx:399 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:674 msgid "Load more replies" msgstr "Load more replies" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:207 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:216 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:208 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:217 msgid "Load more shares" msgstr "Load more shares" +#: src/components/NewsList.tsx:139 +msgid "Load more stories" +msgstr "Load more stories" + #: src/components/article-composer/ArticleComposer.tsx:31 msgid "Loading draft…" msgstr "Loading draft…" @@ -1628,7 +1782,7 @@ msgstr "Loading more followers…" msgid "Loading more following…" msgstr "Loading more following…" -#: src/routes/(root)/[handle]/settings/invite.tsx:944 +#: src/routes/(root)/[handle]/settings/invite.tsx:945 msgid "Loading more invitees…" msgstr "Loading more invitees…" @@ -1640,7 +1794,7 @@ msgstr "Loading more notes…" msgid "Loading more notifications" msgstr "Loading more notifications" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:493 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:494 msgid "Loading more passkeys…" msgstr "Loading more passkeys…" @@ -1652,8 +1806,8 @@ msgstr "Loading more passkeys…" msgid "Loading more posts…" msgstr "Loading more posts…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:191 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:192 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:199 msgid "Loading more quotes…" msgstr "Loading more quotes…" @@ -1661,21 +1815,28 @@ msgstr "Loading more quotes…" msgid "Loading more reactors…" msgstr "Loading more reactors…" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:667 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:668 msgid "Loading more replies…" msgstr "Loading more replies…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:201 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:210 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:202 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:211 msgid "Loading more shares…" msgstr "Loading more shares…" -#: src/components/BlockedAccountsList.tsx:196 -#: src/components/MutedAccountsList.tsx:193 +#: src/components/NewsDiscussion.tsx:79 +msgid "Loading more sharing posts…" +msgstr "Loading more sharing posts…" + +#: src/components/NewsList.tsx:141 +msgid "Loading more stories…" +msgstr "Loading more stories…" + +#: src/components/AccountListBase.tsx:109 msgid "Loading more…" msgstr "Loading more…" -#: src/components/NoteComposer.tsx:1066 +#: src/components/NoteComposer.tsx:1077 msgid "Loading quoted post…" msgstr "Loading quoted post…" @@ -1683,13 +1844,13 @@ msgstr "Loading quoted post…" msgid "Loading search results…" msgstr "Loading search results…" -#: src/components/NoteComposer.tsx:1015 -#: src/routes/(root)/[handle]/drafts/index.tsx:342 +#: src/components/NoteComposer.tsx:1026 +#: src/routes/(root)/[handle]/drafts/index.tsx:344 #: src/routes/(root)/sign/up/[token].tsx:465 msgid "Loading…" msgstr "Loading…" -#: src/routes/(root)/[handle]/settings/preferences.tsx:226 +#: src/routes/(root)/[handle]/settings/preferences.tsx:227 msgid "Locked to \"Only me\" because your default note privacy restricts visibility." msgstr "Locked to \"Only me\" because your default note privacy restricts visibility." @@ -1706,12 +1867,12 @@ msgid "Markdown guide" msgstr "Markdown guide" #: src/components/article-composer/ArticleComposerForm.tsx:90 -#: src/components/NoteComposer.tsx:1232 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:417 +#: src/components/NoteComposer.tsx:1243 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:418 msgid "Markdown supported" msgstr "Markdown supported" -#: src/components/AppSidebar.tsx:796 +#: src/components/AppSidebar.tsx:819 #: src/routes/(root)/admin/media.tsx:152 msgid "Media" msgstr "Media" @@ -1722,6 +1883,11 @@ msgstr "Media" msgid "Mentioned only" msgstr "Mentioned only" +#: src/components/NewsStoryCard.tsx:225 +#: src/components/NewsStoryCard.tsx:243 +msgid "Moderate" +msgstr "Moderate" + #: src/components/PostEngagementBar.tsx:436 #: src/components/PostEngagementBar.tsx:437 msgid "More engagement views" @@ -1751,7 +1917,7 @@ msgstr "Multiple choice" msgid "Mute" msgstr "Mute" -#: src/routes/(root)/[handle]/settings/blocks.tsx:84 +#: src/routes/(root)/[handle]/settings/blocks.tsx:85 msgid "Muted accounts" msgstr "Muted accounts" @@ -1759,7 +1925,7 @@ msgstr "Muted accounts" #~ msgid "Muted accounts are hidden from your feeds and stop notifying you, but you can still visit their profiles. Muting is private and is never federated." #~ msgstr "Muted accounts are hidden from your feeds and stop notifying you, but you can still visit their profiles. Muting is private and is never federated." -#: src/routes/(root)/[handle]/settings/blocks.tsx:86 +#: src/routes/(root)/[handle]/settings/blocks.tsx:87 msgid "Muted accounts are hidden from your feeds and stop notifying you, except for replies and mentions from accounts you follow. You can still visit their profiles, and muting is private and never federated." msgstr "Muted accounts are hidden from your feeds and stop notifying you, except for replies and mentions from accounts you follow. You can still visit their profiles, and muting is private and never federated." @@ -1767,11 +1933,11 @@ msgstr "Muted accounts are hidden from your feeds and stop notifying you, except msgid "Mutes & blocks" msgstr "Mutes & blocks" -#: src/routes/(root)/[handle]/settings/blocks.tsx:77 +#: src/routes/(root)/[handle]/settings/blocks.tsx:78 msgid "Mutes and blocks" msgstr "Mutes and blocks" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:375 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:376 msgid "My passkey" msgstr "My passkey" @@ -1787,21 +1953,25 @@ msgstr "Name is too long. Maximum length is 50 characters." msgid "Native name" msgstr "Native name" +#: src/routes/(root)/admin/news.tsx:365 +msgid "never" +msgstr "never" + #: src/routes/(root)/admin/invitations.tsx:167 msgid "Never" msgstr "Never" -#: src/routes/(root)/[handle]/settings/invite.tsx:755 -#: src/routes/(root)/[handle]/settings/invite.tsx:821 +#: src/routes/(root)/[handle]/settings/invite.tsx:756 +#: src/routes/(root)/[handle]/settings/invite.tsx:822 msgid "Never expires" msgstr "Never expires" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:446 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:447 msgid "Never used" msgstr "Never used" -#: src/routes/(root)/[handle]/drafts/index.tsx:251 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/index.tsx:253 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "New article" msgstr "New article" @@ -1814,6 +1984,22 @@ msgstr "New notifications can now appear even when Hackers' Pub is not open." msgid "New posts available — click to load" msgstr "New posts available — click to load" +#: src/components/NewsList.tsx:89 +msgid "Newest" +msgstr "Newest" + +#: src/components/AppSidebar.tsx:244 +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:337 +#: src/routes/(root)/news/index.tsx:39 +msgid "News" +msgstr "News" + +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:139 +#~ msgid "News scores" +#~ msgstr "News scores" + #: src/components/ImageLightbox.tsx:126 msgid "Next image" msgstr "Next image" @@ -1826,10 +2012,14 @@ msgstr "No bookmarks yet" msgid "No draft to delete" msgstr "No draft to delete" -#: src/routes/(root)/[handle]/drafts/index.tsx:262 +#: src/routes/(root)/[handle]/drafts/index.tsx:264 msgid "No drafts yet. Create your first article!" msgstr "No drafts yet. Create your first article!" +#: src/routes/(root)/admin/news.tsx:428 +msgid "No exclusion patterns yet." +msgstr "No exclusion patterns yet." + #: src/components/ActorFollowerList.tsx:92 msgid "No followers found" msgstr "No followers found" @@ -1838,9 +2028,9 @@ msgstr "No followers found" msgid "No following found" msgstr "No following found" -#: src/routes/(root)/[handle]/invite/[id].tsx:265 -#: src/routes/(root)/[handle]/settings/invite.tsx:410 -#: src/routes/(root)/[handle]/settings/invite.tsx:831 +#: src/routes/(root)/[handle]/invite/[id].tsx:270 +#: src/routes/(root)/[handle]/settings/invite.tsx:411 +#: src/routes/(root)/[handle]/settings/invite.tsx:832 msgid "No invitations left" msgstr "No invitations left" @@ -1856,16 +2046,24 @@ msgstr "No notes articles" msgid "No notes found" msgstr "No notes found" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:171 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:178 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:172 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:179 msgid "No one has quoted this yet." msgstr "No one has quoted this yet." -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:184 +#: src/components/NewsDiscussion.tsx:86 +msgid "No one has shared this link in a public post yet." +msgstr "No one has shared this link in a public post yet." + +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:185 msgid "No one has shared this yet." msgstr "No one has shared this yet." +#: src/routes/(root)/admin/news.tsx:473 +msgid "No penalized links." +msgstr "No penalized links." + #: src/components/ActorPostList.tsx:140 #: src/components/ActorSharedPostList.tsx:93 #: src/components/PersonalTimeline.tsx:289 @@ -1878,8 +2076,8 @@ msgstr "No posts found" msgid "No previews" msgstr "No previews" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:189 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:190 msgid "No reactions yet." msgstr "No reactions yet." @@ -1887,6 +2085,10 @@ msgstr "No reactions yet." msgid "No reactors loaded." msgstr "No reactors loaded." +#: src/components/NewsList.tsx:148 +msgid "No shared links yet. Once links start circulating across the fediverse, they will appear here." +msgstr "No shared links yet. Once links start circulating across the fediverse, they will appear here." + #: src/routes/(root)/sign/index.tsx:223 msgid "No such account in Hackers' Pub—please try again." msgstr "No such account in Hackers' Pub—please try again." @@ -1895,16 +2097,29 @@ msgstr "No such account in Hackers' Pub—please try again." msgid "No user URI provided." msgstr "No user URI provided." +#: src/routes/(root)/admin/news.tsx:303 +msgid "Not authorized to clear penalties." +msgstr "Not authorized to clear penalties." + #: src/routes/(root)/admin/media.tsx:117 msgid "Not authorized to delete orphan media." msgstr "Not authorized to delete orphan media." +#: src/routes/(root)/admin/news.tsx:244 +#: src/routes/(root)/admin/news.tsx:274 +msgid "Not authorized to manage exclusions." +msgstr "Not authorized to manage exclusions." + +#: src/routes/(root)/admin/news.tsx:203 +msgid "Not authorized to recompute news scores." +msgstr "Not authorized to recompute news scores." + #: src/routes/(root)/admin/invitations.tsx:116 msgid "Not authorized to regenerate invitations." msgstr "Not authorized to regenerate invitations." -#: src/routes/(root)/[handle]/invite/[id].tsx:222 -#: src/routes/(root)/[handle]/invite/[id].tsx:225 +#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:230 msgid "Not found" msgstr "Not found" @@ -1916,26 +2131,30 @@ msgstr "Not found" #~ msgid "Note" #~ msgstr "Note" -#: src/components/NoteComposer.tsx:885 +#: src/routes/(root)/admin/news.tsx:407 +msgid "Note (optional)" +msgstr "Note (optional)" + +#: src/components/NoteComposer.tsx:896 msgid "Note created successfully" msgstr "Note created successfully" #. placeholder {0}: "REL_ME_ATTR" -#: src/routes/(root)/[handle]/settings/index.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:524 msgid "Note that you can verify your links belong to you by making the linked pages also link to your Hackers' Pub profile with {0} attribute." msgstr "Note that you can verify your links belong to you by making the linked pages also link to your Hackers' Pub profile with {0} attribute." -#: src/components/NoteComposer.tsx:833 +#: src/components/NoteComposer.tsx:839 msgid "Note updated" msgstr "Note updated" #: src/components/ProfileTabs.tsx:44 -#: src/routes/(root)/[handle]/bookmarks.tsx:137 +#: src/routes/(root)/[handle]/bookmarks.tsx:138 msgid "Notes" msgstr "Notes" #: src/components/MarkdownEditor.tsx:193 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:463 msgid "Nothing to preview" msgstr "Nothing to preview" @@ -1948,7 +2167,7 @@ msgstr "Notification permission was not granted." msgid "Notification preview privacy" msgstr "Notification preview privacy" -#: src/components/AppSidebar.tsx:574 +#: src/components/AppSidebar.tsx:597 msgid "Notifications" msgstr "Notifications" @@ -1961,7 +2180,7 @@ msgstr "Notifications are blocked for this site." msgid "Notifications are blocked in your browser settings." msgstr "Notifications are blocked in your browser settings." -#: src/routes/(root)/[handle]/settings/invite.tsx:782 +#: src/routes/(root)/[handle]/settings/invite.tsx:783 msgid "Number of invitations" msgstr "Number of invitations" @@ -1986,7 +2205,7 @@ msgstr "Or" msgid "Or enter the code from the email" msgstr "Or enter the code from the email" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:877 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:878 msgid "Other languages" msgstr "Other languages" @@ -1998,31 +2217,39 @@ msgstr "Page not found" msgid "Passkey authentication failed" msgstr "Passkey authentication failed" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:370 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:371 msgid "Passkey name" msgstr "Passkey name" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:265 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:266 msgid "Passkey registered successfully" msgstr "Passkey registered successfully" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:312 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 msgid "Passkey revoked" msgstr "Passkey revoked" #: src/components/SettingsTabs.tsx:77 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:355 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:356 msgid "Passkeys" msgstr "Passkeys" -#: src/routes/(root)/[handle]/bookmarks.tsx:98 -#: src/routes/(root)/[handle]/bookmarks.tsx:101 -#: src/routes/(root)/[handle]/drafts/[id].tsx:50 -#: src/routes/(root)/[handle]/drafts/[id].tsx:51 -#: src/routes/(root)/[handle]/drafts/index.tsx:213 -#: src/routes/(root)/[handle]/drafts/index.tsx:216 -#: src/routes/(root)/[handle]/drafts/new.tsx:60 -#: src/routes/(root)/[handle]/drafts/new.tsx:61 +#: src/routes/(root)/admin/news.tsx:463 +msgid "Penalized links" +msgstr "Penalized links" + +#: src/routes/(root)/admin/news.tsx:297 +msgid "Penalty cleared." +msgstr "Penalty cleared." + +#: src/routes/(root)/[handle]/bookmarks.tsx:99 +#: src/routes/(root)/[handle]/bookmarks.tsx:102 +#: src/routes/(root)/[handle]/drafts/[id].tsx:52 +#: src/routes/(root)/[handle]/drafts/[id].tsx:53 +#: src/routes/(root)/[handle]/drafts/index.tsx:215 +#: src/routes/(root)/[handle]/drafts/index.tsx:218 +#: src/routes/(root)/[handle]/drafts/new.tsx:62 +#: src/routes/(root)/[handle]/drafts/new.tsx:63 msgid "Permission denied" msgstr "Permission denied" @@ -2030,21 +2257,21 @@ msgstr "Permission denied" msgid "Pin to profile" msgstr "Pin to profile" -#: src/routes/(root)/[handle]/(profile)/index.tsx:302 +#: src/routes/(root)/[handle]/(profile)/index.tsx:305 msgid "Pinned posts" msgstr "Pinned posts" -#: src/routes/(root)/[handle]/settings/index.tsx:187 +#: src/routes/(root)/[handle]/settings/index.tsx:188 msgid "Please choose an image file smaller than 5 MiB." msgstr "Please choose an image file smaller than 5 MiB." -#: src/routes/(root)/[handle]/settings/invite.tsx:249 -#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:250 +#: src/routes/(root)/[handle]/settings/invite.tsx:541 msgid "Please correct the errors and try again." msgstr "Please correct the errors and try again." #: src/components/article-composer/ArticleComposerForm.tsx:53 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:383 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:384 msgid "Please enter a title for your article." msgstr "Please enter a title for your article." @@ -2052,9 +2279,9 @@ msgstr "Please enter a title for your article." msgid "Please enter your Fediverse handle." msgstr "Please enter your Fediverse handle." -#: src/routes/(root)/[handle]/drafts/[id].tsx:55 -#: src/routes/(root)/[handle]/drafts/index.tsx:220 -#: src/routes/(root)/[handle]/drafts/new.tsx:65 +#: src/routes/(root)/[handle]/drafts/[id].tsx:57 +#: src/routes/(root)/[handle]/drafts/index.tsx:222 +#: src/routes/(root)/[handle]/drafts/new.tsx:67 msgid "Please sign in to access this page" msgstr "Please sign in to access this page" @@ -2066,6 +2293,10 @@ msgstr "Please sign in to vote" msgid "Poll closed" msgstr "Poll closed" +#: src/components/NewsList.tsx:88 +msgid "Popular" +msgstr "Popular" + #: src/components/PostActionMenu.tsx:291 msgid "Post deleted" msgstr "Post deleted" @@ -2083,27 +2314,27 @@ msgstr "Post unpinned" msgid "Posts" msgstr "Posts" -#: src/routes/(root)/[handle]/settings/preferences.tsx:183 +#: src/routes/(root)/[handle]/settings/preferences.tsx:184 msgid "Prefer AI-generated summary" msgstr "Prefer AI-generated summary" #: src/components/SettingsTabs.tsx:53 -#: src/routes/(root)/[handle]/settings/preferences.tsx:169 #: src/routes/(root)/[handle]/settings/preferences.tsx:170 +#: src/routes/(root)/[handle]/settings/preferences.tsx:171 msgid "Preferences" msgstr "Preferences" -#: src/routes/(root)/[handle]/invite/[id].tsx:387 +#: src/routes/(root)/[handle]/invite/[id].tsx:392 msgid "Preferred language" msgstr "Preferred language" -#: src/routes/(root)/[handle]/settings/language.tsx:83 +#: src/routes/(root)/[handle]/settings/language.tsx:84 msgid "Preferred languages" msgstr "Preferred languages" #: src/components/article-composer/ArticleComposerForm.tsx:111 #: src/components/MarkdownEditor.tsx:164 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:426 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:427 msgid "Preview" msgstr "Preview" @@ -2115,7 +2346,7 @@ msgstr "Previous image" msgid "Priority" msgstr "Priority" -#: src/components/AppSidebar.tsx:982 +#: src/components/AppSidebar.tsx:1028 #: src/routes/(root)/privacy.tsx:40 msgid "Privacy policy" msgstr "Privacy policy" @@ -2128,8 +2359,8 @@ msgstr "Profile" msgid "Profile actions" msgstr "Profile actions" -#: src/routes/(root)/[handle]/settings/index.tsx:124 #: src/routes/(root)/[handle]/settings/index.tsx:125 +#: src/routes/(root)/[handle]/settings/index.tsx:126 msgid "Profile settings" msgstr "Profile settings" @@ -2159,8 +2390,8 @@ msgstr "Publishing…" msgid "Push notification privacy updated" msgstr "Push notification privacy updated" -#: src/routes/(root)/[handle]/settings/invite.tsx:647 -#: src/routes/(root)/[handle]/settings/invite.tsx:713 +#: src/routes/(root)/[handle]/settings/invite.tsx:648 +#: src/routes/(root)/[handle]/settings/invite.tsx:714 msgid "QR code" msgstr "QR code" @@ -2174,7 +2405,7 @@ msgstr "Query cannot be empty" msgid "Quiet public" msgstr "Quiet public" -#: src/components/NoteComposeModal.tsx:71 +#: src/components/NoteComposeModal.tsx:72 #: src/components/PostEngagementBar.tsx:270 #: src/components/ui/markdown-editor.tsx:289 msgid "Quote" @@ -2198,8 +2429,8 @@ msgid "Quoted post hidden" msgstr "Quoted post hidden" #: src/components/EngagementTabs.tsx:46 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:100 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:111 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:101 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:112 msgid "Quotes" msgstr "Quotes" @@ -2222,8 +2453,8 @@ msgid "reaction" msgstr "reaction" #: src/components/EngagementTabs.tsx:55 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:159 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:173 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:160 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:174 msgid "Reactions" msgstr "Reactions" @@ -2236,6 +2467,10 @@ msgstr "Read full article" #~ msgid "Read the full Code of conduct" #~ msgstr "Read the full Code of conduct" +#: src/routes/(root)/admin/news.tsx:344 +msgid "Rebuilds the popularity score of every shared link from scratch. The operation is idempotent and safe to run at any time; scores normally stay fresh on their own, so this is mainly a manual backstop and a development tool." +msgstr "Rebuilds the popularity score of every shared link from scratch. The operation is idempotent and safe to run at any time; scores normally stay fresh on their own, so this is mainly a manual backstop and a development tool." + #: src/components/WebPushPromptBanner.tsx:252 msgid "Receive new notifications immediately, even when this tab is closed." msgstr "Receive new notifications immediately, even when this tab is closed." @@ -2244,10 +2479,19 @@ msgstr "Receive new notifications immediately, even when this tab is closed." msgid "Receive notifications immediately through this browser, even when this tab is closed." msgstr "Receive notifications immediately through this browser, even when this tab is closed." -#: src/components/AppSidebar.tsx:901 +#: src/components/AppSidebar.tsx:947 msgid "Recent drafts" msgstr "Recent drafts" +#: src/routes/(root)/admin/news.tsx:342 +#: src/routes/(root)/admin/news.tsx:375 +msgid "Recompute news scores" +msgstr "Recompute news scores" + +#: src/routes/(root)/admin/news.tsx:374 +msgid "Recomputing…" +msgstr "Recomputing…" + #: src/routes/(root)/admin/invitations.tsx:203 msgid "Regenerate" msgstr "Regenerate" @@ -2264,23 +2508,23 @@ msgstr "Regenerate invitations" msgid "Regenerating…" msgstr "Regenerating…" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Register" msgstr "Register" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:362 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:363 msgid "Register a passkey" msgstr "Register a passkey" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:364 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:365 msgid "Register a passkey to sign in to your account. You can use a passkey instead of receiving a sign-in link by email." msgstr "Register a passkey to sign in to your account. You can use a passkey instead of receiving a sign-in link by email." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:393 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:394 msgid "Registered passkeys" msgstr "Registered passkeys" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Registering…" msgstr "Registering…" @@ -2291,6 +2535,7 @@ msgid "Remote follow" msgstr "Remote follow" #: src/components/LanguageList.tsx:225 +#: src/routes/(root)/admin/news.tsx:451 msgid "Remove" msgstr "Remove" @@ -2308,13 +2553,13 @@ msgstr "Remove bookmark" msgid "Remove from sidebar" msgstr "Remove from sidebar" -#: src/components/NoteComposer.tsx:1358 -#: src/components/NoteComposer.tsx:1359 +#: src/components/NoteComposer.tsx:1369 +#: src/components/NoteComposer.tsx:1370 msgid "Remove image" msgstr "Remove image" -#: src/components/NoteComposer.tsx:1113 -#: src/components/NoteComposer.tsx:1114 +#: src/components/NoteComposer.tsx:1124 +#: src/components/NoteComposer.tsx:1125 msgid "Remove quote" msgstr "Remove quote" @@ -2324,11 +2569,11 @@ msgstr "Removes stored media that are old enough and no longer attached to an av #: src/components/article-composer/ArticleComposerForm.tsx:134 #: src/components/MarkdownEditor.tsx:181 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:452 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:453 msgid "Rendering…" msgstr "Rendering…" -#: src/components/NoteComposeModal.tsx:70 +#: src/components/NoteComposeModal.tsx:71 #: src/components/PostEngagementBar.tsx:258 msgid "Reply" msgstr "Reply" @@ -2337,20 +2582,20 @@ msgstr "Reply" msgid "Replying is not available for this post" msgstr "Replying is not available for this post" -#: src/components/NoteComposer.tsx:1007 +#: src/components/NoteComposer.tsx:1018 msgid "Replying to" msgstr "Replying to" -#: src/components/AppSidebar.tsx:498 +#: src/components/AppSidebar.tsx:521 msgid "Return to old UI" msgstr "Return to old UI" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:475 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:526 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:476 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:527 msgid "Revoke" msgstr "Revoke" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:515 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:516 msgid "Revoke passkey" msgstr "Revoke passkey" @@ -2363,14 +2608,14 @@ msgstr "Revoke quote" msgid "Revoke this quote?" msgstr "Revoke this quote?" -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Save" msgstr "Save" -#: src/components/NoteComposer.tsx:1412 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 +#: src/components/NoteComposer.tsx:1423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 msgid "Save changes" msgstr "Save changes" @@ -2383,16 +2628,16 @@ msgid "Save draft to see preview" msgstr "Save draft to see preview" #: src/components/article-composer/ArticleComposerActions.tsx:36 -#: src/components/NoteComposer.tsx:1413 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/components/NoteComposer.tsx:1424 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Saving…" msgstr "Saving…" #: src/components/admin/AdminAccountsTable.tsx:202 -#: src/components/AppSidebar.tsx:377 +#: src/components/AppSidebar.tsx:400 #: src/components/SearchForm.tsx:65 #: src/components/SearchForm.tsx:80 msgid "Search" @@ -2427,16 +2672,16 @@ msgstr "Select an option" msgid "Select options" msgstr "Select options" -#: src/routes/(root)/[handle]/settings/language.tsx:84 +#: src/routes/(root)/[handle]/settings/language.tsx:85 msgid "Select your preferred languages in order of preference. This will help tailor content to your preferences." msgstr "Select your preferred languages in order of preference. This will help tailor content to your preferences." -#: src/routes/(root)/[handle]/settings/invite.tsx:413 +#: src/routes/(root)/[handle]/settings/invite.tsx:414 msgid "Send" msgstr "Send" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 -#: src/routes/(root)/[handle]/settings/invite.tsx:412 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 +#: src/routes/(root)/[handle]/settings/invite.tsx:413 msgid "Sending…" msgstr "Sending…" @@ -2448,11 +2693,11 @@ msgstr "Sensitive content" msgid "Separate tags with spaces. Tags help readers discover your article." msgstr "Separate tags with spaces. Tags help readers discover your article." -#: src/routes/(root)/[handle]/settings/preferences.tsx:171 +#: src/routes/(root)/[handle]/settings/preferences.tsx:172 msgid "Set your personal preferences." msgstr "Set your personal preferences." -#: src/components/AppSidebar.tsx:674 +#: src/components/AppSidebar.tsx:697 msgid "Settings" msgstr "Settings" @@ -2460,10 +2705,19 @@ msgstr "Settings" msgid "Share" msgstr "Share" +#: src/components/NewsStoryCard.tsx:198 +#: src/components/NewsStoryHeader.tsx:132 +msgid "Share this link" +msgstr "Share this link" + +#: src/components/NewsDiscussionComposer.tsx:30 +msgid "Share your opinion on this story…" +msgstr "Share your opinion on this story…" + #: src/components/EngagementTabs.tsx:37 #: src/components/ProfileTabs.tsx:54 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:101 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:114 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:102 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:115 msgid "Shares" msgstr "Shares" @@ -2478,6 +2732,15 @@ msgstr "Sharing is not available for this post" msgid "Show" msgstr "Show" +#. placeholder {0}: childCount() +#: src/components/NewsDiscussionThread.tsx:356 +msgid "Show {0} more in this thread" +msgstr "Show {0} more in this thread" + +#: src/components/NewsDiscussion.tsx:77 +msgid "Show more sharing posts" +msgstr "Show more sharing posts" + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Show preview" #~ msgstr "Show preview" @@ -2486,7 +2749,7 @@ msgstr "Show" msgid "Show sensitive content" msgstr "Show sensitive content" -#: src/components/AppSidebar.tsx:535 +#: src/components/AppSidebar.tsx:558 #: src/routes/(root)/sign/index.tsx:382 msgid "Sign in" msgstr "Sign in" @@ -2495,6 +2758,10 @@ msgstr "Sign in" msgid "Sign in to Hackers' Pub" msgstr "Sign in to Hackers' Pub" +#: src/components/NewsDiscussionComposer.tsx:44 +msgid "Sign in to post" +msgstr "Sign in to post" + #: src/components/QuestionCard.tsx:394 msgid "Sign in to vote" msgstr "Sign in to vote" @@ -2503,11 +2770,11 @@ msgstr "Sign in to vote" msgid "Sign in with passkey" msgstr "Sign in with passkey" -#: src/components/AppSidebar.tsx:964 +#: src/components/AppSidebar.tsx:1010 msgid "Sign out" msgstr "Sign out" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 #: src/routes/(root)/sign/up/[token].tsx:494 msgid "Sign up" msgstr "Sign up" @@ -2540,7 +2807,7 @@ msgstr "Slug (URL)" msgid "Slug cannot be empty" msgstr "Slug cannot be empty" -#: src/components/NoteComposer.tsx:613 +#: src/components/NoteComposer.tsx:619 msgid "Some images were skipped because the limit of {MAX_MEDIA} was reached" msgstr "Some images were skipped because the limit of {MAX_MEDIA} was reached" @@ -2555,22 +2822,22 @@ msgstr "Something went wrong—please try again." #: src/components/article-composer/ArticleComposerContext.tsx:309 #: src/components/article-composer/ArticleComposerContext.tsx:384 #: src/components/article-composer/ArticleComposerContext.tsx:449 -#: src/components/NoteComposer.tsx:832 -#: src/components/NoteComposer.tsx:884 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:331 -#: src/routes/(root)/[handle]/drafts/index.tsx:174 +#: src/components/NoteComposer.tsx:838 +#: src/components/NoteComposer.tsx:895 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/drafts/index.tsx:175 msgid "Success" msgstr "Success" -#: src/routes/(root)/[handle]/settings/language.tsx:147 +#: src/routes/(root)/[handle]/settings/language.tsx:148 msgid "Successfully saved language preferences" msgstr "Successfully saved language preferences" -#: src/routes/(root)/[handle]/settings/preferences.tsx:133 +#: src/routes/(root)/[handle]/settings/preferences.tsx:134 msgid "Successfully saved preferences" msgstr "Successfully saved preferences" -#: src/routes/(root)/[handle]/settings/index.tsx:328 +#: src/routes/(root)/[handle]/settings/index.tsx:329 msgid "Successfully saved settings" msgstr "Successfully saved settings" @@ -2579,9 +2846,9 @@ msgid "Summarized by LLM" msgstr "Summarized by LLM" #: src/components/DocumentView.tsx:38 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:721 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:729 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1056 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:722 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:730 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1057 msgid "Table of contents" msgstr "Table of contents" @@ -2590,8 +2857,8 @@ msgstr "Table of contents" #~ msgstr "Tag" #: src/components/article-composer/ArticleComposerForm.tsx:158 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:478 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1065 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:479 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1066 msgid "Tags" msgstr "Tags" @@ -2599,65 +2866,73 @@ msgstr "Tags" msgid "Tell us about yourself…" msgstr "Tell us about yourself…" +#: src/routes/(root)/admin/news.tsx:237 +msgid "That is not a valid URL pattern." +msgstr "That is not a valid URL pattern." + #: src/components/WebPushNotificationSettings.tsx:158 #: src/components/WebPushPromptBanner.tsx:93 msgid "The browser did not provide a complete push subscription." msgstr "The browser did not provide a complete push subscription." -#: src/routes/(root)/[handle]/settings/preferences.tsx:200 +#: src/routes/(root)/[handle]/settings/preferences.tsx:201 msgid "The default privacy setting for your notes." msgstr "The default privacy setting for your notes." -#: src/routes/(root)/[handle]/settings/preferences.tsx:212 +#: src/routes/(root)/[handle]/settings/preferences.tsx:213 msgid "The default privacy setting for your shares." msgstr "The default privacy setting for your shares." -#: src/routes/(root)/[handle]/settings/preferences.tsx:227 +#: src/routes/(root)/[handle]/settings/preferences.tsx:228 msgid "The default quote permission for your notes." msgstr "The default quote permission for your notes." -#: src/routes/(root)/[handle]/invite/[id].tsx:169 -#: src/routes/(root)/[handle]/settings/invite.tsx:352 +#: src/routes/(root)/[handle]/invite/[id].tsx:174 +#: src/routes/(root)/[handle]/settings/invite.tsx:353 msgid "The email address is invalid." msgstr "The email address is invalid." -#: src/routes/(root)/[handle]/settings/invite.tsx:347 +#: src/routes/(root)/[handle]/settings/invite.tsx:348 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "The email address is not only used for receiving the invitation, but also for signing in to the account." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:395 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:396 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "The following passkeys are registered to your account. You can use them to sign in to your account." -#: src/routes/(root)/[handle]/invite/[id].tsx:187 +#: src/routes/(root)/[handle]/invite/[id].tsx:192 msgid "The invitation email could not be sent. Please try again later." msgstr "The invitation email could not be sent. Please try again later." -#: src/routes/(root)/[handle]/settings/invite.tsx:259 +#: src/routes/(root)/[handle]/settings/invite.tsx:260 msgid "The invitation has been sent successfully." msgstr "The invitation has been sent successfully." -#: src/routes/(root)/[handle]/settings/invite.tsx:590 +#: src/routes/(root)/[handle]/settings/invite.tsx:591 msgid "The invitation link could not be found or you are not authorized to delete it." msgstr "The invitation link could not be found or you are not authorized to delete it." -#: src/routes/(root)/[handle]/settings/invite.tsx:612 +#: src/routes/(root)/[handle]/settings/invite.tsx:613 msgid "The invitation link has been copied to the clipboard." msgstr "The invitation link has been copied to the clipboard." -#: src/routes/(root)/[handle]/settings/invite.tsx:532 +#: src/routes/(root)/[handle]/settings/invite.tsx:533 msgid "The invitation link has been created successfully." msgstr "The invitation link has been created successfully." -#: src/routes/(root)/[handle]/settings/invite.tsx:583 +#: src/routes/(root)/[handle]/settings/invite.tsx:584 msgid "The invitation link has been deleted successfully." msgstr "The invitation link has been deleted successfully." +#: src/components/NewsDiscussionComposer.tsx:34 +msgid "The link to this story is added to your post automatically." +msgstr "The link to this story is added to your post automatically." + #: src/components/NotFoundPage.tsx:45 msgid "The page you're looking for doesn't exist or has been moved." msgstr "The page you're looking for doesn't exist or has been moved." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:314 msgid "The passkey has been successfully revoked." msgstr "The passkey has been successfully revoked." @@ -2675,7 +2950,7 @@ msgstr "The sign-up link is invalid. Please make sure you're using the correct l #. placeholder {0}: "GITHUB_REPOSITORY" #. placeholder {1}: "AGPL-3.0" -#: src/components/AppSidebar.tsx:1006 +#: src/components/AppSidebar.tsx:1052 msgid "The source code of this website is available on {0} under the {1} license." msgstr "The source code of this website is available on {0} under the {1} license." @@ -2683,7 +2958,7 @@ msgstr "The source code of this website is available on {0} under the {1} licens msgid "The title will appear at the top of your article and in link previews." msgstr "The title will appear at the top of your article and in link previews." -#: src/routes/(root)/[handle]/settings/index.tsx:603 +#: src/routes/(root)/[handle]/settings/index.tsx:604 msgid "The URL of the link, e.g., https://github.com/yourhandle." msgstr "The URL of the link, e.g., https://github.com/yourhandle." @@ -2701,26 +2976,26 @@ msgstr "This action cannot be undone. This will permanently delete this post." msgid "This browser does not support Web Push." msgstr "This browser does not support Web Push." -#: src/routes/(root)/[handle]/invite/[id].tsx:173 -#: src/routes/(root)/[handle]/invite/[id].tsx:177 +#: src/routes/(root)/[handle]/invite/[id].tsx:178 +#: src/routes/(root)/[handle]/invite/[id].tsx:182 msgid "This email is already associated with an existing account." msgstr "This email is already associated with an existing account." -#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:232 msgid "This invitation link does not exist or has been deleted." msgstr "This invitation link does not exist or has been deleted." -#: src/routes/(root)/[handle]/invite/[id].tsx:161 -#: src/routes/(root)/[handle]/invite/[id].tsx:257 +#: src/routes/(root)/[handle]/invite/[id].tsx:166 +#: src/routes/(root)/[handle]/invite/[id].tsx:262 msgid "This invitation link has expired." msgstr "This invitation link has expired." -#: src/routes/(root)/[handle]/invite/[id].tsx:164 -#: src/routes/(root)/[handle]/invite/[id].tsx:267 +#: src/routes/(root)/[handle]/invite/[id].tsx:169 +#: src/routes/(root)/[handle]/invite/[id].tsx:272 msgid "This invitation link has no remaining invitations." msgstr "This invitation link has no remaining invitations." -#: src/routes/(root)/[handle]/invite/[id].tsx:159 +#: src/routes/(root)/[handle]/invite/[id].tsx:164 msgid "This invitation link was not found." msgstr "This invitation link was not found." @@ -2752,7 +3027,7 @@ msgstr "This server has not configured Web Push yet." msgid "This service does not support remote follow." msgstr "This service does not support remote follow." -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:552 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:553 msgid "This usually takes about a minute. The page will update automatically when the translation is ready." msgstr "This usually takes about a minute. The page will update automatically when the translation is ready." @@ -2769,7 +3044,7 @@ msgid "Timeline" msgstr "Timeline" #: src/components/article-composer/ArticleComposerForm.tsx:49 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:380 msgid "Title" msgstr "Title" @@ -2793,7 +3068,7 @@ msgstr "Total: {0}" #. placeholder {0}: "LANGUAGE" #: src/components/ArticleCard.tsx:350 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:861 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:862 msgid "Translated from {0}" msgstr "Translated from {0}" @@ -2801,18 +3076,18 @@ msgstr "Translated from {0}" #~ msgid "Translating to {0}…" #~ msgstr "Translating to {0}…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:548 msgid "Translating to {name}…" msgstr "Translating to {name}…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:543 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:544 msgid "Translating…" msgstr "Translating…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:378 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:401 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:410 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:588 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:402 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:411 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:589 msgid "Translation request failed" msgstr "Translation request failed" @@ -2820,17 +3095,17 @@ msgstr "Translation request failed" #~ msgid "Translation request failed for {0}" #~ msgstr "Translation request failed for {0}" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:593 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:594 msgid "Translation request failed for {name}" msgstr "Translation request failed for {name}" #: src/app.tsx:125 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:601 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:602 msgid "Try again" msgstr "Try again" #: src/components/article-composer/ArticleComposerForm.tsx:162 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:482 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:483 msgid "Type tags separated by spaces" msgstr "Type tags separated by spaces" @@ -2848,7 +3123,7 @@ msgstr "Unable to add reaction. Please try again." msgid "Unable to remove reaction. Please try again." msgstr "Unable to remove reaction. Please try again." -#: src/components/BlockedAccountsList.tsx:181 +#: src/components/BlockedAccountsList.tsx:117 #: src/components/ProfileActionMenu.tsx:377 #: src/components/ProfileActionMenu.tsx:405 #: src/components/ProfileActionMenu.tsx:413 @@ -2864,7 +3139,7 @@ msgstr "Unblock user?" msgid "Unfollow" msgstr "Unfollow" -#: src/components/MutedAccountsList.tsx:178 +#: src/components/MutedAccountsList.tsx:114 #: src/components/ProfileActionMenu.tsx:361 msgid "Unmute" msgstr "Unmute" @@ -2877,23 +3152,27 @@ msgstr "Unpin from profile" msgid "Unshare" msgstr "Unshare" -#: src/routes/(root)/[handle]/settings/index.tsx:126 +#: src/routes/(root)/[handle]/settings/index.tsx:127 msgid "Update your profile information, including your avatar, username, display name, bio, and links." msgstr "Update your profile information, including your avatar, username, display name, bio, and links." #. placeholder {0}: new Date(edge.node.updated).toLocaleDateString() -#: src/routes/(root)/[handle]/drafts/index.tsx:304 +#: src/routes/(root)/[handle]/drafts/index.tsx:306 msgid "Updated {0}" msgstr "Updated {0}" -#: src/components/NoteComposer.tsx:1274 +#: src/components/NoteComposer.tsx:1285 msgid "Upload progress" msgstr "Upload progress" -#: src/routes/(root)/[handle]/settings/index.tsx:585 +#: src/routes/(root)/[handle]/settings/index.tsx:586 msgid "URL" msgstr "URL" +#: src/routes/(root)/admin/news.tsx:394 +msgid "URL pattern" +msgstr "URL pattern" + #: src/components/ProfileActionMenu.tsx:277 msgid "User blocked" msgstr "User blocked" @@ -2906,17 +3185,17 @@ msgstr "User muted" msgid "User not found." msgstr "User not found." -#: src/components/BlockedAccountsList.tsx:95 +#: src/components/BlockedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:259 msgid "User unblocked" msgstr "User unblocked" -#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:304 msgid "User unmuted" msgstr "User unmuted" -#: src/routes/(root)/[handle]/settings/index.tsx:413 +#: src/routes/(root)/[handle]/settings/index.tsx:414 #: src/routes/(root)/sign/up/[token].tsx:331 msgid "Username" msgstr "Username" @@ -2937,7 +3216,7 @@ msgstr "Username is required." msgid "Username is too long. Maximum length is 15 characters." msgstr "Username is too long. Maximum length is 15 characters." -#: src/routes/(root)/[handle]/settings/invite.tsx:435 +#: src/routes/(root)/[handle]/settings/invite.tsx:436 msgid "Users you have invited" msgstr "Users you have invited" @@ -2951,7 +3230,7 @@ msgstr "Verified that this link is owned by {0} {1}" msgid "Verifying your invitation…" msgstr "Verifying your invitation…" -#: src/components/AppSidebar.tsx:927 +#: src/components/AppSidebar.tsx:973 msgid "View all drafts →" msgstr "View all drafts →" @@ -3019,7 +3298,7 @@ msgstr "Voted" msgid "Voting…" msgstr "Voting…" -#: src/components/NoteComposer.tsx:611 +#: src/components/NoteComposer.tsx:617 msgid "Warning" msgstr "Warning" @@ -3027,11 +3306,11 @@ msgstr "Warning" msgid "We couldn't reach the server" msgstr "We couldn't reach the server" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:598 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:599 msgid "We couldn't reach the translation service. Try again, or come back in a few minutes." msgstr "We couldn't reach the translation service. Try again, or come back in a few minutes." -#: src/routes/(root)/[handle]/settings/index.tsx:566 +#: src/routes/(root)/[handle]/settings/index.tsx:567 msgid "Website" msgstr "Website" @@ -3043,7 +3322,7 @@ msgstr "Welcome to Hackers' Pub! Please fill out the form below to complete your msgid "What is Hackers' Pub?" msgstr "What is Hackers' Pub?" -#: src/components/NoteComposer.tsx:1153 +#: src/components/NoteComposer.tsx:1164 msgid "What's on your mind?" msgstr "What's on your mind?" @@ -3055,25 +3334,25 @@ msgstr "When enabled, AI may automatically translate this article into other lan #~ msgid "Who can quote this note" #~ msgstr "Who can quote this note" -#: src/components/AppSidebar.tsx:285 +#: src/components/AppSidebar.tsx:308 msgid "Without shares" msgstr "Without shares" #: src/components/article-composer/ArticleComposerForm.tsx:108 #: src/components/MarkdownEditor.tsx:161 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:424 msgid "Write" msgstr "Write" -#: src/components/NoteComposeModal.tsx:109 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1004 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:384 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:462 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:538 +#: src/components/NoteComposeModal.tsx:110 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1005 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:385 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:463 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:539 msgid "Write a reply…" msgstr "Write a reply…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:437 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:438 msgid "Write your article here." msgstr "Write your article here." @@ -3098,53 +3377,53 @@ msgstr "You are blocked by this user. You can't follow them or see their posts." msgid "You are blocking this user. They can't follow you or see your posts." msgstr "You are blocking this user. They can't follow you or see your posts." -#: src/components/NoteComposer.tsx:602 +#: src/components/NoteComposer.tsx:608 msgid "You can attach up to {MAX_MEDIA} images" msgstr "You can attach up to {MAX_MEDIA} images" -#: src/routes/(root)/[handle]/settings/index.tsx:448 +#: src/routes/(root)/[handle]/settings/index.tsx:449 msgid "You can change it only once, and the old username will become available to others." msgstr "You can change it only once, and the old username will become available to others." -#: src/routes/(root)/[handle]/settings/index.tsx:614 +#: src/routes/(root)/[handle]/settings/index.tsx:615 msgid "You can leave this empty to remove the link." msgstr "You can leave this empty to remove the link." -#: src/routes/(root)/[handle]/settings/invite.tsx:397 -#: src/routes/(root)/[handle]/settings/invite.tsx:802 +#: src/routes/(root)/[handle]/settings/invite.tsx:398 +#: src/routes/(root)/[handle]/settings/invite.tsx:803 msgid "You can leave this field empty." msgstr "You can leave this field empty." -#: src/routes/(root)/[handle]/drafts/new.tsx:64 +#: src/routes/(root)/[handle]/drafts/new.tsx:66 msgid "You can only create drafts for your own account" msgstr "You can only create drafts for your own account" -#: src/routes/(root)/[handle]/drafts/[id].tsx:54 +#: src/routes/(root)/[handle]/drafts/[id].tsx:56 msgid "You can only edit your own drafts" msgstr "You can only edit your own drafts" -#: src/routes/(root)/[handle]/bookmarks.tsx:103 +#: src/routes/(root)/[handle]/bookmarks.tsx:104 msgid "You can only view your own bookmarks" msgstr "You can only view your own bookmarks" -#: src/routes/(root)/[handle]/drafts/index.tsx:219 +#: src/routes/(root)/[handle]/drafts/index.tsx:221 msgid "You can only view your own drafts" msgstr "You can only view your own drafts" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:404 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:405 msgid "You don't have any passkeys registered yet." msgstr "You don't have any passkeys registered yet." -#: src/routes/(root)/[handle]/settings/invite.tsx:309 -#: src/routes/(root)/[handle]/settings/invite.tsx:420 +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:421 msgid "You have no invitations left. Please wait until you receive more." msgstr "You have no invitations left. Please wait until you receive more." -#: src/components/BlockedAccountsList.tsx:116 +#: src/components/BlockedAccountsList.tsx:119 msgid "You haven't blocked anyone." msgstr "You haven't blocked anyone." -#: src/components/MutedAccountsList.tsx:113 +#: src/components/MutedAccountsList.tsx:116 msgid "You haven't muted anyone." msgstr "You haven't muted anyone." @@ -3156,20 +3435,20 @@ msgstr "You haven't muted anyone." msgid "You must be signed in" msgstr "You must be signed in" -#: src/components/NoteComposer.tsx:901 +#: src/components/NoteComposer.tsx:912 msgid "You must be signed in to create a note" msgstr "You must be signed in to create a note" #: src/components/article-composer/ArticleComposerContext.tsx:467 -#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:193 msgid "You must be signed in to delete a draft" msgstr "You must be signed in to delete a draft" -#: src/components/NoteComposer.tsx:851 +#: src/components/NoteComposer.tsx:857 msgid "You must be signed in to edit a note" msgstr "You must be signed in to edit a note" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:357 msgid "You must be signed in to edit an article" msgstr "You must be signed in to edit an article" @@ -3189,15 +3468,15 @@ msgstr "You were invited by" msgid "You'll automatically follow each other when you sign up." msgstr "You'll automatically follow each other when you sign up." -#: src/routes/(root)/[handle]/invite/[id].tsx:276 +#: src/routes/(root)/[handle]/invite/[id].tsx:281 msgid "You've been invited to Hackers' Pub" msgstr "You've been invited to Hackers' Pub" -#: src/routes/(root)/[handle]/settings/index.tsx:353 +#: src/routes/(root)/[handle]/settings/index.tsx:354 msgid "Your avatar will be displayed on your profile and in your posts. You can upload a PNG, JPEG, GIF, or WebP image up to 5 MiB in size." msgstr "Your avatar will be displayed on your profile and in your posts. You can upload a PNG, JPEG, GIF, or WebP image up to 5 MiB in size." -#: src/routes/(root)/[handle]/settings/index.tsx:492 +#: src/routes/(root)/[handle]/settings/index.tsx:493 msgid "Your bio will be displayed on your profile. You can use Markdown to format it." msgstr "Your bio will be displayed on your profile. You can use Markdown to format it." @@ -3213,36 +3492,36 @@ msgstr "Your connection looks unstable. Check your network and try again." msgid "Your email address will be used to sign in to your account." msgstr "Your email address will be used to sign in to your account." -#: src/routes/(root)/[handle]/settings/invite.tsx:400 +#: src/routes/(root)/[handle]/settings/invite.tsx:401 msgid "Your friend will see this message in the invitation email." msgstr "Your friend will see this message in the invitation email." -#: src/routes/(root)/[handle]/settings/index.tsx:478 +#: src/routes/(root)/[handle]/settings/index.tsx:479 #: src/routes/(root)/sign/up/[token].tsx:393 msgid "Your name will be displayed on your profile and in your posts." msgstr "Your name will be displayed on your profile and in your posts." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:267 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:268 msgid "Your passkey has been registered and can now be used for authentication." msgstr "Your passkey has been registered and can now be used for authentication." -#: src/routes/(root)/[handle]/settings/preferences.tsx:134 +#: src/routes/(root)/[handle]/settings/preferences.tsx:135 msgid "Your preferences have been updated successfully." msgstr "Your preferences have been updated successfully." -#: src/routes/(root)/[handle]/settings/language.tsx:148 +#: src/routes/(root)/[handle]/settings/language.tsx:149 msgid "Your preferred languages have been updated." msgstr "Your preferred languages have been updated." -#: src/routes/(root)/[handle]/settings/index.tsx:329 +#: src/routes/(root)/[handle]/settings/index.tsx:330 msgid "Your profile settings have been updated successfully." msgstr "Your profile settings have been updated successfully." -#: src/components/NoteComposeModal.tsx:128 +#: src/components/NoteComposeModal.tsx:129 msgid "Your unsaved draft will be lost." msgstr "Your unsaved draft will be lost." -#: src/routes/(root)/[handle]/settings/index.tsx:445 +#: src/routes/(root)/[handle]/settings/index.tsx:446 #: src/routes/(root)/sign/up/[token].tsx:367 msgid "Your username will be used to create your profile URL and your fediverse handle." msgstr "Your username will be used to create your profile URL and your fediverse handle." diff --git a/web-next/src/locales/ja-JP/messages.po b/web-next/src/locales/ja-JP/messages.po index d880fb132..8e894cb2c 100644 --- a/web-next/src/locales/ja-JP/messages.po +++ b/web-next/src/locales/ja-JP/messages.po @@ -14,7 +14,7 @@ msgstr "" "Plural-Forms: \n" #. placeholder {0}: article.replies?.edges.length ?? 0 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:991 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:992 msgid "{0, plural, one {# comment} other {# comments}}" msgstr "{0, plural, other {#コメント}}" @@ -44,11 +44,16 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {#フォロー}}" #. placeholder {0}: link.invitationsLeft -#: src/routes/(root)/[handle]/invite/[id].tsx:321 -#: src/routes/(root)/[handle]/settings/invite.tsx:742 +#: src/routes/(root)/[handle]/invite/[id].tsx:326 +#: src/routes/(root)/[handle]/settings/invite.tsx:743 msgid "{0, plural, one {# invitation left} other {# invitations left}}" msgstr "{0, plural, other {残り#件の招待}}" +#. placeholder {0}: status()?.scoredLinkCount ?? 0 +#: src/routes/(root)/admin/news.tsx:350 +msgid "{0, plural, one {# link is currently in the news feed.} other {# links are currently in the news feed.}}" +msgstr "{0, plural, one {現在ニュースフィードに#件のリンクがあります。} other {現在ニュースフィードに#件のリンクがあります。}}" + #. placeholder {0}: count() #: src/routes/(root)/admin/media.tsx:172 msgid "{0, plural, one {# orphan medium can be deleted.} other {# orphan media can be deleted.}}" @@ -77,7 +82,7 @@ msgid "{0, plural, one {# voter} other {# voters}}" msgstr "{0, plural, other {投票者 #人}}" #. placeholder {0}: edge.node.tags.length - 3 -#: src/routes/(root)/[handle]/drafts/index.tsx:293 +#: src/routes/(root)/[handle]/drafts/index.tsx:295 msgid "{0, plural, one {+1 more} other {+# more}}" msgstr "{0, plural, other {他#件}}" @@ -87,7 +92,7 @@ msgid "{0, plural, one {Deleted # orphan medium.} other {Deleted # orphan media. msgstr "{0, plural, other {#件の孤立したメディアを削除しました。}}" #. placeholder {0}: account.invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:311 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, one {友達をHackers' Pubに招待しましょう。最大#人まで招待できます。} other {友達をHackers' Pubに招待しましょう。最大#人まで招待できます。}}" @@ -96,13 +101,18 @@ msgstr "{0, plural, one {友達をHackers' Pubに招待しましょう。最大# msgid "{0, plural, one {Load # more reactor} other {Load # more reactors}}" msgstr "{0, plural, one {リアクター #件をもっと読み込む} other {リアクター #件をもっと読み込む}}" +#. placeholder {0}: result.linksUpdated! +#: src/routes/(root)/admin/news.tsx:190 +msgid "{0, plural, one {Recomputed # link.} other {Recomputed # links.}}" +msgstr "{0, plural, one {#件のリンクのスコアを再計算しました。} other {#件のリンクのスコアを再計算しました。}}" + #. placeholder {0}: result.accountsAffected! #: src/routes/(root)/admin/invitations.tsx:97 msgid "{0, plural, one {Regenerated invitations for # account.} other {Regenerated invitations for # accounts.}}" msgstr "{0, plural, other {#件のアカウントに招待状を再付与しました。}}" #. placeholder {0}: account.inviteesCount.totalCount -#: src/routes/(root)/[handle]/settings/invite.tsx:438 +#: src/routes/(root)/[handle]/settings/invite.tsx:439 msgid "{0, plural, one {You have invited total # person so far.} other {You have invited total # people so far.}}" msgstr "{0, plural, other {これまでに合計#人を招待しました。}}" @@ -172,8 +182,23 @@ msgstr "{0}さん他{1}人があなたが共有したコンテンツを更新し msgid "{0} followed you" msgstr "{0}さんがあなたをフォローしました" +#. placeholder {0}: s.sourceBreakdown.bluesky +#: src/components/NewsStoryHeader.tsx:124 +msgid "{0} from Bluesky" +msgstr "Blueskyから{0}件" + +#. placeholder {0}: s.sourceBreakdown.local +#: src/components/NewsStoryHeader.tsx:118 +msgid "{0} from Hackers' Pub" +msgstr "Hackers' Pubから{0}件" + +#. placeholder {0}: s.sourceBreakdown.remote +#: src/components/NewsStoryHeader.tsx:121 +msgid "{0} from the fediverse" +msgstr "フェディバースから{0}件" + #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:361 +#: src/routes/(root)/[handle]/settings/invite.tsx:362 msgid "{0} is already a member of Hackers' Pub." msgstr "{0}さんは既にHackers' Pubのメンバーです。" @@ -229,47 +254,57 @@ msgstr "{0}さんがあなたが共有したコンテンツを更新しました #. placeholder {0}: post.actor.rawName ?? post.actor.username #. placeholder {1}: post.excerpt #. placeholder {1}: title() -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:237 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:283 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:238 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:284 msgid "{0}: {1}" msgstr "{0}:{1}" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/articles.tsx:86 -#: src/routes/(root)/[handle]/(profile)/articles.tsx:90 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:87 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:91 msgid "{0}'s articles" msgstr "{0}さんの記事" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/followers.tsx:77 -#: src/routes/(root)/[handle]/(profile)/followers.tsx:80 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:78 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:81 msgid "{0}'s followers" msgstr "{0}さんのフォロワー" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/following.tsx:77 -#: src/routes/(root)/[handle]/(profile)/following.tsx:80 +#: src/routes/(root)/[handle]/(profile)/following.tsx:78 +#: src/routes/(root)/[handle]/(profile)/following.tsx:81 msgid "{0}'s following" msgstr "{0}さんのフォロー中" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/notes.tsx:86 -#: src/routes/(root)/[handle]/(profile)/notes.tsx:90 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:87 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:91 msgid "{0}'s notes" msgstr "{0}さんの投稿" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/shares.tsx:86 -#: src/routes/(root)/[handle]/(profile)/shares.tsx:90 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:87 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:91 msgid "{0}'s shares" msgstr "{0}さんの共有" +#: src/components/NewsStoryCard.tsx:107 +#: src/components/NewsStoryHeader.tsx:54 +msgid "{count, plural, one {# opinion} other {# opinions}}" +msgstr "{count, plural, other {#件の意見}}" + +#: src/components/NewsStoryCard.tsx:51 +#: src/components/NewsStoryHeader.tsx:53 +#~ msgid "{count, plural, one {# share} other {# shares}}" +#~ msgstr "{count, plural, one {共有 #件} other {共有 #件}}" + #: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:231 #: src/routes/(root)/[handle]/[noteId]/reactions.tsx:253 #~ msgid "+{0} more reactor(s) not shown" #~ msgstr "+{0}件のリアクションは表示されていません" -#: src/routes/(root)/[handle]/settings/index.tsx:579 +#: src/routes/(root)/[handle]/settings/index.tsx:580 msgid "A name for the link that will be displayed on your profile, e.g., GitHub." msgstr "プロフィールに表示されるリンクの名前。(例:GitHub)" @@ -278,11 +313,11 @@ msgid "A sign-in link has been sent to your email. Please check your inbox (or s msgstr "ログインリンクがメールに送信されました。受信トレイ(または迷惑メールフォルダ)を確認してください。" #: src/components/admin/AdminAccountsTable.tsx:224 -#: src/components/AppSidebar.tsx:479 +#: src/components/AppSidebar.tsx:502 msgid "Account" msgstr "アカウント" -#: src/components/AppSidebar.tsx:750 +#: src/components/AppSidebar.tsx:773 #: src/routes/(root)/admin/index.tsx:95 msgid "Accounts" msgstr "アカウント" @@ -292,7 +327,8 @@ msgstr "アカウント" msgid "Actions" msgstr "操作" -#: src/routes/(root)/[handle]/settings/language.tsx:195 +#: src/routes/(root)/[handle]/settings/language.tsx:196 +#: src/routes/(root)/admin/news.tsx:421 msgid "Add" msgstr "追加" @@ -305,19 +341,23 @@ msgstr "{0}を追加" msgid "Add to sidebar" msgstr "サイドバーに追加" -#: src/components/AppSidebar.tsx:728 +#: src/routes/(root)/admin/news.tsx:421 +msgid "Adding…" +msgstr "追加中…" + +#: src/components/AppSidebar.tsx:751 msgid "Admin" msgstr "管理" -#: src/routes/(root)/[handle]/bookmarks.tsx:135 +#: src/routes/(root)/[handle]/bookmarks.tsx:136 msgid "All" msgstr "すべて" -#: src/components/NoteComposer.tsx:801 +#: src/components/NoteComposer.tsx:807 msgid "All images must finish uploading before posting" msgstr "投稿する前に、すべての画像のアップロードを完了してください。" -#: src/components/NoteComposer.tsx:809 +#: src/components/NoteComposer.tsx:815 msgid "All images require alt text" msgstr "すべての画像に代替テキストが必要です。" @@ -329,17 +369,21 @@ msgstr "すべての言語" msgid "All notifications" msgstr "すべての通知" +#: src/components/NewsList.tsx:90 +msgid "All-time" +msgstr "全期間" + #: src/components/article-composer/ArticleComposerPublishFields.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:506 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:507 msgid "Allow automatic translation by AI" msgstr "AIによる自動翻訳を許可" #. placeholder {0}: index() + 1 -#: src/components/NoteComposer.tsx:1287 +#: src/components/NoteComposer.tsx:1298 msgid "Alt text for image {0}" msgstr "画像 {0} の代替テキスト" -#: src/components/NoteComposer.tsx:1299 +#: src/components/NoteComposer.tsx:1310 msgid "Alt text for visually impaired people (required)" msgstr "視覚障碍者向けの代替テキスト(必須)" @@ -347,31 +391,31 @@ msgstr "視覚障碍者向けの代替テキスト(必須)" msgid "An error occurred during signup. Please try again." msgstr "登録中にエラーが発生しました。もう一度お試しください。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:280 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:281 msgid "An error occurred while registering your passkey." msgstr "パスキーの登録中にエラーが発生しました。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:328 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:329 msgid "An error occurred while revoking your passkey." msgstr "パスキーの取消中にエラーが発生しました。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:143 +#: src/routes/(root)/[handle]/settings/preferences.tsx:144 msgid "An error occurred while saving your preferences. Please try again, or contact support if the problem persists." msgstr "環境設定の保存中にエラーが発生しました。再度お試しいただくか、問題が解決しない場合はサポートにお問い合わせください。" -#: src/routes/(root)/[handle]/settings/language.tsx:162 +#: src/routes/(root)/[handle]/settings/language.tsx:163 msgid "An error occurred while saving your preferred languages. Please try again, or contact support if the problem persists." msgstr "言語設定の保存中にエラーが発生しました。再度お試しいただくか、問題が解決しない場合はサポートにお問い合わせください。" -#: src/routes/(root)/[handle]/settings/index.tsx:296 -#: src/routes/(root)/[handle]/settings/index.tsx:337 +#: src/routes/(root)/[handle]/settings/index.tsx:297 +#: src/routes/(root)/[handle]/settings/index.tsx:338 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "設定の保存中にエラーが発生しました。再度お試しいただくか、問題が解決しない場合はサポートにお問い合わせください。" -#: src/routes/(root)/[handle]/invite/[id].tsx:198 -#: src/routes/(root)/[handle]/settings/invite.tsx:270 -#: src/routes/(root)/[handle]/settings/invite.tsx:551 -#: src/routes/(root)/[handle]/settings/invite.tsx:601 +#: src/routes/(root)/[handle]/invite/[id].tsx:203 +#: src/routes/(root)/[handle]/settings/invite.tsx:271 +#: src/routes/(root)/[handle]/settings/invite.tsx:552 +#: src/routes/(root)/[handle]/settings/invite.tsx:602 msgid "An unexpected error occurred. Please try again later." msgstr "予期しないエラーが発生しました。後でもう一度お試しください。" @@ -386,7 +430,7 @@ msgstr "誰でも引用できます" msgid "Are you sure you want to block {0} ({1})? They won't be able to follow you or see your posts." msgstr "{0}さん({1})をブロックしますか?このユーザーはあなたをフォローしたり、あなたのコンテンツを見たりできなくなります。" -#: src/routes/(root)/[handle]/drafts/index.tsx:156 +#: src/routes/(root)/[handle]/drafts/index.tsx:157 msgid "Are you sure you want to delete \"{draftTitle}\"? This action cannot be undone." msgstr "「{draftTitle}」を削除してもよろしいですか?この操作は元に戻せません。" @@ -395,7 +439,7 @@ msgid "Are you sure you want to delete this draft? This action cannot be undone. msgstr "この下書きを削除してもよろしいですか?この操作は元に戻せません。" #. placeholder {0}: passkeyToRevoke()?.name -#: src/routes/(root)/[handle]/settings/passkeys.tsx:517 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:518 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." msgstr "パスキー{0}を取り消しますか?このパスキーを使用してアカウントにログインできなくなります。" @@ -405,8 +449,8 @@ msgstr "パスキー{0}を取り消しますか?このパスキーを使用し msgid "Are you sure you want to unblock {0} ({1})? They will be able to follow you and see your posts." msgstr "{0}さん({1})のブロックを解除しますか?このユーザーはあなたをフォローしたり、あなたのコンテンツを見たりできるようになります。" -#: src/routes/(root)/[handle]/drafts/index.tsx:243 #: src/routes/(root)/[handle]/drafts/index.tsx:245 +#: src/routes/(root)/[handle]/drafts/index.tsx:247 msgid "Article drafts" msgstr "記事の下書き" @@ -414,7 +458,7 @@ msgstr "記事の下書き" msgid "Article published" msgstr "記事を公開しました" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:333 msgid "Article updated" msgstr "記事を更新しました" @@ -423,21 +467,21 @@ msgid "article-url-slug" msgstr "記事URLスラッグ" #: src/components/ProfileTabs.tsx:51 -#: src/routes/(root)/[handle]/bookmarks.tsx:136 +#: src/routes/(root)/[handle]/bookmarks.tsx:137 msgid "Articles" msgstr "記事" -#: src/components/AppSidebar.tsx:308 +#: src/components/AppSidebar.tsx:331 msgid "Articles only" msgstr "記事のみ" #. placeholder {0}: "CHANGED" -#: src/routes/(root)/[handle]/settings/index.tsx:454 +#: src/routes/(root)/[handle]/settings/index.tsx:455 msgid "As you have already changed it {0}, you can't change it again." msgstr "すでに{0}に変更済みのため、再度変更することはできません。" -#: src/components/NoteComposer.tsx:1173 -#: src/components/NoteComposer.tsx:1174 +#: src/components/NoteComposer.tsx:1184 +#: src/components/NoteComposer.tsx:1185 msgid "Attach image" msgstr "画像を添付" @@ -445,20 +489,20 @@ msgstr "画像を添付" msgid "Authenticating…" msgstr "認証中…" -#: src/components/NoteComposer.tsx:1318 +#: src/components/NoteComposer.tsx:1329 msgid "Auto-fill" msgstr "自動入力" -#: src/components/NoteComposer.tsx:1311 -#: src/components/NoteComposer.tsx:1312 +#: src/components/NoteComposer.tsx:1322 +#: src/components/NoteComposer.tsx:1323 msgid "Auto-fill alt text" msgstr "代替テキストを自動入力" -#: src/routes/(root)/[handle]/settings/index.tsx:351 +#: src/routes/(root)/[handle]/settings/index.tsx:352 msgid "Avatar" msgstr "アイコン" -#: src/routes/(root)/[handle]/settings/index.tsx:483 +#: src/routes/(root)/[handle]/settings/index.tsx:484 #: src/routes/(root)/sign/up/[token].tsx:403 msgid "Bio" msgstr "自己紹介" @@ -477,11 +521,11 @@ msgstr "ブロック" msgid "Block user?" msgstr "ユーザーをブロックしますか?" -#: src/routes/(root)/[handle]/settings/blocks.tsx:98 +#: src/routes/(root)/[handle]/settings/blocks.tsx:99 msgid "Blocked accounts" msgstr "ブロックしたアカウント" -#: src/routes/(root)/[handle]/settings/blocks.tsx:100 +#: src/routes/(root)/[handle]/settings/blocks.tsx:101 msgid "Blocked accounts cannot follow you or see your posts. Unlike muting, blocking is federated to the blocked account's instance." msgstr "ブロックしたアカウントは、あなたをフォローしたり、あなたのコンテンツを見たりできません。ミュートとは異なり、ブロックはブロックした相手のインスタンスにも連合されます。" @@ -494,8 +538,8 @@ msgstr "太字" msgid "Bookmark" msgstr "ブックマーク" -#: src/components/AppSidebar.tsx:618 -#: src/routes/(root)/[handle]/bookmarks.tsx:125 +#: src/components/AppSidebar.tsx:641 +#: src/routes/(root)/[handle]/bookmarks.tsx:126 msgid "Bookmarks" msgstr "ブックマーク" @@ -524,24 +568,33 @@ msgstr "ブラウザー通知を無効にしました" msgid "Browser notifications enabled" msgstr "ブラウザー通知を有効にしました" +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:492 +msgid "Buried" +msgstr "埋没済み" + +#: src/components/NewsStoryCard.tsx:258 +msgid "Bury" +msgstr "埋没" + #: src/components/article-composer/ArticleComposerActions.tsx:48 -#: src/components/NoteComposer.tsx:1346 -#: src/components/NoteComposer.tsx:1347 -#: src/components/NoteComposer.tsx:1390 +#: src/components/NoteComposer.tsx:1357 +#: src/components/NoteComposer.tsx:1358 +#: src/components/NoteComposer.tsx:1401 #: src/components/PostActionMenu.tsx:367 #: src/components/ProfileActionMenu.tsx:401 #: src/components/ProfileActionMenu.tsx:402 #: src/components/QuotedNoteCard.tsx:248 #: src/components/RemoteFollowButton.tsx:218 #: src/components/RemoteFollowButton.tsx:280 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:522 -#: src/routes/(root)/[handle]/settings/index.tsx:398 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:521 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:399 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:522 #: src/routes/(root)/authorize_interaction.tsx:273 msgid "Cancel" msgstr "キャンセル" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:347 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 msgid "Cannot change the language because translations already exist" msgstr "翻訳が既に存在するため、言語を変更できません" @@ -549,11 +602,11 @@ msgstr "翻訳が既に存在するため、言語を変更できません" msgid "Check again" msgstr "再確認" -#: src/routes/(root)/[handle]/invite/[id].tsx:244 +#: src/routes/(root)/[handle]/invite/[id].tsx:249 msgid "Check your email" msgstr "メールを確認してください" -#: src/routes/(root)/[handle]/invite/[id].tsx:246 +#: src/routes/(root)/[handle]/invite/[id].tsx:251 msgid "Check your email to complete sign-up. We've sent a verification link to your email address." msgstr "登録を完了するにはメールを確認してください。確認リンクをメールアドレスに送信しました。" @@ -561,7 +614,7 @@ msgstr "登録を完了するにはメールを確認してください。確認 msgid "Checking…" msgstr "確認中…" -#: src/routes/(root)/[handle]/settings/invite.tsx:386 +#: src/routes/(root)/[handle]/settings/invite.tsx:387 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "招待する友達の使用言語を選択してください。この言語は招待状にのみ使用されます。" @@ -569,20 +622,25 @@ msgstr "招待する友達の使用言語を選択してください。この言 msgid "Choose whether push notifications may include post excerpts. Generic notification text is used when previews are hidden." msgstr "プッシュ通知にコンテンツの抜粋を含めるかを選択します。プレビューを非表示にすると、汎用の通知文が使われます。" -#: src/routes/(root)/[handle]/invite/[id].tsx:395 +#: src/routes/(root)/[handle]/invite/[id].tsx:400 msgid "Choose your preferred language for the verification email." msgstr "確認メールの言語を選択してください。" #: src/components/admin/AdminAccountsTable.tsx:213 +#: src/routes/(root)/admin/news.tsx:501 msgid "Clear" msgstr "クリア" +#: src/components/NewsStoryCard.tsx:264 +msgid "Clear penalty" +msgstr "ペナルティを解除" + #: src/components/WebPushNotificationSettings.tsx:395 msgid "Clicking a notification opens your notifications page." msgstr "通知をクリックすると通知ページが開きます。" #: src/components/ImageLightbox.tsx:74 -#: src/routes/(root)/[handle]/settings/invite.tsx:663 +#: src/routes/(root)/[handle]/settings/invite.tsx:664 msgid "Close" msgstr "閉じる" @@ -594,7 +652,7 @@ msgstr "終了" msgid "Code" msgstr "コード" -#: src/components/AppSidebar.tsx:977 +#: src/components/AppSidebar.tsx:1023 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/sign/up/[token].tsx:458 msgid "Code of conduct" @@ -604,18 +662,18 @@ msgstr "行動規範" #~ msgid "Comments ({0})" #~ msgstr "コメント({0})" -#: src/components/AppSidebar.tsx:819 +#: src/components/AppSidebar.tsx:865 #: src/components/FloatingComposeButton.tsx:61 msgid "Compose" msgstr "作成" #: src/components/article-composer/ArticleComposerForm.tsx:65 -#: src/components/NoteComposer.tsx:1122 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:392 +#: src/components/NoteComposer.tsx:1133 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:393 msgid "Content" msgstr "内容" -#: src/components/NoteComposer.tsx:791 +#: src/components/NoteComposer.tsx:797 msgid "Content cannot be empty" msgstr "内容を入力してください" @@ -627,15 +685,15 @@ msgstr "ブラウザで続ける" msgid "Controls who can quote this article on their timeline." msgstr "タイムラインでこの記事を引用できる相手を設定します。" -#: src/routes/(root)/[handle]/settings/invite.tsx:611 +#: src/routes/(root)/[handle]/settings/invite.tsx:612 msgid "Copied" msgstr "コピーしました" -#: src/routes/(root)/[handle]/settings/invite.tsx:705 +#: src/routes/(root)/[handle]/settings/invite.tsx:706 msgid "Copy" msgstr "コピー" -#: src/routes/(root)/[handle]/settings/invite.tsx:618 +#: src/routes/(root)/[handle]/settings/invite.tsx:619 msgid "Could not copy the link to the clipboard." msgstr "クリップボードにリンクをコピーできませんでした。" @@ -655,23 +713,23 @@ msgstr "引用を取り消せませんでした" msgid "Could not vote on this poll" msgstr "このアンケートに投票できませんでした" -#: src/components/AppSidebar.tsx:865 +#: src/components/AppSidebar.tsx:911 #: src/components/FloatingComposeButton.tsx:123 msgid "Create article" msgstr "記事を作成" -#: src/routes/(root)/[handle]/settings/invite.tsx:834 +#: src/routes/(root)/[handle]/settings/invite.tsx:835 msgid "Create invitation link" msgstr "招待リンクを作成" -#: src/components/AppSidebar.tsx:841 +#: src/components/AppSidebar.tsx:887 #: src/components/FloatingComposeButton.tsx:99 -#: src/components/NoteComposeModal.tsx:72 -#: src/components/NoteComposer.tsx:1407 +#: src/components/NoteComposeModal.tsx:73 +#: src/components/NoteComposer.tsx:1418 msgid "Create note" msgstr "投稿を作成" -#: src/routes/(root)/[handle]/settings/invite.tsx:628 +#: src/routes/(root)/[handle]/settings/invite.tsx:629 msgid "Create shareable invitation links. Each link can be used multiple times until the invitation count runs out or the link expires." msgstr "共有可能な招待リンクを作成できます。各リンクは、招待数がなくなるかリンクの有効期限が切れるまで複数回使用できます。" @@ -680,7 +738,7 @@ msgid "Created" msgstr "作成日" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:426 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:427 msgid "Created {0}" msgstr "{0}に作成" @@ -688,16 +746,16 @@ msgstr "{0}に作成" msgid "Creating account…" msgstr "アカウントを作成中…" -#: src/components/NoteComposer.tsx:1408 -#: src/routes/(root)/[handle]/settings/invite.tsx:833 +#: src/components/NoteComposer.tsx:1419 +#: src/routes/(root)/[handle]/settings/invite.tsx:834 msgid "Creating…" msgstr "作成中…" -#: src/routes/(root)/[handle]/settings/index.tsx:405 +#: src/routes/(root)/[handle]/settings/index.tsx:406 msgid "Crop" msgstr "切り抜き" -#: src/routes/(root)/[handle]/settings/index.tsx:378 +#: src/routes/(root)/[handle]/settings/index.tsx:379 msgid "Crop your new avatar" msgstr "新しいアイコンを切り抜く" @@ -711,22 +769,22 @@ msgstr "カットオフ:" msgid "CW" msgstr "CW" -#: src/routes/(root)/[handle]/settings/preferences.tsx:192 +#: src/routes/(root)/[handle]/settings/preferences.tsx:193 msgid "Default note privacy" msgstr "投稿のデフォルト公開範囲" -#: src/routes/(root)/[handle]/settings/preferences.tsx:217 +#: src/routes/(root)/[handle]/settings/preferences.tsx:218 msgid "Default quote permission" msgstr "引用のデフォルト許可設定" -#: src/routes/(root)/[handle]/settings/preferences.tsx:204 +#: src/routes/(root)/[handle]/settings/preferences.tsx:205 msgid "Default share privacy" msgstr "共有のデフォルト公開範囲" #: src/components/PostActionMenu.tsx:353 #: src/components/PostActionMenu.tsx:373 -#: src/routes/(root)/[handle]/drafts/index.tsx:320 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/drafts/index.tsx:322 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 msgid "Delete" msgstr "削除" @@ -744,11 +802,20 @@ msgid "Delete post?" msgstr "コンテンツを削除しますか?" #: src/components/article-composer/ArticleComposerActions.tsx:21 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 #: src/routes/(root)/admin/media.tsx:187 msgid "Deleting…" msgstr "削除中…" +#: src/components/NewsStoryCard.tsx:252 +msgid "Demote" +msgstr "降格" + +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:493 +msgid "Demoted" +msgstr "降格済み" + #: src/components/WebPushNotificationSettings.tsx:431 msgid "Disable" msgstr "無効化" @@ -757,11 +824,11 @@ msgstr "無効化" msgid "Disabling…" msgstr "無効化中…" -#: src/components/NoteComposeModal.tsx:137 +#: src/components/NoteComposeModal.tsx:138 msgid "Discard" msgstr "破棄" -#: src/components/NoteComposeModal.tsx:126 +#: src/components/NoteComposeModal.tsx:127 msgid "Discard draft?" msgstr "下書きを破棄しますか?" @@ -769,11 +836,15 @@ msgstr "下書きを破棄しますか?" msgid "Discard unsaved changes - are you sure?" msgstr "未保存の変更を破棄してもよろしいですか?" +#: src/components/NewsStoryCard.tsx:145 +#~ msgid "Discussion" +#~ msgstr "議論" + #: src/components/WebPushPromptBanner.tsx:269 msgid "Dismiss" msgstr "閉じる" -#: src/routes/(root)/[handle]/settings/index.tsx:467 +#: src/routes/(root)/[handle]/settings/index.tsx:468 #: src/routes/(root)/sign/up/[token].tsx:377 msgid "Display name" msgstr "名前" @@ -782,12 +853,12 @@ msgstr "名前" msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "アカウントが必要ですか?Hackers' Pubは招待制ですので、友人に招待をお願いしてください。" -#: src/components/NoteComposer.tsx:742 +#: src/components/NoteComposer.tsx:748 msgid "Do you want to quote this link?" msgstr "このリンクを引用しますか?" #: src/components/article-composer/ArticleComposerContext.tsx:450 -#: src/routes/(root)/[handle]/drafts/index.tsx:175 +#: src/routes/(root)/[handle]/drafts/index.tsx:176 msgid "Draft deleted" msgstr "下書きを削除しました" @@ -803,7 +874,7 @@ msgstr "下書きが見つかりません" msgid "Draft saved" msgstr "下書きを保存しました" -#: src/routes/(root)/[handle]/settings/index.tsx:381 +#: src/routes/(root)/[handle]/settings/index.tsx:382 msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "保持したい領域をドラッグして選択し、「切り抜き」をクリックしてアイコンを更新してください。" @@ -812,25 +883,25 @@ msgid "e.g., @user@mastodon.social" msgstr "例: @user@mastodon.social" #: src/components/PostActionMenu.tsx:328 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:695 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:696 msgid "Edit" msgstr "編集" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:374 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:375 msgid "Edit article" msgstr "記事を編集" -#: src/routes/(root)/[handle]/drafts/[id].tsx:73 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/[id].tsx:75 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "Edit draft" msgstr "下書きを編集" -#: src/components/NoteComposeModal.tsx:69 +#: src/components/NoteComposeModal.tsx:70 msgid "Edit note" msgstr "投稿を編集" -#: src/routes/(root)/[handle]/invite/[id].tsx:366 -#: src/routes/(root)/[handle]/settings/invite.tsx:334 +#: src/routes/(root)/[handle]/invite/[id].tsx:371 +#: src/routes/(root)/[handle]/settings/invite.tsx:335 #: src/routes/(root)/sign/up/[token].tsx:311 msgid "Email address" msgstr "メールアドレス" @@ -857,7 +928,7 @@ msgstr "終了" msgid "Ends" msgstr "終了予定" -#: src/routes/(root)/[handle]/invite/[id].tsx:279 +#: src/routes/(root)/[handle]/invite/[id].tsx:284 msgid "Enter your email address below to get started." msgstr "以下にメールアドレスを入力して始めましょう。" @@ -879,47 +950,63 @@ msgstr "以下にメールアドレスまたはユーザー名を入力してロ #: src/components/article-composer/ArticleComposerContext.tsx:466 #: src/components/article-composer/ArticleComposerContext.tsx:474 #: src/components/article-composer/ArticleComposerForm.tsx:35 -#: src/components/NoteComposer.tsx:601 -#: src/components/NoteComposer.tsx:648 -#: src/components/NoteComposer.tsx:790 -#: src/components/NoteComposer.tsx:800 -#: src/components/NoteComposer.tsx:808 -#: src/components/NoteComposer.tsx:842 -#: src/components/NoteComposer.tsx:850 -#: src/components/NoteComposer.tsx:858 -#: src/components/NoteComposer.tsx:892 -#: src/components/NoteComposer.tsx:900 -#: src/components/NoteComposer.tsx:908 -#: src/components/NoteComposer.tsx:965 +#: src/components/NoteComposer.tsx:607 +#: src/components/NoteComposer.tsx:654 +#: src/components/NoteComposer.tsx:796 +#: src/components/NoteComposer.tsx:806 +#: src/components/NoteComposer.tsx:814 +#: src/components/NoteComposer.tsx:848 +#: src/components/NoteComposer.tsx:856 +#: src/components/NoteComposer.tsx:864 +#: src/components/NoteComposer.tsx:903 +#: src/components/NoteComposer.tsx:911 +#: src/components/NoteComposer.tsx:919 +#: src/components/NoteComposer.tsx:976 #: src/components/QuotedNoteCard.tsx:270 #: src/components/QuotedNoteCard.tsx:278 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:257 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:345 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:355 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:364 -#: src/routes/(root)/[handle]/drafts/index.tsx:182 -#: src/routes/(root)/[handle]/drafts/index.tsx:191 -#: src/routes/(root)/[handle]/drafts/index.tsx:199 -#: src/routes/(root)/[handle]/invite/[id].tsx:196 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:258 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:346 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/drafts/index.tsx:183 +#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:200 +#: src/routes/(root)/[handle]/invite/[id].tsx:201 #: src/routes/(root)/sign/up/[token].tsx:269 msgid "Error" msgstr "エラー" +#: src/routes/(root)/admin/news.tsx:382 +msgid "Excluded URL patterns" +msgstr "除外URLパターン" + +#: src/routes/(root)/admin/news.tsx:233 +msgid "Exclusion pattern added." +msgstr "除外パターンを追加しました。" + +#: src/routes/(root)/admin/news.tsx:268 +msgid "Exclusion pattern removed." +msgstr "除外パターンを削除しました。" + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/invite/[id].tsx:334 -#: src/routes/(root)/[handle]/settings/invite.tsx:759 +#: src/routes/(root)/[handle]/invite/[id].tsx:339 +#: src/routes/(root)/[handle]/settings/invite.tsx:760 msgid "Expires {0}" msgstr "{0}に期限切れ" -#: src/routes/(root)/[handle]/settings/invite.tsx:806 +#: src/routes/(root)/[handle]/settings/invite.tsx:807 msgid "Expiry" msgstr "有効期限" -#: src/routes/(root)/[handle]/settings/invite.tsx:391 -#: src/routes/(root)/[handle]/settings/invite.tsx:796 +#: src/routes/(root)/[handle]/settings/invite.tsx:392 +#: src/routes/(root)/[handle]/settings/invite.tsx:797 msgid "Extra message" msgstr "追加メッセージ" +#: src/routes/(root)/admin/news.tsx:252 +msgid "Failed to add exclusion pattern." +msgstr "除外パターンの追加に失敗しました。" + #: src/components/HashtagActionBar.tsx:192 #: src/components/HashtagActionBar.tsx:199 msgid "Failed to add to sidebar" @@ -935,17 +1022,21 @@ msgstr "このユーザーをブロックできませんでした" msgid "Failed to bookmark" msgstr "ブックマークに失敗しました" -#: src/routes/(root)/[handle]/settings/invite.tsx:617 +#: src/routes/(root)/admin/news.tsx:310 +msgid "Failed to clear penalty." +msgstr "ペナルティの解除に失敗しました。" + +#: src/routes/(root)/[handle]/settings/invite.tsx:618 msgid "Failed to copy" msgstr "コピーに失敗しました" -#: src/routes/(root)/[handle]/settings/invite.tsx:539 -#: src/routes/(root)/[handle]/settings/invite.tsx:549 +#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:550 msgid "Failed to create invitation link" msgstr "招待リンクの作成に失敗しました" -#: src/routes/(root)/[handle]/settings/invite.tsx:588 -#: src/routes/(root)/[handle]/settings/invite.tsx:599 +#: src/routes/(root)/[handle]/settings/invite.tsx:589 +#: src/routes/(root)/[handle]/settings/invite.tsx:600 msgid "Failed to delete invitation link" msgstr "招待リンクの削除に失敗しました" @@ -979,7 +1070,7 @@ msgstr "ブラウザー通知を有効にできませんでした" msgid "Failed to follow" msgstr "フォローに失敗しました" -#: src/components/NoteComposer.tsx:966 +#: src/components/NoteComposer.tsx:977 msgid "Failed to generate alt text" msgstr "代替テキストの生成に失敗しました" @@ -995,7 +1086,7 @@ msgstr "記事の読み込みに失敗しました。クリックして再試行 msgid "Failed to load more bookmarks; click to retry" msgstr "ブックマークの読み込みに失敗しました。クリックして再試行してください" -#: src/routes/(root)/[handle]/drafts/index.tsx:345 +#: src/routes/(root)/[handle]/drafts/index.tsx:347 msgid "Failed to load more drafts; click to retry" msgstr "下書きの読み込みに失敗しました。クリックして再試行してください" @@ -1007,7 +1098,7 @@ msgstr "フォロワーの読み込みに失敗しました。クリックして msgid "Failed to load more following; click to retry" msgstr "フォロー中の読み込みに失敗しました。クリックして再試行してください" -#: src/routes/(root)/[handle]/settings/invite.tsx:947 +#: src/routes/(root)/[handle]/settings/invite.tsx:948 msgid "Failed to load more invitees; click to retry" msgstr "招待された人々の読み込みに失敗しました。クリックして再試行" @@ -1019,7 +1110,7 @@ msgstr "投稿の読み込みに失敗しました。クリックして再試行 msgid "Failed to load more notifications; click to retry" msgstr "通知の読み込みに失敗しました。クリックして再試行してください" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:495 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 msgid "Failed to load more passkeys; click to retry" msgstr "パスキーの読み込みに失敗しました。クリックして再試行してください" @@ -1031,8 +1122,8 @@ msgstr "パスキーの読み込みに失敗しました。クリックして再 msgid "Failed to load more posts; click to retry" msgstr "コンテンツの読み込みに失敗しました。クリックして再試行してください" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:194 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:201 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:195 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:202 msgid "Failed to load more quotes; click to retry" msgstr "引用の追加読み込みに失敗しました。再試行するにはクリックしてください" @@ -1040,28 +1131,31 @@ msgstr "引用の追加読み込みに失敗しました。再試行するには msgid "Failed to load more reactors; click to retry" msgstr "リアクターの追加読み込みに失敗しました。再試行するにはクリックしてください" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:670 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:671 msgid "Failed to load more replies; click to retry" msgstr "返信の読み込みに失敗しました。クリックして再試行してください" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:204 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:213 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:205 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:214 msgid "Failed to load more shares; click to retry" msgstr "共有の追加読み込みに失敗しました。再試行するにはクリックしてください" -#: src/components/BlockedAccountsList.tsx:199 -#: src/components/MutedAccountsList.tsx:196 +#: src/components/AccountListBase.tsx:112 msgid "Failed to load more; click to retry" msgstr "読み込みに失敗しました。クリックして再試行してください" -#: src/components/NoteComposer.tsx:1014 +#: src/components/NoteComposer.tsx:1025 msgid "Failed to load post" msgstr "コンテンツの読み込みに失敗しました" -#: src/components/NoteComposer.tsx:1065 +#: src/components/NoteComposer.tsx:1076 msgid "Failed to load quoted post" msgstr "引用コンテンツの読み込みに失敗しました" +#: src/components/NewsDiscussionThread.tsx:408 +msgid "Failed to load replies; click to retry" +msgstr "返信の読み込みに失敗しました。クリックして再試行してください" + #: src/components/RemoteFollowButton.tsx:126 msgid "Failed to look up user." msgstr "ユーザーの検索に失敗しました。" @@ -1087,11 +1181,15 @@ msgstr "コンテンツを固定できませんでした" msgid "Failed to react" msgstr "リアクションに失敗しました" +#: src/routes/(root)/admin/news.tsx:212 +msgid "Failed to recompute news scores." +msgstr "ニュースのスコアを再計算できませんでした。" + #: src/routes/(root)/admin/invitations.tsx:125 msgid "Failed to regenerate invitations." msgstr "招待状の再付与に失敗しました。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:277 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:278 msgid "Failed to register passkey" msgstr "パスキーの登録に失敗しました" @@ -1100,40 +1198,44 @@ msgstr "パスキーの登録に失敗しました" msgid "Failed to remove bookmark" msgstr "ブックマークの削除に失敗しました" +#: src/routes/(root)/admin/news.tsx:281 +msgid "Failed to remove exclusion pattern." +msgstr "除外パターンの削除に失敗しました。" + #: src/components/HashtagActionBar.tsx:212 #: src/components/HashtagActionBar.tsx:219 msgid "Failed to remove from sidebar" msgstr "サイドバーからの削除に失敗しました" #: src/components/MarkdownEditor.tsx:192 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:461 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 msgid "Failed to render preview" msgstr "プレビューの表示に失敗しました" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:319 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:325 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:320 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "Failed to revoke passkey" msgstr "パスキーの取り消しに失敗しました" -#: src/routes/(root)/[handle]/settings/language.tsx:160 +#: src/routes/(root)/[handle]/settings/language.tsx:161 msgid "Failed to save language preferences" msgstr "言語設定の保存に失敗しました" -#: src/routes/(root)/[handle]/settings/preferences.tsx:141 +#: src/routes/(root)/[handle]/settings/preferences.tsx:142 msgid "Failed to save preferences" msgstr "環境設定の保存に失敗しました" -#: src/routes/(root)/[handle]/settings/index.tsx:293 -#: src/routes/(root)/[handle]/settings/index.tsx:335 +#: src/routes/(root)/[handle]/settings/index.tsx:294 +#: src/routes/(root)/[handle]/settings/index.tsx:336 msgid "Failed to save settings" msgstr "設定の保存に失敗しました" -#: src/routes/(root)/[handle]/invite/[id].tsx:185 +#: src/routes/(root)/[handle]/invite/[id].tsx:190 msgid "Failed to send email" msgstr "メールの送信に失敗しました" -#: src/routes/(root)/[handle]/settings/invite.tsx:248 -#: src/routes/(root)/[handle]/settings/invite.tsx:268 +#: src/routes/(root)/[handle]/settings/invite.tsx:249 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "Failed to send invitation" msgstr "招待状の送信に失敗しました" @@ -1147,8 +1249,8 @@ msgstr "コンテンツの共有に失敗しました" msgid "Failed to sign out: {0}" msgstr "ログアウトに失敗:{0}" -#: src/components/BlockedAccountsList.tsx:98 -#: src/components/BlockedAccountsList.tsx:104 +#: src/components/BlockedAccountsList.tsx:96 +#: src/components/BlockedAccountsList.tsx:102 #: src/components/ProfileActionMenu.tsx:258 #: src/components/ProfileActionMenu.tsx:264 msgid "Failed to unblock this user" @@ -1160,8 +1262,8 @@ msgstr "このユーザーのブロックを解除できませんでした" msgid "Failed to unfollow" msgstr "フォロー解除に失敗しました" -#: src/components/MutedAccountsList.tsx:97 -#: src/components/MutedAccountsList.tsx:101 +#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:99 #: src/components/ProfileActionMenu.tsx:300 #: src/components/ProfileActionMenu.tsx:308 msgid "Failed to unmute this user" @@ -1177,11 +1279,16 @@ msgstr "コンテンツの固定を解除できませんでした" msgid "Failed to unshare post" msgstr "共有の取り消しに失敗しました" +#: src/components/NewsStoryCard.tsx:87 +#: src/components/NewsStoryCard.tsx:91 +msgid "Failed to update penalty." +msgstr "ペナルティの更新に失敗しました。" + #: src/components/WebPushNotificationSettings.tsx:354 msgid "Failed to update push notification privacy" msgstr "プッシュ通知のプライバシー設定を更新できませんでした" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:366 msgid "Failed to update the article. Please try again." msgstr "記事の更新に失敗しました。もう一度お試しください。" @@ -1190,8 +1297,8 @@ msgstr "記事の更新に失敗しました。もう一度お試しください #~ msgstr "投稿の更新に失敗しました。もう一度お試しください。" #: src/components/article-composer/ArticleComposerForm.tsx:38 -#: src/components/NoteComposer.tsx:651 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:260 +#: src/components/NoteComposer.tsx:657 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:261 msgid "Failed to upload image" msgstr "画像のアップロードに失敗しました" @@ -1199,7 +1306,7 @@ msgstr "画像のアップロードに失敗しました" msgid "Failed to vote" msgstr "投票に失敗しました" -#: src/components/AppSidebar.tsx:354 +#: src/components/AppSidebar.tsx:377 msgid "Fediverse" msgstr "フェディバース" @@ -1207,10 +1314,14 @@ msgstr "フェディバース" msgid "Fediverse handle" msgstr "フェディバースのハンドル" -#: src/components/AppSidebar.tsx:245 +#: src/components/AppSidebar.tsx:268 msgid "Feed" msgstr "フィード" +#: src/components/NewsStoryHeader.tsx:113 +msgid "First shared" +msgstr "最初の共有" + #: src/components/FollowButton.tsx:195 #: src/components/HashtagActionBar.tsx:237 msgid "Follow" @@ -1253,7 +1364,7 @@ msgstr "あなたをフォロー中" msgid "Formatting" msgstr "書式設定" -#: src/components/NoteComposer.tsx:1337 +#: src/components/NoteComposer.tsx:1348 msgid "Generating…" msgstr "生成中…" @@ -1261,14 +1372,14 @@ msgstr "生成中…" msgid "Get browser notifications" msgstr "ブラウザー通知を受け取る" -#: src/components/AppSidebar.tsx:1015 +#: src/components/AppSidebar.tsx:1061 msgid "GitHub repository" msgstr "GitHubリポジトリ" -#: src/routes/(root)/[handle]/bookmarks.tsx:108 -#: src/routes/(root)/[handle]/drafts/[id].tsx:59 -#: src/routes/(root)/[handle]/drafts/index.tsx:225 -#: src/routes/(root)/[handle]/drafts/new.tsx:69 +#: src/routes/(root)/[handle]/bookmarks.tsx:109 +#: src/routes/(root)/[handle]/drafts/[id].tsx:61 +#: src/routes/(root)/[handle]/drafts/index.tsx:227 +#: src/routes/(root)/[handle]/drafts/new.tsx:71 msgid "Go back" msgstr "戻る" @@ -1280,13 +1391,13 @@ msgstr "ホームに戻る" msgid "Go to Drafts" msgstr "下書きへ" -#: src/routes/(root)/[handle]/bookmarks.tsx:114 +#: src/routes/(root)/[handle]/bookmarks.tsx:115 msgid "Go to my bookmarks" msgstr "マイブックマークへ" -#: src/routes/(root)/[handle]/drafts/[id].tsx:64 -#: src/routes/(root)/[handle]/drafts/index.tsx:230 -#: src/routes/(root)/[handle]/drafts/new.tsx:74 +#: src/routes/(root)/[handle]/drafts/[id].tsx:66 +#: src/routes/(root)/[handle]/drafts/index.tsx:232 +#: src/routes/(root)/[handle]/drafts/new.tsx:76 msgid "Go to my drafts" msgstr "マイ下書きへ" @@ -1294,8 +1405,8 @@ msgstr "マイ下書きへ" msgid "Grants one extra invitation to the most active accounts (the top third by post count) since the last regeneration cutoff." msgstr "前回の再付与カットオフ以降、最も活発なアカウント(投稿数上位3分の1)に招待状を1通追加で付与します。" -#: src/components/AppSidebar.tsx:323 -#: src/components/AppSidebar.tsx:446 +#: src/components/AppSidebar.tsx:346 +#: src/components/AppSidebar.tsx:469 #: src/routes/(root).tsx:134 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/markdown.tsx:40 @@ -1324,6 +1435,19 @@ msgstr "Hackers' Pub:管理 · 招待" msgid "Hackers' Pub: Admin · Media" msgstr "Hackers' Pub:管理 · メディア" +#: src/routes/(root)/admin/news.tsx:320 +msgid "Hackers' Pub: Admin · News" +msgstr "Hackers' Pub:管理 · ニュース" + +#: src/routes/(root)/admin/news.tsx:124 +#~ msgid "Hackers' Pub: Admin · News scores" +#~ msgstr "Hackers' Pub:管理 · ニュースのスコア" + +#: src/routes/(root)/news/[link_id]/index.tsx:56 +#: src/routes/(root)/news/index.tsx:37 +msgid "Hackers' Pub: News" +msgstr "Hackers' Pub:ニュース" + #: src/routes/(root)/notifications.tsx:47 msgid "Hackers' Pub: Notifications" msgstr "Hackers' Pub:通知" @@ -1346,6 +1470,10 @@ msgstr "見出し3" msgid "Hide" msgstr "隠す" +#: src/routes/(root)/admin/news.tsx:384 +msgid "Hide links matching a URL pattern from the news feed list (every sort order). Their discussion pages stay reachable by direct link. Patterns use the URLPattern syntax, e.g. https://example.com/* or https://*.example.com/*." +msgstr "URLパターンに一致するリンクをニュースフィードの一覧(すべての並び順)から隠します。該当リンクの議論ページは直接URLでアクセスできます。パターンはURLPatternの構文を使います。例: https://example.com/* または https://*.example.com/*。" + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Hide preview" #~ msgstr "プレビューを隠す" @@ -1358,23 +1486,23 @@ msgstr "隠す" msgid "I have read and agree to the Code of conduct." msgstr "行動規範に同意します。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:186 +#: src/routes/(root)/[handle]/settings/preferences.tsx:187 msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "有効にすると、AIが記事の要約を生成します。無効の場合は、記事の最初の数行が要約として使用されます。" #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1013 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1014 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:548 msgid "If you have a fediverse account, you can reply to this article from your own instance. Search {0} on your instance and reply to it." msgstr "フェディバース(fediverse)アカウントをお持ちの場合、この記事に返信することができます。ご利用のインスタンスの検索バーに{0}を検索し、該当記事に返信してください。" #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:393 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:394 msgid "If you have a fediverse account, you can reply to this note from your own instance. Search {0} on your instance and reply to it." msgstr "フェディバース(fediverse)アカウントをお持ちの場合、この投稿に返信することができます。ご利用のインスタンスの検索バーに{0}を検索し、該当投稿に返信してください。" #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:471 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:472 msgid "If you have a fediverse account, you can reply to this post from your own instance. Search {0} on your instance and reply to it." msgstr "フェディバースのアカウントをお持ちの場合、自分のインスタンスからこのコンテンツに返信できます。お使いのインスタンスで{0}を検索して返信してください。" @@ -1397,42 +1525,42 @@ msgstr "無効なフェディバースのハンドルの形式です。" #: src/components/article-composer/ArticleComposerContext.tsx:321 #: src/components/article-composer/ArticleComposerContext.tsx:394 #: src/components/article-composer/ArticleComposerContext.tsx:459 -#: src/components/NoteComposer.tsx:843 -#: src/components/NoteComposer.tsx:893 -#: src/routes/(root)/[handle]/drafts/index.tsx:184 +#: src/components/NoteComposer.tsx:849 +#: src/components/NoteComposer.tsx:904 +#: src/routes/(root)/[handle]/drafts/index.tsx:185 msgid "Invalid input: {0}" msgstr "無効な入力:{0}" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:349 msgid "Invalid input: {inputPath}" msgstr "無効な入力:{inputPath}" #. placeholder {0}: link.inviter.name ?? link.inviter.username -#: src/routes/(root)/[handle]/invite/[id].tsx:237 +#: src/routes/(root)/[handle]/invite/[id].tsx:242 msgid "Invitation from {0}" msgstr "{0}からの招待" -#: src/routes/(root)/[handle]/settings/invite.tsx:379 +#: src/routes/(root)/[handle]/settings/invite.tsx:380 msgid "Invitation language" msgstr "招待状の言語" -#: src/routes/(root)/[handle]/settings/invite.tsx:531 +#: src/routes/(root)/[handle]/settings/invite.tsx:532 msgid "Invitation link created" msgstr "招待リンクを作成しました" -#: src/routes/(root)/[handle]/settings/invite.tsx:582 +#: src/routes/(root)/[handle]/settings/invite.tsx:583 msgid "Invitation link deleted" msgstr "招待リンクを削除しました" -#: src/routes/(root)/[handle]/settings/invite.tsx:626 +#: src/routes/(root)/[handle]/settings/invite.tsx:627 msgid "Invitation links" msgstr "招待リンク" -#: src/routes/(root)/[handle]/settings/invite.tsx:258 +#: src/routes/(root)/[handle]/settings/invite.tsx:259 msgid "Invitation sent" msgstr "招待状を送信しました" -#: src/components/AppSidebar.tsx:773 +#: src/components/AppSidebar.tsx:796 #: src/routes/(root)/admin/invitations.tsx:148 msgid "Invitations" msgstr "招待" @@ -1441,13 +1569,13 @@ msgstr "招待" msgid "Invitations left" msgstr "残り招待数" -#: src/components/AppSidebar.tsx:642 +#: src/components/AppSidebar.tsx:665 #: src/components/SettingsTabs.tsx:69 -#: src/routes/(root)/[handle]/settings/invite.tsx:295 +#: src/routes/(root)/[handle]/settings/invite.tsx:296 msgid "Invite" msgstr "招待" -#: src/routes/(root)/[handle]/settings/invite.tsx:304 +#: src/routes/(root)/[handle]/settings/invite.tsx:305 msgid "Invite a friend" msgstr "友達を招待" @@ -1463,23 +1591,27 @@ msgstr "招待者" msgid "Italic" msgstr "斜体" -#: src/routes/(root)/[handle]/settings/index.tsx:474 +#: src/routes/(root)/[handle]/settings/index.tsx:475 msgid "John Doe" msgstr "田中太郎" +#: src/components/NewsDiscussionComposer.tsx:41 +msgid "Join the discussion about this story." +msgstr "この話題の議論に参加しましょう。" + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/settings/invite.tsx:921 +#: src/routes/(root)/[handle]/settings/invite.tsx:922 msgid "Joined on {0}" msgstr "{0}に参加" -#: src/components/NoteComposeModal.tsx:132 +#: src/components/NoteComposeModal.tsx:133 msgid "Keep editing" msgstr "編集を続ける" #: src/components/article-composer/ArticleComposerPublishFields.tsx:53 #: src/components/LanguageList.tsx:33 #: src/components/LanguageSelect.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:489 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:490 msgid "Language" msgstr "言語" @@ -1487,7 +1619,7 @@ msgstr "言語" msgid "Language code" msgstr "言語コード" -#: src/routes/(root)/[handle]/settings/language.tsx:82 +#: src/routes/(root)/[handle]/settings/language.tsx:83 msgid "Language settings" msgstr "言語設定" @@ -1495,16 +1627,25 @@ msgstr "言語設定" msgid "Languages" msgstr "言語" +#: src/components/NewsStoryCard.tsx:205 +#: src/components/NewsStoryHeader.tsx:106 +msgid "Last active" +msgstr "最後の活動" + #: src/components/admin/AdminAccountsTable.tsx:278 msgid "Last activity" msgstr "最終アクティビティ" +#: src/routes/(root)/admin/news.tsx:360 +msgid "Last recomputed:" +msgstr "最終再計算:" + #: src/routes/(root)/admin/invitations.tsx:160 msgid "Last regenerated:" msgstr "最終再付与:" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:450 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:451 msgid "Last used {0}" msgstr "{0}に最終使用" @@ -1520,21 +1661,28 @@ msgstr "リンクの著者:" #~ msgid "Link author: " #~ msgstr "リンクの著者:" -#: src/routes/(root)/[handle]/invite/[id].tsx:255 +#: src/routes/(root)/[handle]/invite/[id].tsx:260 msgid "Link expired" msgstr "リンクの有効期限が切れています" -#: src/routes/(root)/[handle]/settings/index.tsx:560 +#: src/routes/(root)/[handle]/settings/index.tsx:561 msgid "Link name" msgstr "リンク名" +#: src/routes/(root)/admin/news.tsx:465 +msgid "Links a moderator has demoted in the popular feed. Clear a penalty to restore a link's normal ranking." +msgstr "モデレーターが人気フィードで降格させたリンクです。ペナルティを解除すると、リンクの順位が元に戻ります。" + +#: src/routes/(root)/news/index.tsx:41 +msgid "Links circulating across the fediverse, ranked by how much they are being shared and discussed." +msgstr "フェディバース全体で広がっているリンクを、共有や議論の多さに応じてランキング表示します。" + #: src/components/ui/markdown-editor.tsx:291 msgid "List" msgstr "リスト" -#: src/components/BlockedAccountsList.tsx:202 -#: src/components/MutedAccountsList.tsx:199 -#: src/routes/(root)/[handle]/drafts/index.tsx:348 +#: src/components/AccountListBase.tsx:115 +#: src/routes/(root)/[handle]/drafts/index.tsx:350 msgid "Load more" msgstr "さらに読み込む" @@ -1558,7 +1706,7 @@ msgstr "フォロワーをもっと読み込む" msgid "Load more following" msgstr "フォロー中をもっと読み込む" -#: src/routes/(root)/[handle]/settings/invite.tsx:950 +#: src/routes/(root)/[handle]/settings/invite.tsx:951 msgid "Load more invitees" msgstr "もっと招待された人々を読み込む" @@ -1570,7 +1718,7 @@ msgstr "投稿をもっと読み込む" msgid "Load more notifications" msgstr "通知をもっと読み込む" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:497 msgid "Load more passkeys" msgstr "パスキーを読み込む" @@ -1582,8 +1730,9 @@ msgstr "パスキーを読み込む" msgid "Load more posts" msgstr "コンテンツをもっと読み込む" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:197 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:204 +#: src/components/NewsDiscussionThread.tsx:378 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:205 msgid "Load more quotes" msgstr "引用をもっと読み込む" @@ -1591,15 +1740,20 @@ msgstr "引用をもっと読み込む" #~ msgid "Load more reactors (+{0})" #~ msgstr "リアクターをもっと読み込む (+{0})" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:673 +#: src/components/NewsDiscussionThread.tsx:399 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:674 msgid "Load more replies" msgstr "返信をもっと読み込む" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:207 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:216 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:208 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:217 msgid "Load more shares" msgstr "共有をもっと読み込む" +#: src/components/NewsList.tsx:139 +msgid "Load more stories" +msgstr "ニュースをもっと読み込む" + #: src/components/article-composer/ArticleComposer.tsx:31 msgid "Loading draft…" msgstr "下書きを読み込み中…" @@ -1624,7 +1778,7 @@ msgstr "フォロワーを読み込み中…" msgid "Loading more following…" msgstr "フォロー中を読み込み中…" -#: src/routes/(root)/[handle]/settings/invite.tsx:944 +#: src/routes/(root)/[handle]/settings/invite.tsx:945 msgid "Loading more invitees…" msgstr "招待された人々を読み込み中…" @@ -1636,7 +1790,7 @@ msgstr "投稿を読み込み中…" msgid "Loading more notifications" msgstr "通知を読み込み中…" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:493 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:494 msgid "Loading more passkeys…" msgstr "パスキーを読み込み中…" @@ -1648,8 +1802,8 @@ msgstr "パスキーを読み込み中…" msgid "Loading more posts…" msgstr "コンテンツを読み込み中…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:191 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:192 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:199 msgid "Loading more quotes…" msgstr "引用を読み込み中…" @@ -1657,21 +1811,28 @@ msgstr "引用を読み込み中…" msgid "Loading more reactors…" msgstr "リアクターを読み込み中…" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:667 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:668 msgid "Loading more replies…" msgstr "返信を読み込み中…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:201 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:210 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:202 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:211 msgid "Loading more shares…" msgstr "共有を読み込み中…" -#: src/components/BlockedAccountsList.tsx:196 -#: src/components/MutedAccountsList.tsx:193 +#: src/components/NewsDiscussion.tsx:79 +msgid "Loading more sharing posts…" +msgstr "共有コンテンツをもっと読み込み中…" + +#: src/components/NewsList.tsx:141 +msgid "Loading more stories…" +msgstr "ニュースを読み込み中…" + +#: src/components/AccountListBase.tsx:109 msgid "Loading more…" msgstr "さらに読み込んでいます…" -#: src/components/NoteComposer.tsx:1066 +#: src/components/NoteComposer.tsx:1077 msgid "Loading quoted post…" msgstr "引用コンテンツを読み込み中…" @@ -1679,13 +1840,13 @@ msgstr "引用コンテンツを読み込み中…" msgid "Loading search results…" msgstr "検索結果を読み込み中…" -#: src/components/NoteComposer.tsx:1015 -#: src/routes/(root)/[handle]/drafts/index.tsx:342 +#: src/components/NoteComposer.tsx:1026 +#: src/routes/(root)/[handle]/drafts/index.tsx:344 #: src/routes/(root)/sign/up/[token].tsx:465 msgid "Loading…" msgstr "読み込み中…" -#: src/routes/(root)/[handle]/settings/preferences.tsx:226 +#: src/routes/(root)/[handle]/settings/preferences.tsx:227 msgid "Locked to \"Only me\" because your default note privacy restricts visibility." msgstr "デフォルトの投稿公開範囲が制限されているため、「自分のみ」に固定されています。" @@ -1702,12 +1863,12 @@ msgid "Markdown guide" msgstr "Markdown ガイド" #: src/components/article-composer/ArticleComposerForm.tsx:90 -#: src/components/NoteComposer.tsx:1232 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:417 +#: src/components/NoteComposer.tsx:1243 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:418 msgid "Markdown supported" msgstr "Markdown対応" -#: src/components/AppSidebar.tsx:796 +#: src/components/AppSidebar.tsx:819 #: src/routes/(root)/admin/media.tsx:152 msgid "Media" msgstr "メディア" @@ -1718,6 +1879,11 @@ msgstr "メディア" msgid "Mentioned only" msgstr "メンションされたユーザーのみ" +#: src/components/NewsStoryCard.tsx:225 +#: src/components/NewsStoryCard.tsx:243 +msgid "Moderate" +msgstr "管理" + #: src/components/PostEngagementBar.tsx:436 #: src/components/PostEngagementBar.tsx:437 msgid "More engagement views" @@ -1747,7 +1913,7 @@ msgstr "複数選択" msgid "Mute" msgstr "ミュート" -#: src/routes/(root)/[handle]/settings/blocks.tsx:84 +#: src/routes/(root)/[handle]/settings/blocks.tsx:85 msgid "Muted accounts" msgstr "ミュートしたアカウント" @@ -1755,7 +1921,7 @@ msgstr "ミュートしたアカウント" #~ msgid "Muted accounts are hidden from your feeds and stop notifying you, but you can still visit their profiles. Muting is private and is never federated." #~ msgstr "ミュートしたアカウントはタイムラインに表示されず、通知も届きません。ただし、プロフィールは引き続き閲覧できます。ミュートは非公開で、連合されることはありません。" -#: src/routes/(root)/[handle]/settings/blocks.tsx:86 +#: src/routes/(root)/[handle]/settings/blocks.tsx:87 msgid "Muted accounts are hidden from your feeds and stop notifying you, except for replies and mentions from accounts you follow. You can still visit their profiles, and muting is private and never federated." msgstr "ミュートしたアカウントはタイムラインに表示されず、通知も届きません。ただし、フォローしているアカウントからの返信やメンションは通知されます。プロフィールは引き続き閲覧でき、ミュートは非公開で連合されることはありません。" @@ -1763,11 +1929,11 @@ msgstr "ミュートしたアカウントはタイムラインに表示されず msgid "Mutes & blocks" msgstr "ミュートとブロック" -#: src/routes/(root)/[handle]/settings/blocks.tsx:77 +#: src/routes/(root)/[handle]/settings/blocks.tsx:78 msgid "Mutes and blocks" msgstr "ミュートとブロック" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:375 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:376 msgid "My passkey" msgstr "私のパスキー" @@ -1783,21 +1949,25 @@ msgstr "名前が長すぎます。最大50文字です。" msgid "Native name" msgstr "現地名" +#: src/routes/(root)/admin/news.tsx:365 +msgid "never" +msgstr "なし" + #: src/routes/(root)/admin/invitations.tsx:167 msgid "Never" msgstr "なし" -#: src/routes/(root)/[handle]/settings/invite.tsx:755 -#: src/routes/(root)/[handle]/settings/invite.tsx:821 +#: src/routes/(root)/[handle]/settings/invite.tsx:756 +#: src/routes/(root)/[handle]/settings/invite.tsx:822 msgid "Never expires" msgstr "有効期限なし" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:446 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:447 msgid "Never used" msgstr "使用履歴なし" -#: src/routes/(root)/[handle]/drafts/index.tsx:251 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/index.tsx:253 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "New article" msgstr "新しい記事" @@ -1810,6 +1980,22 @@ msgstr "Hackers' Pubを開いていなくても新しい通知が表示される msgid "New posts available — click to load" msgstr "新しいコンテンツがあります — クリックして読み込む" +#: src/components/NewsList.tsx:89 +msgid "Newest" +msgstr "最新" + +#: src/components/AppSidebar.tsx:244 +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:337 +#: src/routes/(root)/news/index.tsx:39 +msgid "News" +msgstr "ニュース" + +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:139 +#~ msgid "News scores" +#~ msgstr "ニュースのスコア" + #: src/components/ImageLightbox.tsx:126 msgid "Next image" msgstr "次の画像" @@ -1822,10 +2008,14 @@ msgstr "ブックマークはまだありません" msgid "No draft to delete" msgstr "削除する下書きがありません" -#: src/routes/(root)/[handle]/drafts/index.tsx:262 +#: src/routes/(root)/[handle]/drafts/index.tsx:264 msgid "No drafts yet. Create your first article!" msgstr "まだ下書きがありません。最初の記事を作成しましょう!" +#: src/routes/(root)/admin/news.tsx:428 +msgid "No exclusion patterns yet." +msgstr "除外パターンはまだありません。" + #: src/components/ActorFollowerList.tsx:92 msgid "No followers found" msgstr "フォロワーはいません" @@ -1834,9 +2024,9 @@ msgstr "フォロワーはいません" msgid "No following found" msgstr "フォロー中のユーザーはいません" -#: src/routes/(root)/[handle]/invite/[id].tsx:265 -#: src/routes/(root)/[handle]/settings/invite.tsx:410 -#: src/routes/(root)/[handle]/settings/invite.tsx:831 +#: src/routes/(root)/[handle]/invite/[id].tsx:270 +#: src/routes/(root)/[handle]/settings/invite.tsx:411 +#: src/routes/(root)/[handle]/settings/invite.tsx:832 msgid "No invitations left" msgstr "招待状が残っていません" @@ -1852,16 +2042,24 @@ msgstr "記事はありません" msgid "No notes found" msgstr "投稿はありません" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:171 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:178 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:172 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:179 msgid "No one has quoted this yet." msgstr "まだ誰も引用していません。" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:184 +#: src/components/NewsDiscussion.tsx:86 +msgid "No one has shared this link in a public post yet." +msgstr "このリンクを公開投稿で共有した人はまだいません。" + +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:185 msgid "No one has shared this yet." msgstr "まだ誰も共有していません。" +#: src/routes/(root)/admin/news.tsx:473 +msgid "No penalized links." +msgstr "ペナルティ付きのリンクはありません。" + #: src/components/ActorPostList.tsx:140 #: src/components/ActorSharedPostList.tsx:93 #: src/components/PersonalTimeline.tsx:289 @@ -1874,8 +2072,8 @@ msgstr "コンテンツはありません" msgid "No previews" msgstr "プレビューなし" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:189 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:190 msgid "No reactions yet." msgstr "まだリアクションがありません。" @@ -1883,6 +2081,10 @@ msgstr "まだリアクションがありません。" msgid "No reactors loaded." msgstr "読み込まれたリアクターはありません。" +#: src/components/NewsList.tsx:148 +msgid "No shared links yet. Once links start circulating across the fediverse, they will appear here." +msgstr "共有されたリンクはまだありません。フェディバースでリンクが広がり始めると、ここに表示されます。" + #: src/routes/(root)/sign/index.tsx:223 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pubにそのようなアカウントはありません。もう一度お試しください。" @@ -1891,16 +2093,29 @@ msgstr "Hackers' Pubにそのようなアカウントはありません。もう msgid "No user URI provided." msgstr "ユーザーURIが提供されていません。" +#: src/routes/(root)/admin/news.tsx:303 +msgid "Not authorized to clear penalties." +msgstr "ペナルティを解除する権限がありません。" + #: src/routes/(root)/admin/media.tsx:117 msgid "Not authorized to delete orphan media." msgstr "孤立したメディアを削除する権限がありません。" +#: src/routes/(root)/admin/news.tsx:244 +#: src/routes/(root)/admin/news.tsx:274 +msgid "Not authorized to manage exclusions." +msgstr "除外パターンを管理する権限がありません。" + +#: src/routes/(root)/admin/news.tsx:203 +msgid "Not authorized to recompute news scores." +msgstr "ニュースのスコアを再計算する権限がありません。" + #: src/routes/(root)/admin/invitations.tsx:116 msgid "Not authorized to regenerate invitations." msgstr "招待状を再付与する権限がありません。" -#: src/routes/(root)/[handle]/invite/[id].tsx:222 -#: src/routes/(root)/[handle]/invite/[id].tsx:225 +#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:230 msgid "Not found" msgstr "見つかりません" @@ -1912,26 +2127,30 @@ msgstr "見つかりません" #~ msgid "Note" #~ msgstr "投稿" -#: src/components/NoteComposer.tsx:885 +#: src/routes/(root)/admin/news.tsx:407 +msgid "Note (optional)" +msgstr "メモ(任意)" + +#: src/components/NoteComposer.tsx:896 msgid "Note created successfully" msgstr "投稿が作成されました" #. placeholder {0}: "REL_ME_ATTR" -#: src/routes/(root)/[handle]/settings/index.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:524 msgid "Note that you can verify your links belong to you by making the linked pages also link to your Hackers' Pub profile with {0} attribute." msgstr "リンクが実際にあなたのものであることを認証できます。リンク先のページでも{0}属性を使用してあなたのHackers' Pubプロフィールへリンクしてください。" -#: src/components/NoteComposer.tsx:833 +#: src/components/NoteComposer.tsx:839 msgid "Note updated" msgstr "投稿を更新しました。" #: src/components/ProfileTabs.tsx:44 -#: src/routes/(root)/[handle]/bookmarks.tsx:137 +#: src/routes/(root)/[handle]/bookmarks.tsx:138 msgid "Notes" msgstr "投稿" #: src/components/MarkdownEditor.tsx:193 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:463 msgid "Nothing to preview" msgstr "プレビューする内容がありません" @@ -1944,7 +2163,7 @@ msgstr "通知の権限が許可されませんでした。" msgid "Notification preview privacy" msgstr "通知プレビューのプライバシー" -#: src/components/AppSidebar.tsx:574 +#: src/components/AppSidebar.tsx:597 msgid "Notifications" msgstr "通知" @@ -1957,7 +2176,7 @@ msgstr "このサイトの通知はブロックされています。" msgid "Notifications are blocked in your browser settings." msgstr "ブラウザー設定で通知がブロックされています。" -#: src/routes/(root)/[handle]/settings/invite.tsx:782 +#: src/routes/(root)/[handle]/settings/invite.tsx:783 msgid "Number of invitations" msgstr "招待数" @@ -1982,7 +2201,7 @@ msgstr "または" msgid "Or enter the code from the email" msgstr "またはメールのコードを入力してください" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:877 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:878 msgid "Other languages" msgstr "他の言語" @@ -1994,31 +2213,39 @@ msgstr "ページが見つかりません" msgid "Passkey authentication failed" msgstr "パスキー認証に失敗しました" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:370 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:371 msgid "Passkey name" msgstr "パスキー名" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:265 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:266 msgid "Passkey registered successfully" msgstr "パスキーの登録に成功しました" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:312 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 msgid "Passkey revoked" msgstr "パスキーを取り消しました" #: src/components/SettingsTabs.tsx:77 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:355 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:356 msgid "Passkeys" msgstr "パスキー" -#: src/routes/(root)/[handle]/bookmarks.tsx:98 -#: src/routes/(root)/[handle]/bookmarks.tsx:101 -#: src/routes/(root)/[handle]/drafts/[id].tsx:50 -#: src/routes/(root)/[handle]/drafts/[id].tsx:51 -#: src/routes/(root)/[handle]/drafts/index.tsx:213 -#: src/routes/(root)/[handle]/drafts/index.tsx:216 -#: src/routes/(root)/[handle]/drafts/new.tsx:60 -#: src/routes/(root)/[handle]/drafts/new.tsx:61 +#: src/routes/(root)/admin/news.tsx:463 +msgid "Penalized links" +msgstr "ペナルティ付きのリンク" + +#: src/routes/(root)/admin/news.tsx:297 +msgid "Penalty cleared." +msgstr "ペナルティを解除しました。" + +#: src/routes/(root)/[handle]/bookmarks.tsx:99 +#: src/routes/(root)/[handle]/bookmarks.tsx:102 +#: src/routes/(root)/[handle]/drafts/[id].tsx:52 +#: src/routes/(root)/[handle]/drafts/[id].tsx:53 +#: src/routes/(root)/[handle]/drafts/index.tsx:215 +#: src/routes/(root)/[handle]/drafts/index.tsx:218 +#: src/routes/(root)/[handle]/drafts/new.tsx:62 +#: src/routes/(root)/[handle]/drafts/new.tsx:63 msgid "Permission denied" msgstr "アクセスが拒否されました" @@ -2026,21 +2253,21 @@ msgstr "アクセスが拒否されました" msgid "Pin to profile" msgstr "プロフィールに固定" -#: src/routes/(root)/[handle]/(profile)/index.tsx:302 +#: src/routes/(root)/[handle]/(profile)/index.tsx:305 msgid "Pinned posts" msgstr "固定されたコンテンツ" -#: src/routes/(root)/[handle]/settings/index.tsx:187 +#: src/routes/(root)/[handle]/settings/index.tsx:188 msgid "Please choose an image file smaller than 5 MiB." msgstr "5 MiB未満の画像ファイルを選択してください。" -#: src/routes/(root)/[handle]/settings/invite.tsx:249 -#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:250 +#: src/routes/(root)/[handle]/settings/invite.tsx:541 msgid "Please correct the errors and try again." msgstr "エラーを修正して、もう一度お試しください。" #: src/components/article-composer/ArticleComposerForm.tsx:53 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:383 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:384 msgid "Please enter a title for your article." msgstr "記事のタイトルを入力してください。" @@ -2048,9 +2275,9 @@ msgstr "記事のタイトルを入力してください。" msgid "Please enter your Fediverse handle." msgstr "フェディバースのハンドルを入力してください。" -#: src/routes/(root)/[handle]/drafts/[id].tsx:55 -#: src/routes/(root)/[handle]/drafts/index.tsx:220 -#: src/routes/(root)/[handle]/drafts/new.tsx:65 +#: src/routes/(root)/[handle]/drafts/[id].tsx:57 +#: src/routes/(root)/[handle]/drafts/index.tsx:222 +#: src/routes/(root)/[handle]/drafts/new.tsx:67 msgid "Please sign in to access this page" msgstr "このページにアクセスするにはログインしてください" @@ -2062,6 +2289,10 @@ msgstr "投票するにはログインしてください" msgid "Poll closed" msgstr "アンケートは終了しました" +#: src/components/NewsList.tsx:88 +msgid "Popular" +msgstr "人気" + #: src/components/PostActionMenu.tsx:291 msgid "Post deleted" msgstr "コンテンツを削除しました" @@ -2079,27 +2310,27 @@ msgstr "コンテンツの固定を解除しました" msgid "Posts" msgstr "コンテンツ" -#: src/routes/(root)/[handle]/settings/preferences.tsx:183 +#: src/routes/(root)/[handle]/settings/preferences.tsx:184 msgid "Prefer AI-generated summary" msgstr "AI生成の要約を優先" #: src/components/SettingsTabs.tsx:53 -#: src/routes/(root)/[handle]/settings/preferences.tsx:169 #: src/routes/(root)/[handle]/settings/preferences.tsx:170 +#: src/routes/(root)/[handle]/settings/preferences.tsx:171 msgid "Preferences" msgstr "環境設定" -#: src/routes/(root)/[handle]/invite/[id].tsx:387 +#: src/routes/(root)/[handle]/invite/[id].tsx:392 msgid "Preferred language" msgstr "優先言語" -#: src/routes/(root)/[handle]/settings/language.tsx:83 +#: src/routes/(root)/[handle]/settings/language.tsx:84 msgid "Preferred languages" msgstr "優先言語" #: src/components/article-composer/ArticleComposerForm.tsx:111 #: src/components/MarkdownEditor.tsx:164 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:426 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:427 msgid "Preview" msgstr "プレビュー" @@ -2111,7 +2342,7 @@ msgstr "前の画像" msgid "Priority" msgstr "優先度" -#: src/components/AppSidebar.tsx:982 +#: src/components/AppSidebar.tsx:1028 #: src/routes/(root)/privacy.tsx:40 msgid "Privacy policy" msgstr "プライバシーポリシー" @@ -2124,8 +2355,8 @@ msgstr "プロフィール" msgid "Profile actions" msgstr "プロフィール操作" -#: src/routes/(root)/[handle]/settings/index.tsx:124 #: src/routes/(root)/[handle]/settings/index.tsx:125 +#: src/routes/(root)/[handle]/settings/index.tsx:126 msgid "Profile settings" msgstr "プロフィール設定" @@ -2155,8 +2386,8 @@ msgstr "公開中…" msgid "Push notification privacy updated" msgstr "プッシュ通知のプライバシー設定を更新しました" -#: src/routes/(root)/[handle]/settings/invite.tsx:647 -#: src/routes/(root)/[handle]/settings/invite.tsx:713 +#: src/routes/(root)/[handle]/settings/invite.tsx:648 +#: src/routes/(root)/[handle]/settings/invite.tsx:714 msgid "QR code" msgstr "QRコード" @@ -2170,7 +2401,7 @@ msgstr "検索文字列は空にできません" msgid "Quiet public" msgstr "ひかえめな公開" -#: src/components/NoteComposeModal.tsx:71 +#: src/components/NoteComposeModal.tsx:72 #: src/components/PostEngagementBar.tsx:270 #: src/components/ui/markdown-editor.tsx:289 msgid "Quote" @@ -2194,8 +2425,8 @@ msgid "Quoted post hidden" msgstr "引用元は非表示です" #: src/components/EngagementTabs.tsx:46 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:100 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:111 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:101 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:112 msgid "Quotes" msgstr "引用" @@ -2218,8 +2449,8 @@ msgid "reaction" msgstr "リアクション" #: src/components/EngagementTabs.tsx:55 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:159 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:173 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:160 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:174 msgid "Reactions" msgstr "リアクション" @@ -2232,6 +2463,10 @@ msgstr "記事全文を読む" #~ msgid "Read the full Code of conduct" #~ msgstr "行動規範の全文を読む" +#: src/routes/(root)/admin/news.tsx:344 +msgid "Rebuilds the popularity score of every shared link from scratch. The operation is idempotent and safe to run at any time; scores normally stay fresh on their own, so this is mainly a manual backstop and a development tool." +msgstr "共有されたすべてのリンクの人気スコアを一から再構築します。この操作は冪等で、いつ実行しても安全です。スコアは通常自動的に最新の状態に保たれるため、これは主に手動の予備手段および開発ツールです。" + #: src/components/WebPushPromptBanner.tsx:252 msgid "Receive new notifications immediately, even when this tab is closed." msgstr "このタブを閉じていても新しい通知をすぐに受け取れます。" @@ -2240,10 +2475,19 @@ msgstr "このタブを閉じていても新しい通知をすぐに受け取れ msgid "Receive notifications immediately through this browser, even when this tab is closed." msgstr "このブラウザーで通知をすぐに受け取ります。このタブを閉じていても通知されます。" -#: src/components/AppSidebar.tsx:901 +#: src/components/AppSidebar.tsx:947 msgid "Recent drafts" msgstr "最近の下書き" +#: src/routes/(root)/admin/news.tsx:342 +#: src/routes/(root)/admin/news.tsx:375 +msgid "Recompute news scores" +msgstr "ニュースのスコアを再計算" + +#: src/routes/(root)/admin/news.tsx:374 +msgid "Recomputing…" +msgstr "再計算中…" + #: src/routes/(root)/admin/invitations.tsx:203 msgid "Regenerate" msgstr "再付与" @@ -2260,23 +2504,23 @@ msgstr "招待状を再付与" msgid "Regenerating…" msgstr "再付与中…" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Register" msgstr "登録" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:362 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:363 msgid "Register a passkey" msgstr "パスキーを登録" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:364 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:365 msgid "Register a passkey to sign in to your account. You can use a passkey instead of receiving a sign-in link by email." msgstr "アカウントにパスキーを登録してください。メールでログインリンクを受け取る代わりにパスキーを使用できます。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:393 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:394 msgid "Registered passkeys" msgstr "登録済みパスキー" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Registering…" msgstr "登録中…" @@ -2287,6 +2531,7 @@ msgid "Remote follow" msgstr "リモートフォロー" #: src/components/LanguageList.tsx:225 +#: src/routes/(root)/admin/news.tsx:451 msgid "Remove" msgstr "削除" @@ -2304,13 +2549,13 @@ msgstr "ブックマークを削除" msgid "Remove from sidebar" msgstr "サイドバーから削除" -#: src/components/NoteComposer.tsx:1358 -#: src/components/NoteComposer.tsx:1359 +#: src/components/NoteComposer.tsx:1369 +#: src/components/NoteComposer.tsx:1370 msgid "Remove image" msgstr "画像を削除" -#: src/components/NoteComposer.tsx:1113 -#: src/components/NoteComposer.tsx:1114 +#: src/components/NoteComposer.tsx:1124 +#: src/components/NoteComposer.tsx:1125 msgid "Remove quote" msgstr "引用を削除" @@ -2320,11 +2565,11 @@ msgstr "作成から十分な時間が経過し、アイコン、投稿、記事 #: src/components/article-composer/ArticleComposerForm.tsx:134 #: src/components/MarkdownEditor.tsx:181 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:452 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:453 msgid "Rendering…" msgstr "レンダリング中…" -#: src/components/NoteComposeModal.tsx:70 +#: src/components/NoteComposeModal.tsx:71 #: src/components/PostEngagementBar.tsx:258 msgid "Reply" msgstr "返信" @@ -2333,20 +2578,20 @@ msgstr "返信" msgid "Replying is not available for this post" msgstr "このコンテンツには返信できません" -#: src/components/NoteComposer.tsx:1007 +#: src/components/NoteComposer.tsx:1018 msgid "Replying to" msgstr "返信先" -#: src/components/AppSidebar.tsx:498 +#: src/components/AppSidebar.tsx:521 msgid "Return to old UI" msgstr "旧UIに戻る" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:475 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:526 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:476 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:527 msgid "Revoke" msgstr "取り消す" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:515 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:516 msgid "Revoke passkey" msgstr "パスキーを取り消す" @@ -2359,14 +2604,14 @@ msgstr "引用を取り消す" msgid "Revoke this quote?" msgstr "この引用を取り消しますか?" -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Save" msgstr "保存" -#: src/components/NoteComposer.tsx:1412 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 +#: src/components/NoteComposer.tsx:1423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 msgid "Save changes" msgstr "変更を保存" @@ -2379,16 +2624,16 @@ msgid "Save draft to see preview" msgstr "下書きを保存するとプレビューが表示されます" #: src/components/article-composer/ArticleComposerActions.tsx:36 -#: src/components/NoteComposer.tsx:1413 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/components/NoteComposer.tsx:1424 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Saving…" msgstr "保存中…" #: src/components/admin/AdminAccountsTable.tsx:202 -#: src/components/AppSidebar.tsx:377 +#: src/components/AppSidebar.tsx:400 #: src/components/SearchForm.tsx:65 #: src/components/SearchForm.tsx:80 msgid "Search" @@ -2423,16 +2668,16 @@ msgstr "選択肢を選んでください" msgid "Select options" msgstr "選択肢を選んでください" -#: src/routes/(root)/[handle]/settings/language.tsx:84 +#: src/routes/(root)/[handle]/settings/language.tsx:85 msgid "Select your preferred languages in order of preference. This will help tailor content to your preferences." msgstr "優先度の高い順に言語を選択してください。コンテンツをあなたの好みに合わせて調整するのに役立ちます。" -#: src/routes/(root)/[handle]/settings/invite.tsx:413 +#: src/routes/(root)/[handle]/settings/invite.tsx:414 msgid "Send" msgstr "送信" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 -#: src/routes/(root)/[handle]/settings/invite.tsx:412 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 +#: src/routes/(root)/[handle]/settings/invite.tsx:413 msgid "Sending…" msgstr "送信中…" @@ -2444,11 +2689,11 @@ msgstr "センシティブなコンテンツ" msgid "Separate tags with spaces. Tags help readers discover your article." msgstr "タグはスペースで区切ります。タグを付けると読者が記事を見つけやすくなります。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:171 +#: src/routes/(root)/[handle]/settings/preferences.tsx:172 msgid "Set your personal preferences." msgstr "個人の環境設定を設定してください。" -#: src/components/AppSidebar.tsx:674 +#: src/components/AppSidebar.tsx:697 msgid "Settings" msgstr "設定" @@ -2456,10 +2701,19 @@ msgstr "設定" msgid "Share" msgstr "共有" +#: src/components/NewsStoryCard.tsx:198 +#: src/components/NewsStoryHeader.tsx:132 +msgid "Share this link" +msgstr "このリンクを共有" + +#: src/components/NewsDiscussionComposer.tsx:30 +msgid "Share your opinion on this story…" +msgstr "この話題について意見を投稿しましょう…" + #: src/components/EngagementTabs.tsx:37 #: src/components/ProfileTabs.tsx:54 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:101 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:114 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:102 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:115 msgid "Shares" msgstr "共有" @@ -2474,6 +2728,15 @@ msgstr "このコンテンツは共有できません。" msgid "Show" msgstr "表示" +#. placeholder {0}: childCount() +#: src/components/NewsDiscussionThread.tsx:356 +msgid "Show {0} more in this thread" +msgstr "このスレッドであと{0}件を表示" + +#: src/components/NewsDiscussion.tsx:77 +msgid "Show more sharing posts" +msgstr "共有コンテンツをもっと表示" + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Show preview" #~ msgstr "プレビューを表示" @@ -2482,7 +2745,7 @@ msgstr "表示" msgid "Show sensitive content" msgstr "センシティブなコンテンツを表示" -#: src/components/AppSidebar.tsx:535 +#: src/components/AppSidebar.tsx:558 #: src/routes/(root)/sign/index.tsx:382 msgid "Sign in" msgstr "ログイン" @@ -2491,6 +2754,10 @@ msgstr "ログイン" msgid "Sign in to Hackers' Pub" msgstr "Hackers' Pubにログイン" +#: src/components/NewsDiscussionComposer.tsx:44 +msgid "Sign in to post" +msgstr "ログインして投稿" + #: src/components/QuestionCard.tsx:394 msgid "Sign in to vote" msgstr "ログインして投票" @@ -2499,11 +2766,11 @@ msgstr "ログインして投票" msgid "Sign in with passkey" msgstr "パスキーでサインイン" -#: src/components/AppSidebar.tsx:964 +#: src/components/AppSidebar.tsx:1010 msgid "Sign out" msgstr "ログアウト" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 #: src/routes/(root)/sign/up/[token].tsx:494 msgid "Sign up" msgstr "登録" @@ -2536,7 +2803,7 @@ msgstr "スラッグ(URL)" msgid "Slug cannot be empty" msgstr "スラッグは空にできません" -#: src/components/NoteComposer.tsx:613 +#: src/components/NoteComposer.tsx:619 msgid "Some images were skipped because the limit of {MAX_MEDIA} was reached" msgstr "{MAX_MEDIA} 枚の上限に達したため、一部の画像をスキップしました" @@ -2551,22 +2818,22 @@ msgstr "問題が発生しました。再度お試しください。" #: src/components/article-composer/ArticleComposerContext.tsx:309 #: src/components/article-composer/ArticleComposerContext.tsx:384 #: src/components/article-composer/ArticleComposerContext.tsx:449 -#: src/components/NoteComposer.tsx:832 -#: src/components/NoteComposer.tsx:884 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:331 -#: src/routes/(root)/[handle]/drafts/index.tsx:174 +#: src/components/NoteComposer.tsx:838 +#: src/components/NoteComposer.tsx:895 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/drafts/index.tsx:175 msgid "Success" msgstr "成功" -#: src/routes/(root)/[handle]/settings/language.tsx:147 +#: src/routes/(root)/[handle]/settings/language.tsx:148 msgid "Successfully saved language preferences" msgstr "言語設定を正常に保存しました" -#: src/routes/(root)/[handle]/settings/preferences.tsx:133 +#: src/routes/(root)/[handle]/settings/preferences.tsx:134 msgid "Successfully saved preferences" msgstr "環境設定を正常に保存しました" -#: src/routes/(root)/[handle]/settings/index.tsx:328 +#: src/routes/(root)/[handle]/settings/index.tsx:329 msgid "Successfully saved settings" msgstr "設定を正常に保存しました" @@ -2575,9 +2842,9 @@ msgid "Summarized by LLM" msgstr "LLMによる要約" #: src/components/DocumentView.tsx:38 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:721 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:729 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1056 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:722 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:730 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1057 msgid "Table of contents" msgstr "目次" @@ -2586,8 +2853,8 @@ msgstr "目次" #~ msgstr "タグ" #: src/components/article-composer/ArticleComposerForm.tsx:158 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:478 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1065 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:479 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1066 msgid "Tags" msgstr "タグ" @@ -2595,65 +2862,73 @@ msgstr "タグ" msgid "Tell us about yourself…" msgstr "自己紹介をお聞かせください…" +#: src/routes/(root)/admin/news.tsx:237 +msgid "That is not a valid URL pattern." +msgstr "有効なURLパターンではありません。" + #: src/components/WebPushNotificationSettings.tsx:158 #: src/components/WebPushPromptBanner.tsx:93 msgid "The browser did not provide a complete push subscription." msgstr "ブラウザーから完全なプッシュ通知サブスクリプションが提供されませんでした。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:200 +#: src/routes/(root)/[handle]/settings/preferences.tsx:201 msgid "The default privacy setting for your notes." msgstr "投稿のデフォルト公開設定です。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:212 +#: src/routes/(root)/[handle]/settings/preferences.tsx:213 msgid "The default privacy setting for your shares." msgstr "共有のデフォルト公開設定です。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:227 +#: src/routes/(root)/[handle]/settings/preferences.tsx:228 msgid "The default quote permission for your notes." msgstr "投稿のデフォルト引用許可設定です。" -#: src/routes/(root)/[handle]/invite/[id].tsx:169 -#: src/routes/(root)/[handle]/settings/invite.tsx:352 +#: src/routes/(root)/[handle]/invite/[id].tsx:174 +#: src/routes/(root)/[handle]/settings/invite.tsx:353 msgid "The email address is invalid." msgstr "メールアドレスが無効です。" -#: src/routes/(root)/[handle]/settings/invite.tsx:347 +#: src/routes/(root)/[handle]/settings/invite.tsx:348 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "メールアドレスは招待状を受け取るだけでなく、アカウントへのログインにも使用されます。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:395 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:396 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "以下のパスキーがあなたのアカウントに登録されています。これらを使用してアカウントにログインできます。" -#: src/routes/(root)/[handle]/invite/[id].tsx:187 +#: src/routes/(root)/[handle]/invite/[id].tsx:192 msgid "The invitation email could not be sent. Please try again later." msgstr "招待メールを送信できませんでした。後でもう一度お試しください。" -#: src/routes/(root)/[handle]/settings/invite.tsx:259 +#: src/routes/(root)/[handle]/settings/invite.tsx:260 msgid "The invitation has been sent successfully." msgstr "招待状が正常に送信されました。" -#: src/routes/(root)/[handle]/settings/invite.tsx:590 +#: src/routes/(root)/[handle]/settings/invite.tsx:591 msgid "The invitation link could not be found or you are not authorized to delete it." msgstr "招待リンクが見つからないか、削除する権限がありません。" -#: src/routes/(root)/[handle]/settings/invite.tsx:612 +#: src/routes/(root)/[handle]/settings/invite.tsx:613 msgid "The invitation link has been copied to the clipboard." msgstr "招待リンクがクリップボードにコピーされました。" -#: src/routes/(root)/[handle]/settings/invite.tsx:532 +#: src/routes/(root)/[handle]/settings/invite.tsx:533 msgid "The invitation link has been created successfully." msgstr "招待リンクが正常に作成されました。" -#: src/routes/(root)/[handle]/settings/invite.tsx:583 +#: src/routes/(root)/[handle]/settings/invite.tsx:584 msgid "The invitation link has been deleted successfully." msgstr "招待リンクが正常に削除されました。" +#: src/components/NewsDiscussionComposer.tsx:34 +msgid "The link to this story is added to your post automatically." +msgstr "この話題のリンクは自動的にコンテンツに追加されます。" + #: src/components/NotFoundPage.tsx:45 msgid "The page you're looking for doesn't exist or has been moved." msgstr "お探しのページは存在しないか、移動された可能性があります。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:314 msgid "The passkey has been successfully revoked." msgstr "パスキーを正常に取り消しました。" @@ -2671,7 +2946,7 @@ msgstr "登録リンクが無効です。受信したメールのリンクを正 #. placeholder {0}: "GITHUB_REPOSITORY" #. placeholder {1}: "AGPL-3.0" -#: src/components/AppSidebar.tsx:1006 +#: src/components/AppSidebar.tsx:1052 msgid "The source code of this website is available on {0} under the {1} license." msgstr "このウェブサイトのソースコードは{1}ライセンスで{0}で公開されています。" @@ -2679,7 +2954,7 @@ msgstr "このウェブサイトのソースコードは{1}ライセンスで{0} msgid "The title will appear at the top of your article and in link previews." msgstr "タイトルは記事の先頭とリンクプレビューに表示されます。" -#: src/routes/(root)/[handle]/settings/index.tsx:603 +#: src/routes/(root)/[handle]/settings/index.tsx:604 msgid "The URL of the link, e.g., https://github.com/yourhandle." msgstr "リンクのURL。(例:https://github.com/yourhandle)" @@ -2697,26 +2972,26 @@ msgstr "この操作は取り消せません。このコンテンツは完全に msgid "This browser does not support Web Push." msgstr "このブラウザーは Web Push に対応していません。" -#: src/routes/(root)/[handle]/invite/[id].tsx:173 -#: src/routes/(root)/[handle]/invite/[id].tsx:177 +#: src/routes/(root)/[handle]/invite/[id].tsx:178 +#: src/routes/(root)/[handle]/invite/[id].tsx:182 msgid "This email is already associated with an existing account." msgstr "このメールアドレスは既存のアカウントに関連付けられています。" -#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:232 msgid "This invitation link does not exist or has been deleted." msgstr "この招待リンクは存在しないか、削除されています。" -#: src/routes/(root)/[handle]/invite/[id].tsx:161 -#: src/routes/(root)/[handle]/invite/[id].tsx:257 +#: src/routes/(root)/[handle]/invite/[id].tsx:166 +#: src/routes/(root)/[handle]/invite/[id].tsx:262 msgid "This invitation link has expired." msgstr "この招待リンクの有効期限が切れています。" -#: src/routes/(root)/[handle]/invite/[id].tsx:164 -#: src/routes/(root)/[handle]/invite/[id].tsx:267 +#: src/routes/(root)/[handle]/invite/[id].tsx:169 +#: src/routes/(root)/[handle]/invite/[id].tsx:272 msgid "This invitation link has no remaining invitations." msgstr "この招待リンクには残りの招待がありません。" -#: src/routes/(root)/[handle]/invite/[id].tsx:159 +#: src/routes/(root)/[handle]/invite/[id].tsx:164 msgid "This invitation link was not found." msgstr "この招待リンクが見つかりませんでした。" @@ -2748,7 +3023,7 @@ msgstr "このサーバーではまだWeb Pushが設定されていません。" msgid "This service does not support remote follow." msgstr "このサービスはリモートフォローに対応していません。" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:552 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:553 msgid "This usually takes about a minute. The page will update automatically when the translation is ready." msgstr "通常1分ほどかかります。翻訳が完了するとページが自動的に更新されます。" @@ -2765,7 +3040,7 @@ msgid "Timeline" msgstr "タイムライン" #: src/components/article-composer/ArticleComposerForm.tsx:49 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:380 msgid "Title" msgstr "タイトル" @@ -2789,7 +3064,7 @@ msgstr "合計:{0}" #. placeholder {0}: "LANGUAGE" #: src/components/ArticleCard.tsx:350 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:861 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:862 msgid "Translated from {0}" msgstr "{0}から翻訳" @@ -2797,18 +3072,18 @@ msgstr "{0}から翻訳" #~ msgid "Translating to {0}…" #~ msgstr "{0}に翻訳中…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:548 msgid "Translating to {name}…" msgstr "{name}に翻訳中…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:543 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:544 msgid "Translating…" msgstr "翻訳中…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:378 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:401 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:410 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:588 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:402 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:411 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:589 msgid "Translation request failed" msgstr "翻訳のリクエストに失敗しました" @@ -2816,17 +3091,17 @@ msgstr "翻訳のリクエストに失敗しました" #~ msgid "Translation request failed for {0}" #~ msgstr "{0}への翻訳リクエストに失敗しました" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:593 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:594 msgid "Translation request failed for {name}" msgstr "{name}への翻訳リクエストに失敗しました" #: src/app.tsx:125 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:601 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:602 msgid "Try again" msgstr "再試行" #: src/components/article-composer/ArticleComposerForm.tsx:162 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:482 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:483 msgid "Type tags separated by spaces" msgstr "スペースで区切ってタグを入力" @@ -2844,7 +3119,7 @@ msgstr "リアクションを追加できませんでした。もう一度お試 msgid "Unable to remove reaction. Please try again." msgstr "リアクションを削除できませんでした。もう一度お試しください。" -#: src/components/BlockedAccountsList.tsx:181 +#: src/components/BlockedAccountsList.tsx:117 #: src/components/ProfileActionMenu.tsx:377 #: src/components/ProfileActionMenu.tsx:405 #: src/components/ProfileActionMenu.tsx:413 @@ -2860,7 +3135,7 @@ msgstr "ユーザーのブロックを解除しますか?" msgid "Unfollow" msgstr "フォロー解除" -#: src/components/MutedAccountsList.tsx:178 +#: src/components/MutedAccountsList.tsx:114 #: src/components/ProfileActionMenu.tsx:361 msgid "Unmute" msgstr "ミュート解除" @@ -2873,23 +3148,27 @@ msgstr "プロフィールから固定解除" msgid "Unshare" msgstr "共有を取り消す" -#: src/routes/(root)/[handle]/settings/index.tsx:126 +#: src/routes/(root)/[handle]/settings/index.tsx:127 msgid "Update your profile information, including your avatar, username, display name, bio, and links." msgstr "アイコン、ユーザー名、名前、自己紹介、リンクなどのプロフィール情報を更新してください。" #. placeholder {0}: new Date(edge.node.updated).toLocaleDateString() -#: src/routes/(root)/[handle]/drafts/index.tsx:304 +#: src/routes/(root)/[handle]/drafts/index.tsx:306 msgid "Updated {0}" msgstr "{0}に更新" -#: src/components/NoteComposer.tsx:1274 +#: src/components/NoteComposer.tsx:1285 msgid "Upload progress" msgstr "アップロードの進捗" -#: src/routes/(root)/[handle]/settings/index.tsx:585 +#: src/routes/(root)/[handle]/settings/index.tsx:586 msgid "URL" msgstr "URL" +#: src/routes/(root)/admin/news.tsx:394 +msgid "URL pattern" +msgstr "URLパターン" + #: src/components/ProfileActionMenu.tsx:277 msgid "User blocked" msgstr "ユーザーをブロックしました" @@ -2902,17 +3181,17 @@ msgstr "ユーザーをミュートしました" msgid "User not found." msgstr "ユーザー情報が見つかりません。" -#: src/components/BlockedAccountsList.tsx:95 +#: src/components/BlockedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:259 msgid "User unblocked" msgstr "ユーザーのブロックを解除しました" -#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:304 msgid "User unmuted" msgstr "ユーザーのミュートを解除しました" -#: src/routes/(root)/[handle]/settings/index.tsx:413 +#: src/routes/(root)/[handle]/settings/index.tsx:414 #: src/routes/(root)/sign/up/[token].tsx:331 msgid "Username" msgstr "ユーザー名" @@ -2933,7 +3212,7 @@ msgstr "ユーザー名は必須です。" msgid "Username is too long. Maximum length is 15 characters." msgstr "ユーザー名が長すぎます。最大15文字です。" -#: src/routes/(root)/[handle]/settings/invite.tsx:435 +#: src/routes/(root)/[handle]/settings/invite.tsx:436 msgid "Users you have invited" msgstr "招待したユーザー" @@ -2947,7 +3226,7 @@ msgstr "{1}に{0}さんがこのリンクの所有者であることを確認済 msgid "Verifying your invitation…" msgstr "招待を確認中…" -#: src/components/AppSidebar.tsx:927 +#: src/components/AppSidebar.tsx:973 msgid "View all drafts →" msgstr "すべての下書きを表示 →" @@ -3015,7 +3294,7 @@ msgstr "投票済み" msgid "Voting…" msgstr "投票中…" -#: src/components/NoteComposer.tsx:611 +#: src/components/NoteComposer.tsx:617 msgid "Warning" msgstr "警告" @@ -3023,11 +3302,11 @@ msgstr "警告" msgid "We couldn't reach the server" msgstr "サーバーに接続できませんでした" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:598 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:599 msgid "We couldn't reach the translation service. Try again, or come back in a few minutes." msgstr "翻訳サービスに接続できませんでした。再試行するか、数分後に再度お試しください。" -#: src/routes/(root)/[handle]/settings/index.tsx:566 +#: src/routes/(root)/[handle]/settings/index.tsx:567 msgid "Website" msgstr "ウェブサイト" @@ -3039,7 +3318,7 @@ msgstr "Hackers' Pubへようこそ!登録を完了するには以下のフォ msgid "What is Hackers' Pub?" msgstr "Hackers' Pubとは?" -#: src/components/NoteComposer.tsx:1153 +#: src/components/NoteComposer.tsx:1164 msgid "What's on your mind?" msgstr "今何してる?" @@ -3051,25 +3330,25 @@ msgstr "有効にすると、AIがこの記事を他の言語に自動翻訳す #~ msgid "Who can quote this note" #~ msgstr "この投稿の引用許可範囲" -#: src/components/AppSidebar.tsx:285 +#: src/components/AppSidebar.tsx:308 msgid "Without shares" msgstr "共有除外" #: src/components/article-composer/ArticleComposerForm.tsx:108 #: src/components/MarkdownEditor.tsx:161 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:424 msgid "Write" msgstr "編集" -#: src/components/NoteComposeModal.tsx:109 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1004 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:384 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:462 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:538 +#: src/components/NoteComposeModal.tsx:110 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1005 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:385 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:463 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:539 msgid "Write a reply…" msgstr "返信を書く…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:437 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:438 msgid "Write your article here." msgstr "ここに記事を書いてください。" @@ -3094,53 +3373,53 @@ msgstr "このユーザーからブロックされています。このユーザ msgid "You are blocking this user. They can't follow you or see your posts." msgstr "このユーザーをブロックしています。このユーザーはあなたをフォローしたり、あなたのコンテンツを見たりできません。" -#: src/components/NoteComposer.tsx:602 +#: src/components/NoteComposer.tsx:608 msgid "You can attach up to {MAX_MEDIA} images" msgstr "画像は最大{MAX_MEDIA}枚まで添付できます" -#: src/routes/(root)/[handle]/settings/index.tsx:448 +#: src/routes/(root)/[handle]/settings/index.tsx:449 msgid "You can change it only once, and the old username will become available to others." msgstr "変更は1回のみ可能で、変更前のユーザー名は他のユーザーが使用できるようになります。" -#: src/routes/(root)/[handle]/settings/index.tsx:614 +#: src/routes/(root)/[handle]/settings/index.tsx:615 msgid "You can leave this empty to remove the link." msgstr "リンクを削除する場合は空にしてください。" -#: src/routes/(root)/[handle]/settings/invite.tsx:397 -#: src/routes/(root)/[handle]/settings/invite.tsx:802 +#: src/routes/(root)/[handle]/settings/invite.tsx:398 +#: src/routes/(root)/[handle]/settings/invite.tsx:803 msgid "You can leave this field empty." msgstr "このフィールドは空欄でも構いません。" -#: src/routes/(root)/[handle]/drafts/new.tsx:64 +#: src/routes/(root)/[handle]/drafts/new.tsx:66 msgid "You can only create drafts for your own account" msgstr "自分のアカウントの下書きのみ作成できます" -#: src/routes/(root)/[handle]/drafts/[id].tsx:54 +#: src/routes/(root)/[handle]/drafts/[id].tsx:56 msgid "You can only edit your own drafts" msgstr "自分の下書きのみ編集できます" -#: src/routes/(root)/[handle]/bookmarks.tsx:103 +#: src/routes/(root)/[handle]/bookmarks.tsx:104 msgid "You can only view your own bookmarks" msgstr "自分のブックマークのみ表示できます" -#: src/routes/(root)/[handle]/drafts/index.tsx:219 +#: src/routes/(root)/[handle]/drafts/index.tsx:221 msgid "You can only view your own drafts" msgstr "自分の下書きのみ表示できます" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:404 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:405 msgid "You don't have any passkeys registered yet." msgstr "まだパスキーが登録されていません。" -#: src/routes/(root)/[handle]/settings/invite.tsx:309 -#: src/routes/(root)/[handle]/settings/invite.tsx:420 +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:421 msgid "You have no invitations left. Please wait until you receive more." msgstr "招待状が残っていません。追加されるまでお待ちください。" -#: src/components/BlockedAccountsList.tsx:116 +#: src/components/BlockedAccountsList.tsx:119 msgid "You haven't blocked anyone." msgstr "ブロックしているアカウントはありません。" -#: src/components/MutedAccountsList.tsx:113 +#: src/components/MutedAccountsList.tsx:116 msgid "You haven't muted anyone." msgstr "ミュートしているアカウントはありません。" @@ -3152,20 +3431,20 @@ msgstr "ミュートしているアカウントはありません。" msgid "You must be signed in" msgstr "ログインが必要です" -#: src/components/NoteComposer.tsx:901 +#: src/components/NoteComposer.tsx:912 msgid "You must be signed in to create a note" msgstr "投稿を作成するにはログインが必要です" #: src/components/article-composer/ArticleComposerContext.tsx:467 -#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:193 msgid "You must be signed in to delete a draft" msgstr "下書きを削除するにはログインする必要があります" -#: src/components/NoteComposer.tsx:851 +#: src/components/NoteComposer.tsx:857 msgid "You must be signed in to edit a note" msgstr "投稿を編集するにはログインが必要です。" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:357 msgid "You must be signed in to edit an article" msgstr "記事を編集するにはログインする必要があります" @@ -3185,15 +3464,15 @@ msgstr "あなたを招待した方" msgid "You'll automatically follow each other when you sign up." msgstr "登録したら自動的にお互いをフォローします。" -#: src/routes/(root)/[handle]/invite/[id].tsx:276 +#: src/routes/(root)/[handle]/invite/[id].tsx:281 msgid "You've been invited to Hackers' Pub" msgstr "Hackers' Pubに招待されました" -#: src/routes/(root)/[handle]/settings/index.tsx:353 +#: src/routes/(root)/[handle]/settings/index.tsx:354 msgid "Your avatar will be displayed on your profile and in your posts. You can upload a PNG, JPEG, GIF, or WebP image up to 5 MiB in size." msgstr "アイコンはプロフィールとコンテンツに表示されます。PNG、JPEG、GIF、WebP形式の画像を5MiBまでアップロードできます。" -#: src/routes/(root)/[handle]/settings/index.tsx:492 +#: src/routes/(root)/[handle]/settings/index.tsx:493 msgid "Your bio will be displayed on your profile. You can use Markdown to format it." msgstr "自己紹介はプロフィールに表示されます。Markdownを使用できます。" @@ -3209,36 +3488,36 @@ msgstr "接続が不安定なようです。ネットワークをご確認のう msgid "Your email address will be used to sign in to your account." msgstr "メールアドレスはアカウントへのログインに使用されます。" -#: src/routes/(root)/[handle]/settings/invite.tsx:400 +#: src/routes/(root)/[handle]/settings/invite.tsx:401 msgid "Your friend will see this message in the invitation email." msgstr "友達は招待メールでこのメッセージを見ることができます。" -#: src/routes/(root)/[handle]/settings/index.tsx:478 +#: src/routes/(root)/[handle]/settings/index.tsx:479 #: src/routes/(root)/sign/up/[token].tsx:393 msgid "Your name will be displayed on your profile and in your posts." msgstr "名前はプロフィールとコンテンツに表示されます。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:267 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:268 msgid "Your passkey has been registered and can now be used for authentication." msgstr "パスキーが登録され、認証に使用できるようになりました。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:134 +#: src/routes/(root)/[handle]/settings/preferences.tsx:135 msgid "Your preferences have been updated successfully." msgstr "環境設定が正常に更新されました。" -#: src/routes/(root)/[handle]/settings/language.tsx:148 +#: src/routes/(root)/[handle]/settings/language.tsx:149 msgid "Your preferred languages have been updated." msgstr "優先言語が更新されました。" -#: src/routes/(root)/[handle]/settings/index.tsx:329 +#: src/routes/(root)/[handle]/settings/index.tsx:330 msgid "Your profile settings have been updated successfully." msgstr "プロフィール設定が正常に更新されました。" -#: src/components/NoteComposeModal.tsx:128 +#: src/components/NoteComposeModal.tsx:129 msgid "Your unsaved draft will be lost." msgstr "保存されていない下書きは失われます。" -#: src/routes/(root)/[handle]/settings/index.tsx:445 +#: src/routes/(root)/[handle]/settings/index.tsx:446 #: src/routes/(root)/sign/up/[token].tsx:367 msgid "Your username will be used to create your profile URL and your fediverse handle." msgstr "ユーザー名はプロフィールURLとフェディバースハンドルの作成に使用されます。" diff --git a/web-next/src/locales/ko-KR/messages.po b/web-next/src/locales/ko-KR/messages.po index 7c6825b2d..cb1a943ac 100644 --- a/web-next/src/locales/ko-KR/messages.po +++ b/web-next/src/locales/ko-KR/messages.po @@ -14,7 +14,7 @@ msgstr "" "Plural-Forms: \n" #. placeholder {0}: article.replies?.edges.length ?? 0 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:991 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:992 msgid "{0, plural, one {# comment} other {# comments}}" msgstr "{0, plural, other {# 댓글}}" @@ -44,11 +44,16 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {# 팔로잉}}" #. placeholder {0}: link.invitationsLeft -#: src/routes/(root)/[handle]/invite/[id].tsx:321 -#: src/routes/(root)/[handle]/settings/invite.tsx:742 +#: src/routes/(root)/[handle]/invite/[id].tsx:326 +#: src/routes/(root)/[handle]/settings/invite.tsx:743 msgid "{0, plural, one {# invitation left} other {# invitations left}}" msgstr "{0, plural, other {남은 초대 #건}}" +#. placeholder {0}: status()?.scoredLinkCount ?? 0 +#: src/routes/(root)/admin/news.tsx:350 +msgid "{0, plural, one {# link is currently in the news feed.} other {# links are currently in the news feed.}}" +msgstr "{0, plural, one {현재 뉴스 피드에 링크 #개가 있습니다.} other {현재 뉴스 피드에 링크 #개가 있습니다.}}" + #. placeholder {0}: count() #: src/routes/(root)/admin/media.tsx:172 msgid "{0, plural, one {# orphan medium can be deleted.} other {# orphan media can be deleted.}}" @@ -77,7 +82,7 @@ msgid "{0, plural, one {# voter} other {# voters}}" msgstr "{0, plural, other {투표자 #명}}" #. placeholder {0}: edge.node.tags.length - 3 -#: src/routes/(root)/[handle]/drafts/index.tsx:293 +#: src/routes/(root)/[handle]/drafts/index.tsx:295 msgid "{0, plural, one {+1 more} other {+# more}}" msgstr "{0, plural, other {외 #개}}" @@ -87,7 +92,7 @@ msgid "{0, plural, one {Deleted # orphan medium.} other {Deleted # orphan media. msgstr "{0, plural, other {연결되지 않은 미디어 #개를 삭제했습니다.}}" #. placeholder {0}: account.invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:311 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, other {Hackers' Pub에 친구를 초대하세요. 최대 #명까지 초대할 수 있습니다.}}" @@ -96,13 +101,18 @@ msgstr "{0, plural, other {Hackers' Pub에 친구를 초대하세요. 최대 # msgid "{0, plural, one {Load # more reactor} other {Load # more reactors}}" msgstr "{0, plural, one {반응자 #명 더 불러오기} other {반응자 #명 더 불러오기}}" +#. placeholder {0}: result.linksUpdated! +#: src/routes/(root)/admin/news.tsx:190 +msgid "{0, plural, one {Recomputed # link.} other {Recomputed # links.}}" +msgstr "{0, plural, one {링크 #개의 점수를 다시 계산했습니다.} other {링크 #개의 점수를 다시 계산했습니다.}}" + #. placeholder {0}: result.accountsAffected! #: src/routes/(root)/admin/invitations.tsx:97 msgid "{0, plural, one {Regenerated invitations for # account.} other {Regenerated invitations for # accounts.}}" msgstr "{0, plural, other {#개의 계정에 초대장을 재발급했습니다.}}" #. placeholder {0}: account.inviteesCount.totalCount -#: src/routes/(root)/[handle]/settings/invite.tsx:438 +#: src/routes/(root)/[handle]/settings/invite.tsx:439 msgid "{0, plural, one {You have invited total # person so far.} other {You have invited total # people so far.}}" msgstr "{0, plural, other {지금까지 총 #명을 초대하셨습니다.}}" @@ -172,8 +182,23 @@ msgstr "{0} 님 외 {1}명이 회원님이 공유한 콘텐츠를 업데이트 msgid "{0} followed you" msgstr "{0} 님이 팔로했습니다" +#. placeholder {0}: s.sourceBreakdown.bluesky +#: src/components/NewsStoryHeader.tsx:124 +msgid "{0} from Bluesky" +msgstr "Bluesky에서 {0}번" + +#. placeholder {0}: s.sourceBreakdown.local +#: src/components/NewsStoryHeader.tsx:118 +msgid "{0} from Hackers' Pub" +msgstr "Hackers' Pub에서 {0}번" + +#. placeholder {0}: s.sourceBreakdown.remote +#: src/components/NewsStoryHeader.tsx:121 +msgid "{0} from the fediverse" +msgstr "연합우주에서 {0}번" + #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:361 +#: src/routes/(root)/[handle]/settings/invite.tsx:362 msgid "{0} is already a member of Hackers' Pub." msgstr "{0} 님은 이미 Hackers' Pub의 회원입니다." @@ -229,47 +254,57 @@ msgstr "{0} 님이 회원님이 공유한 콘텐츠를 업데이트했습니다" #. placeholder {0}: post.actor.rawName ?? post.actor.username #. placeholder {1}: post.excerpt #. placeholder {1}: title() -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:237 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:283 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:238 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:284 msgid "{0}: {1}" msgstr "{0}: {1}" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/articles.tsx:86 -#: src/routes/(root)/[handle]/(profile)/articles.tsx:90 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:87 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:91 msgid "{0}'s articles" msgstr "{0} 님의 게시글" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/followers.tsx:77 -#: src/routes/(root)/[handle]/(profile)/followers.tsx:80 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:78 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:81 msgid "{0}'s followers" msgstr "{0} 님의 팔로워" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/following.tsx:77 -#: src/routes/(root)/[handle]/(profile)/following.tsx:80 +#: src/routes/(root)/[handle]/(profile)/following.tsx:78 +#: src/routes/(root)/[handle]/(profile)/following.tsx:81 msgid "{0}'s following" msgstr "{0} 님의 팔로잉" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/notes.tsx:86 -#: src/routes/(root)/[handle]/(profile)/notes.tsx:90 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:87 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:91 msgid "{0}'s notes" msgstr "{0} 님의 단문" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/shares.tsx:86 -#: src/routes/(root)/[handle]/(profile)/shares.tsx:90 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:87 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:91 msgid "{0}'s shares" msgstr "{0} 님의 공유" +#: src/components/NewsStoryCard.tsx:107 +#: src/components/NewsStoryHeader.tsx:54 +msgid "{count, plural, one {# opinion} other {# opinions}}" +msgstr "{count, plural, other {의견 #개}}" + +#: src/components/NewsStoryCard.tsx:51 +#: src/components/NewsStoryHeader.tsx:53 +#~ msgid "{count, plural, one {# share} other {# shares}}" +#~ msgstr "{count, plural, one {공유 #번} other {공유 #번}}" + #: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:231 #: src/routes/(root)/[handle]/[noteId]/reactions.tsx:253 #~ msgid "+{0} more reactor(s) not shown" #~ msgstr "+{0}명 더 (표시되지 않음)" -#: src/routes/(root)/[handle]/settings/index.tsx:579 +#: src/routes/(root)/[handle]/settings/index.tsx:580 msgid "A name for the link that will be displayed on your profile, e.g., GitHub." msgstr "프로필에 표시될 링크의 이름. 예: GitHub." @@ -278,11 +313,11 @@ msgid "A sign-in link has been sent to your email. Please check your inbox (or s msgstr "로그인 링크가 이메일로 전송되었습니다. 받은 편지함(또는 스팸 폴더)을 확인해주세요." #: src/components/admin/AdminAccountsTable.tsx:224 -#: src/components/AppSidebar.tsx:479 +#: src/components/AppSidebar.tsx:502 msgid "Account" msgstr "계정" -#: src/components/AppSidebar.tsx:750 +#: src/components/AppSidebar.tsx:773 #: src/routes/(root)/admin/index.tsx:95 msgid "Accounts" msgstr "계정" @@ -292,7 +327,8 @@ msgstr "계정" msgid "Actions" msgstr "동작" -#: src/routes/(root)/[handle]/settings/language.tsx:195 +#: src/routes/(root)/[handle]/settings/language.tsx:196 +#: src/routes/(root)/admin/news.tsx:421 msgid "Add" msgstr "추가" @@ -305,19 +341,23 @@ msgstr "{0} 추가" msgid "Add to sidebar" msgstr "사이드바에 추가" -#: src/components/AppSidebar.tsx:728 +#: src/routes/(root)/admin/news.tsx:421 +msgid "Adding…" +msgstr "추가 중…" + +#: src/components/AppSidebar.tsx:751 msgid "Admin" msgstr "관리" -#: src/routes/(root)/[handle]/bookmarks.tsx:135 +#: src/routes/(root)/[handle]/bookmarks.tsx:136 msgid "All" msgstr "전체" -#: src/components/NoteComposer.tsx:801 +#: src/components/NoteComposer.tsx:807 msgid "All images must finish uploading before posting" msgstr "투고하기 전에 모든 이미지 업로드를 완료해야 합니다." -#: src/components/NoteComposer.tsx:809 +#: src/components/NoteComposer.tsx:815 msgid "All images require alt text" msgstr "모든 이미지에 대체 텍스트가 필요합니다." @@ -329,17 +369,21 @@ msgstr "모든 언어" msgid "All notifications" msgstr "모든 알림" +#: src/components/NewsList.tsx:90 +msgid "All-time" +msgstr "전체 기간" + #: src/components/article-composer/ArticleComposerPublishFields.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:506 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:507 msgid "Allow automatic translation by AI" msgstr "AI 자동 번역 허용" #. placeholder {0}: index() + 1 -#: src/components/NoteComposer.tsx:1287 +#: src/components/NoteComposer.tsx:1298 msgid "Alt text for image {0}" msgstr "이미지 {0}의 대체 텍스트" -#: src/components/NoteComposer.tsx:1299 +#: src/components/NoteComposer.tsx:1310 msgid "Alt text for visually impaired people (required)" msgstr "시각장애인을 위한 대체 텍스트 (필수)" @@ -347,31 +391,31 @@ msgstr "시각장애인을 위한 대체 텍스트 (필수)" msgid "An error occurred during signup. Please try again." msgstr "가입중에 오류가 발생했습니다. 다시 시도해 주시기 바랍니다." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:280 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:281 msgid "An error occurred while registering your passkey." msgstr "패스키를 등록하는 중에 오류가 발생했습니다." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:328 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:329 msgid "An error occurred while revoking your passkey." msgstr "패스키를 취소하는 중에 오류가 발생했습니다." -#: src/routes/(root)/[handle]/settings/preferences.tsx:143 +#: src/routes/(root)/[handle]/settings/preferences.tsx:144 msgid "An error occurred while saving your preferences. Please try again, or contact support if the problem persists." msgstr "환경 설정 저장 중 오류가 발생했습니다. 다시 시도하거나 문제가 지속되면 지원팀에 문의해 주세요." -#: src/routes/(root)/[handle]/settings/language.tsx:162 +#: src/routes/(root)/[handle]/settings/language.tsx:163 msgid "An error occurred while saving your preferred languages. Please try again, or contact support if the problem persists." msgstr "언어 설정 저장 중 오류가 발생했습니다. 다시 시도하거나 문제가 지속되면 지원팀에 문의해 주세요." -#: src/routes/(root)/[handle]/settings/index.tsx:296 -#: src/routes/(root)/[handle]/settings/index.tsx:337 +#: src/routes/(root)/[handle]/settings/index.tsx:297 +#: src/routes/(root)/[handle]/settings/index.tsx:338 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "설정 저장 중 오류가 발생했습니다. 다시 시도하거나 문제가 지속되면 지원팀에 문의하세요." -#: src/routes/(root)/[handle]/invite/[id].tsx:198 -#: src/routes/(root)/[handle]/settings/invite.tsx:270 -#: src/routes/(root)/[handle]/settings/invite.tsx:551 -#: src/routes/(root)/[handle]/settings/invite.tsx:601 +#: src/routes/(root)/[handle]/invite/[id].tsx:203 +#: src/routes/(root)/[handle]/settings/invite.tsx:271 +#: src/routes/(root)/[handle]/settings/invite.tsx:552 +#: src/routes/(root)/[handle]/settings/invite.tsx:602 msgid "An unexpected error occurred. Please try again later." msgstr "예상치 못한 오류가 발생했습니다. 나중에 다시 시도해주세요." @@ -386,7 +430,7 @@ msgstr "누구나 인용 가능" msgid "Are you sure you want to block {0} ({1})? They won't be able to follow you or see your posts." msgstr "{0} 님({1})을 차단하시겠습니까? 상대방은 회원님을 팔로하거나 콘텐츠를 볼 수 없게 됩니다." -#: src/routes/(root)/[handle]/drafts/index.tsx:156 +#: src/routes/(root)/[handle]/drafts/index.tsx:157 msgid "Are you sure you want to delete \"{draftTitle}\"? This action cannot be undone." msgstr "「{draftTitle}」을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다." @@ -395,7 +439,7 @@ msgid "Are you sure you want to delete this draft? This action cannot be undone. msgstr "이 초고를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다." #. placeholder {0}: passkeyToRevoke()?.name -#: src/routes/(root)/[handle]/settings/passkeys.tsx:517 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:518 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." msgstr "{0} 패스키를 취소하시겠습니까? 이 패스키를 사용하여 계정에 로그인할 수 없게 됩니다." @@ -405,8 +449,8 @@ msgstr "{0} 패스키를 취소하시겠습니까? 이 패스키를 사용하여 msgid "Are you sure you want to unblock {0} ({1})? They will be able to follow you and see your posts." msgstr "{0} 님({1})의 차단을 해제하시겠습니까? 상대방은 회원님을 팔로하고 콘텐츠를 볼 수 있게 됩니다." -#: src/routes/(root)/[handle]/drafts/index.tsx:243 #: src/routes/(root)/[handle]/drafts/index.tsx:245 +#: src/routes/(root)/[handle]/drafts/index.tsx:247 msgid "Article drafts" msgstr "게시글 초고" @@ -414,7 +458,7 @@ msgstr "게시글 초고" msgid "Article published" msgstr "게시글을 공개했습니다" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:333 msgid "Article updated" msgstr "게시글이 수정되었습니다" @@ -423,21 +467,21 @@ msgid "article-url-slug" msgstr "게시글-url-슬러그" #: src/components/ProfileTabs.tsx:51 -#: src/routes/(root)/[handle]/bookmarks.tsx:136 +#: src/routes/(root)/[handle]/bookmarks.tsx:137 msgid "Articles" msgstr "게시글" -#: src/components/AppSidebar.tsx:308 +#: src/components/AppSidebar.tsx:331 msgid "Articles only" msgstr "게시글만" #. placeholder {0}: "CHANGED" -#: src/routes/(root)/[handle]/settings/index.tsx:454 +#: src/routes/(root)/[handle]/settings/index.tsx:455 msgid "As you have already changed it {0}, you can't change it again." msgstr "이미 {0} 변경하였기 때문에 다시 변경할 수 없습니다." -#: src/components/NoteComposer.tsx:1173 -#: src/components/NoteComposer.tsx:1174 +#: src/components/NoteComposer.tsx:1184 +#: src/components/NoteComposer.tsx:1185 msgid "Attach image" msgstr "이미지 첨부" @@ -445,20 +489,20 @@ msgstr "이미지 첨부" msgid "Authenticating…" msgstr "인증중…" -#: src/components/NoteComposer.tsx:1318 +#: src/components/NoteComposer.tsx:1329 msgid "Auto-fill" msgstr "자동 입력" -#: src/components/NoteComposer.tsx:1311 -#: src/components/NoteComposer.tsx:1312 +#: src/components/NoteComposer.tsx:1322 +#: src/components/NoteComposer.tsx:1323 msgid "Auto-fill alt text" msgstr "대체 텍스트 자동 입력" -#: src/routes/(root)/[handle]/settings/index.tsx:351 +#: src/routes/(root)/[handle]/settings/index.tsx:352 msgid "Avatar" msgstr "프로필 사진" -#: src/routes/(root)/[handle]/settings/index.tsx:483 +#: src/routes/(root)/[handle]/settings/index.tsx:484 #: src/routes/(root)/sign/up/[token].tsx:403 msgid "Bio" msgstr "약력" @@ -477,11 +521,11 @@ msgstr "차단" msgid "Block user?" msgstr "사용자를 차단하시겠습니까?" -#: src/routes/(root)/[handle]/settings/blocks.tsx:98 +#: src/routes/(root)/[handle]/settings/blocks.tsx:99 msgid "Blocked accounts" msgstr "차단한 계정" -#: src/routes/(root)/[handle]/settings/blocks.tsx:100 +#: src/routes/(root)/[handle]/settings/blocks.tsx:101 msgid "Blocked accounts cannot follow you or see your posts. Unlike muting, blocking is federated to the blocked account's instance." msgstr "차단한 계정은 회원님을 팔로하거나 회원님의 콘텐츠를 볼 수 없습니다. 뮤트와 달리 차단은 차단된 상대의 인스턴스로 연합됩니다." @@ -494,8 +538,8 @@ msgstr "굵게" msgid "Bookmark" msgstr "북마크" -#: src/components/AppSidebar.tsx:618 -#: src/routes/(root)/[handle]/bookmarks.tsx:125 +#: src/components/AppSidebar.tsx:641 +#: src/routes/(root)/[handle]/bookmarks.tsx:126 msgid "Bookmarks" msgstr "북마크" @@ -524,24 +568,33 @@ msgstr "브라우저 알림을 껐습니다" msgid "Browser notifications enabled" msgstr "브라우저 알림을 켰습니다" +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:492 +msgid "Buried" +msgstr "매장됨" + +#: src/components/NewsStoryCard.tsx:258 +msgid "Bury" +msgstr "매장" + #: src/components/article-composer/ArticleComposerActions.tsx:48 -#: src/components/NoteComposer.tsx:1346 -#: src/components/NoteComposer.tsx:1347 -#: src/components/NoteComposer.tsx:1390 +#: src/components/NoteComposer.tsx:1357 +#: src/components/NoteComposer.tsx:1358 +#: src/components/NoteComposer.tsx:1401 #: src/components/PostActionMenu.tsx:367 #: src/components/ProfileActionMenu.tsx:401 #: src/components/ProfileActionMenu.tsx:402 #: src/components/QuotedNoteCard.tsx:248 #: src/components/RemoteFollowButton.tsx:218 #: src/components/RemoteFollowButton.tsx:280 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:522 -#: src/routes/(root)/[handle]/settings/index.tsx:398 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:521 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:399 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:522 #: src/routes/(root)/authorize_interaction.tsx:273 msgid "Cancel" msgstr "취소" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:347 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 msgid "Cannot change the language because translations already exist" msgstr "번역이 이미 존재하므로 언어를 변경할 수 없습니다" @@ -549,11 +602,11 @@ msgstr "번역이 이미 존재하므로 언어를 변경할 수 없습니다" msgid "Check again" msgstr "다시 확인" -#: src/routes/(root)/[handle]/invite/[id].tsx:244 +#: src/routes/(root)/[handle]/invite/[id].tsx:249 msgid "Check your email" msgstr "이메일을 확인해 주세요" -#: src/routes/(root)/[handle]/invite/[id].tsx:246 +#: src/routes/(root)/[handle]/invite/[id].tsx:251 msgid "Check your email to complete sign-up. We've sent a verification link to your email address." msgstr "가입을 완료하려면 이메일을 확인해 주세요. 이메일 주소로 인증 링크를 보내드렸습니다." @@ -561,7 +614,7 @@ msgstr "가입을 완료하려면 이메일을 확인해 주세요. 이메일 msgid "Checking…" msgstr "확인 중…" -#: src/routes/(root)/[handle]/settings/invite.tsx:386 +#: src/routes/(root)/[handle]/settings/invite.tsx:387 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "초대장을 받을 친구가 사용하는 언어를 선택하세요. 이 언어는 초대장에만 사용됩니다." @@ -569,20 +622,25 @@ msgstr "초대장을 받을 친구가 사용하는 언어를 선택하세요. msgid "Choose whether push notifications may include post excerpts. Generic notification text is used when previews are hidden." msgstr "푸시 알림에 콘텐츠 발췌문을 포함할지 선택하세요. 미리보기를 숨기면 일반 알림 문구가 사용됩니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:395 +#: src/routes/(root)/[handle]/invite/[id].tsx:400 msgid "Choose your preferred language for the verification email." msgstr "인증 이메일의 언어를 선택해 주세요." #: src/components/admin/AdminAccountsTable.tsx:213 +#: src/routes/(root)/admin/news.tsx:501 msgid "Clear" msgstr "지우기" +#: src/components/NewsStoryCard.tsx:264 +msgid "Clear penalty" +msgstr "패널티 해제" + #: src/components/WebPushNotificationSettings.tsx:395 msgid "Clicking a notification opens your notifications page." msgstr "알림을 누르면 알림 페이지가 열립니다." #: src/components/ImageLightbox.tsx:74 -#: src/routes/(root)/[handle]/settings/invite.tsx:663 +#: src/routes/(root)/[handle]/settings/invite.tsx:664 msgid "Close" msgstr "닫기" @@ -594,7 +652,7 @@ msgstr "닫힘" msgid "Code" msgstr "코드" -#: src/components/AppSidebar.tsx:977 +#: src/components/AppSidebar.tsx:1023 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/sign/up/[token].tsx:458 msgid "Code of conduct" @@ -604,18 +662,18 @@ msgstr "행동 강령" #~ msgid "Comments ({0})" #~ msgstr "댓글 ({0})" -#: src/components/AppSidebar.tsx:819 +#: src/components/AppSidebar.tsx:865 #: src/components/FloatingComposeButton.tsx:61 msgid "Compose" msgstr "작성" #: src/components/article-composer/ArticleComposerForm.tsx:65 -#: src/components/NoteComposer.tsx:1122 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:392 +#: src/components/NoteComposer.tsx:1133 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:393 msgid "Content" msgstr "내용" -#: src/components/NoteComposer.tsx:791 +#: src/components/NoteComposer.tsx:797 msgid "Content cannot be empty" msgstr "내용을 입력하세요" @@ -627,15 +685,15 @@ msgstr "브라우저에서 계속하기" msgid "Controls who can quote this article on their timeline." msgstr "타임라인에서 이 게시글을 인용할 수 있는 대상을 설정합니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:611 +#: src/routes/(root)/[handle]/settings/invite.tsx:612 msgid "Copied" msgstr "복사됨" -#: src/routes/(root)/[handle]/settings/invite.tsx:705 +#: src/routes/(root)/[handle]/settings/invite.tsx:706 msgid "Copy" msgstr "복사" -#: src/routes/(root)/[handle]/settings/invite.tsx:618 +#: src/routes/(root)/[handle]/settings/invite.tsx:619 msgid "Could not copy the link to the clipboard." msgstr "클립보드에 링크를 복사할 수 없습니다." @@ -655,23 +713,23 @@ msgstr "인용을 취소하지 못했습니다" msgid "Could not vote on this poll" msgstr "이 투표에 참여할 수 없습니다" -#: src/components/AppSidebar.tsx:865 +#: src/components/AppSidebar.tsx:911 #: src/components/FloatingComposeButton.tsx:123 msgid "Create article" msgstr "게시글 작성" -#: src/routes/(root)/[handle]/settings/invite.tsx:834 +#: src/routes/(root)/[handle]/settings/invite.tsx:835 msgid "Create invitation link" msgstr "초대 링크 생성" -#: src/components/AppSidebar.tsx:841 +#: src/components/AppSidebar.tsx:887 #: src/components/FloatingComposeButton.tsx:99 -#: src/components/NoteComposeModal.tsx:72 -#: src/components/NoteComposer.tsx:1407 +#: src/components/NoteComposeModal.tsx:73 +#: src/components/NoteComposer.tsx:1418 msgid "Create note" msgstr "단문 작성" -#: src/routes/(root)/[handle]/settings/invite.tsx:628 +#: src/routes/(root)/[handle]/settings/invite.tsx:629 msgid "Create shareable invitation links. Each link can be used multiple times until the invitation count runs out or the link expires." msgstr "공유 가능한 초대 링크를 생성합니다. 각 링크는 초대 횟수가 소진되거나 유효기간이 만료될 때까지 여러 번 사용할 수 있습니다." @@ -680,7 +738,7 @@ msgid "Created" msgstr "가입일" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:426 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:427 msgid "Created {0}" msgstr "{0}에 생성됨" @@ -688,16 +746,16 @@ msgstr "{0}에 생성됨" msgid "Creating account…" msgstr "계정을 생성하는 중…" -#: src/components/NoteComposer.tsx:1408 -#: src/routes/(root)/[handle]/settings/invite.tsx:833 +#: src/components/NoteComposer.tsx:1419 +#: src/routes/(root)/[handle]/settings/invite.tsx:834 msgid "Creating…" msgstr "작성 중…" -#: src/routes/(root)/[handle]/settings/index.tsx:405 +#: src/routes/(root)/[handle]/settings/index.tsx:406 msgid "Crop" msgstr "자르기" -#: src/routes/(root)/[handle]/settings/index.tsx:378 +#: src/routes/(root)/[handle]/settings/index.tsx:379 msgid "Crop your new avatar" msgstr "새 프로필 사진 자르기" @@ -711,22 +769,22 @@ msgstr "기준 시각:" msgid "CW" msgstr "CW" -#: src/routes/(root)/[handle]/settings/preferences.tsx:192 +#: src/routes/(root)/[handle]/settings/preferences.tsx:193 msgid "Default note privacy" msgstr "기본 단문 공개 범위" -#: src/routes/(root)/[handle]/settings/preferences.tsx:217 +#: src/routes/(root)/[handle]/settings/preferences.tsx:218 msgid "Default quote permission" msgstr "기본 인용 권한" -#: src/routes/(root)/[handle]/settings/preferences.tsx:204 +#: src/routes/(root)/[handle]/settings/preferences.tsx:205 msgid "Default share privacy" msgstr "기본 공유 공개 범위" #: src/components/PostActionMenu.tsx:353 #: src/components/PostActionMenu.tsx:373 -#: src/routes/(root)/[handle]/drafts/index.tsx:320 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/drafts/index.tsx:322 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 msgid "Delete" msgstr "삭제" @@ -744,11 +802,20 @@ msgid "Delete post?" msgstr "콘텐츠를 삭제할까요?" #: src/components/article-composer/ArticleComposerActions.tsx:21 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 #: src/routes/(root)/admin/media.tsx:187 msgid "Deleting…" msgstr "삭제 중…" +#: src/components/NewsStoryCard.tsx:252 +msgid "Demote" +msgstr "강등" + +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:493 +msgid "Demoted" +msgstr "강등됨" + #: src/components/WebPushNotificationSettings.tsx:431 msgid "Disable" msgstr "끄기" @@ -757,11 +824,11 @@ msgstr "끄기" msgid "Disabling…" msgstr "끄는 중…" -#: src/components/NoteComposeModal.tsx:137 +#: src/components/NoteComposeModal.tsx:138 msgid "Discard" msgstr "폐기" -#: src/components/NoteComposeModal.tsx:126 +#: src/components/NoteComposeModal.tsx:127 msgid "Discard draft?" msgstr "초고를 폐기할까요?" @@ -769,11 +836,15 @@ msgstr "초고를 폐기할까요?" msgid "Discard unsaved changes - are you sure?" msgstr "저장하지 않은 변경 사항을 삭제하시겠습니까?" +#: src/components/NewsStoryCard.tsx:145 +#~ msgid "Discussion" +#~ msgstr "토론" + #: src/components/WebPushPromptBanner.tsx:269 msgid "Dismiss" msgstr "닫기" -#: src/routes/(root)/[handle]/settings/index.tsx:467 +#: src/routes/(root)/[handle]/settings/index.tsx:468 #: src/routes/(root)/sign/up/[token].tsx:377 msgid "Display name" msgstr "이름" @@ -782,12 +853,12 @@ msgstr "이름" msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "계정이 필요하신가요? Hackers' Pub은 초대 전용입니다. 친구에게 초대를 요청해주세요." -#: src/components/NoteComposer.tsx:742 +#: src/components/NoteComposer.tsx:748 msgid "Do you want to quote this link?" msgstr "이 링크를 인용하시겠습니까?" #: src/components/article-composer/ArticleComposerContext.tsx:450 -#: src/routes/(root)/[handle]/drafts/index.tsx:175 +#: src/routes/(root)/[handle]/drafts/index.tsx:176 msgid "Draft deleted" msgstr "초고를 삭제했습니다" @@ -803,7 +874,7 @@ msgstr "초고를 찾을 수 없습니다" msgid "Draft saved" msgstr "초고로 저장했습니다" -#: src/routes/(root)/[handle]/settings/index.tsx:381 +#: src/routes/(root)/[handle]/settings/index.tsx:382 msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "유지하려는 영역을 드래그하여 선택한 다음 “자르기”를 클릭하여 프로필 사진을 업데이트하세요." @@ -812,25 +883,25 @@ msgid "e.g., @user@mastodon.social" msgstr "예: @user@mastodon.social" #: src/components/PostActionMenu.tsx:328 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:695 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:696 msgid "Edit" msgstr "수정" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:374 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:375 msgid "Edit article" msgstr "게시글 수정" -#: src/routes/(root)/[handle]/drafts/[id].tsx:73 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/[id].tsx:75 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "Edit draft" msgstr "초고 수정" -#: src/components/NoteComposeModal.tsx:69 +#: src/components/NoteComposeModal.tsx:70 msgid "Edit note" msgstr "단문 수정" -#: src/routes/(root)/[handle]/invite/[id].tsx:366 -#: src/routes/(root)/[handle]/settings/invite.tsx:334 +#: src/routes/(root)/[handle]/invite/[id].tsx:371 +#: src/routes/(root)/[handle]/settings/invite.tsx:335 #: src/routes/(root)/sign/up/[token].tsx:311 msgid "Email address" msgstr "이메일 주소" @@ -857,7 +928,7 @@ msgstr "종료됨" msgid "Ends" msgstr "종료까지" -#: src/routes/(root)/[handle]/invite/[id].tsx:279 +#: src/routes/(root)/[handle]/invite/[id].tsx:284 msgid "Enter your email address below to get started." msgstr "아래에 이메일 주소를 입력하여 시작하세요." @@ -879,47 +950,63 @@ msgstr "로그인하려면 아래에 이메일 또는 아이디를 입력해주 #: src/components/article-composer/ArticleComposerContext.tsx:466 #: src/components/article-composer/ArticleComposerContext.tsx:474 #: src/components/article-composer/ArticleComposerForm.tsx:35 -#: src/components/NoteComposer.tsx:601 -#: src/components/NoteComposer.tsx:648 -#: src/components/NoteComposer.tsx:790 -#: src/components/NoteComposer.tsx:800 -#: src/components/NoteComposer.tsx:808 -#: src/components/NoteComposer.tsx:842 -#: src/components/NoteComposer.tsx:850 -#: src/components/NoteComposer.tsx:858 -#: src/components/NoteComposer.tsx:892 -#: src/components/NoteComposer.tsx:900 -#: src/components/NoteComposer.tsx:908 -#: src/components/NoteComposer.tsx:965 +#: src/components/NoteComposer.tsx:607 +#: src/components/NoteComposer.tsx:654 +#: src/components/NoteComposer.tsx:796 +#: src/components/NoteComposer.tsx:806 +#: src/components/NoteComposer.tsx:814 +#: src/components/NoteComposer.tsx:848 +#: src/components/NoteComposer.tsx:856 +#: src/components/NoteComposer.tsx:864 +#: src/components/NoteComposer.tsx:903 +#: src/components/NoteComposer.tsx:911 +#: src/components/NoteComposer.tsx:919 +#: src/components/NoteComposer.tsx:976 #: src/components/QuotedNoteCard.tsx:270 #: src/components/QuotedNoteCard.tsx:278 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:257 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:345 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:355 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:364 -#: src/routes/(root)/[handle]/drafts/index.tsx:182 -#: src/routes/(root)/[handle]/drafts/index.tsx:191 -#: src/routes/(root)/[handle]/drafts/index.tsx:199 -#: src/routes/(root)/[handle]/invite/[id].tsx:196 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:258 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:346 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/drafts/index.tsx:183 +#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:200 +#: src/routes/(root)/[handle]/invite/[id].tsx:201 #: src/routes/(root)/sign/up/[token].tsx:269 msgid "Error" msgstr "오류" +#: src/routes/(root)/admin/news.tsx:382 +msgid "Excluded URL patterns" +msgstr "제외 URL 패턴" + +#: src/routes/(root)/admin/news.tsx:233 +msgid "Exclusion pattern added." +msgstr "제외 패턴을 추가했습니다." + +#: src/routes/(root)/admin/news.tsx:268 +msgid "Exclusion pattern removed." +msgstr "제외 패턴을 제거했습니다." + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/invite/[id].tsx:334 -#: src/routes/(root)/[handle]/settings/invite.tsx:759 +#: src/routes/(root)/[handle]/invite/[id].tsx:339 +#: src/routes/(root)/[handle]/settings/invite.tsx:760 msgid "Expires {0}" msgstr "{0}에 만료" -#: src/routes/(root)/[handle]/settings/invite.tsx:806 +#: src/routes/(root)/[handle]/settings/invite.tsx:807 msgid "Expiry" msgstr "유효기간" -#: src/routes/(root)/[handle]/settings/invite.tsx:391 -#: src/routes/(root)/[handle]/settings/invite.tsx:796 +#: src/routes/(root)/[handle]/settings/invite.tsx:392 +#: src/routes/(root)/[handle]/settings/invite.tsx:797 msgid "Extra message" msgstr "추가 메시지" +#: src/routes/(root)/admin/news.tsx:252 +msgid "Failed to add exclusion pattern." +msgstr "제외 패턴을 추가하지 못했습니다." + #: src/components/HashtagActionBar.tsx:192 #: src/components/HashtagActionBar.tsx:199 msgid "Failed to add to sidebar" @@ -935,17 +1022,21 @@ msgstr "사용자를 차단하지 못했습니다" msgid "Failed to bookmark" msgstr "북마크 실패" -#: src/routes/(root)/[handle]/settings/invite.tsx:617 +#: src/routes/(root)/admin/news.tsx:310 +msgid "Failed to clear penalty." +msgstr "패널티를 해제하지 못했습니다." + +#: src/routes/(root)/[handle]/settings/invite.tsx:618 msgid "Failed to copy" msgstr "복사 실패" -#: src/routes/(root)/[handle]/settings/invite.tsx:539 -#: src/routes/(root)/[handle]/settings/invite.tsx:549 +#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:550 msgid "Failed to create invitation link" msgstr "초대 링크 생성에 실패했습니다" -#: src/routes/(root)/[handle]/settings/invite.tsx:588 -#: src/routes/(root)/[handle]/settings/invite.tsx:599 +#: src/routes/(root)/[handle]/settings/invite.tsx:589 +#: src/routes/(root)/[handle]/settings/invite.tsx:600 msgid "Failed to delete invitation link" msgstr "초대 링크 삭제에 실패했습니다" @@ -979,7 +1070,7 @@ msgstr "브라우저 알림을 켜지 못했습니다" msgid "Failed to follow" msgstr "팔로에 실패했습니다" -#: src/components/NoteComposer.tsx:966 +#: src/components/NoteComposer.tsx:977 msgid "Failed to generate alt text" msgstr "대체 텍스트 생성에 실패했습니다" @@ -995,7 +1086,7 @@ msgstr "게시글 불러오기 실패. 클릭하여 재시도하세요" msgid "Failed to load more bookmarks; click to retry" msgstr "북마크 불러오기 실패. 클릭하여 재시도하세요" -#: src/routes/(root)/[handle]/drafts/index.tsx:345 +#: src/routes/(root)/[handle]/drafts/index.tsx:347 msgid "Failed to load more drafts; click to retry" msgstr "초고 불러오기 실패, 클릭하여 재시도" @@ -1007,7 +1098,7 @@ msgstr "팔로워 불러오기 실패. 클릭하여 재시도하세요" msgid "Failed to load more following; click to retry" msgstr "팔로잉 불러오기 실패. 클릭하여 재시도하세요" -#: src/routes/(root)/[handle]/settings/invite.tsx:947 +#: src/routes/(root)/[handle]/settings/invite.tsx:948 msgid "Failed to load more invitees; click to retry" msgstr "초대받은 사람들을 불러오지 못했습니다. 클릭하여 다시 시도" @@ -1019,7 +1110,7 @@ msgstr "단문 불러오기 실패. 클릭하여 재시도하세요" msgid "Failed to load more notifications; click to retry" msgstr "알림을 더 불러오지 못했습니다. 클릭해서 다시 시도하세요" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:495 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 msgid "Failed to load more passkeys; click to retry" msgstr "패스키를 더 불러오지 못했습니다. 클릭해서 다시 시도하세요" @@ -1031,8 +1122,8 @@ msgstr "패스키를 더 불러오지 못했습니다. 클릭해서 다시 시 msgid "Failed to load more posts; click to retry" msgstr "콘텐츠 불러오기 실패. 클릭하여 재시도하세요" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:194 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:201 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:195 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:202 msgid "Failed to load more quotes; click to retry" msgstr "인용을 더 불러오지 못했습니다. 다시 시도하려면 클릭하세요" @@ -1040,28 +1131,31 @@ msgstr "인용을 더 불러오지 못했습니다. 다시 시도하려면 클 msgid "Failed to load more reactors; click to retry" msgstr "반응자를 더 불러오지 못했습니다. 다시 시도하려면 클릭하세요" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:670 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:671 msgid "Failed to load more replies; click to retry" msgstr "댓글을 더 불러오지 못했습니다. 클릭해서 다시 시도하세요" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:204 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:213 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:205 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:214 msgid "Failed to load more shares; click to retry" msgstr "공유를 더 불러오지 못했습니다. 다시 시도하려면 클릭하세요" -#: src/components/BlockedAccountsList.tsx:199 -#: src/components/MutedAccountsList.tsx:196 +#: src/components/AccountListBase.tsx:112 msgid "Failed to load more; click to retry" msgstr "더 불러오지 못했습니다. 클릭하여 다시 시도하세요" -#: src/components/NoteComposer.tsx:1014 +#: src/components/NoteComposer.tsx:1025 msgid "Failed to load post" msgstr "콘텐츠를 불러오지 못했습니다" -#: src/components/NoteComposer.tsx:1065 +#: src/components/NoteComposer.tsx:1076 msgid "Failed to load quoted post" msgstr "인용 콘텐츠를 불러오지 못했습니다" +#: src/components/NewsDiscussionThread.tsx:408 +msgid "Failed to load replies; click to retry" +msgstr "댓글을 불러오지 못했습니다. 클릭해서 다시 시도하세요" + #: src/components/RemoteFollowButton.tsx:126 msgid "Failed to look up user." msgstr "사용자 조회에 실패했습니다." @@ -1087,11 +1181,15 @@ msgstr "콘텐츠를 고정하지 못했습니다" msgid "Failed to react" msgstr "반응 실패" +#: src/routes/(root)/admin/news.tsx:212 +msgid "Failed to recompute news scores." +msgstr "뉴스 점수를 다시 계산하지 못했습니다." + #: src/routes/(root)/admin/invitations.tsx:125 msgid "Failed to regenerate invitations." msgstr "초대장을 재발급하지 못했습니다." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:277 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:278 msgid "Failed to register passkey" msgstr "패스키 등록 실패" @@ -1100,40 +1198,44 @@ msgstr "패스키 등록 실패" msgid "Failed to remove bookmark" msgstr "북마크 해제 실패" +#: src/routes/(root)/admin/news.tsx:281 +msgid "Failed to remove exclusion pattern." +msgstr "제외 패턴을 제거하지 못했습니다." + #: src/components/HashtagActionBar.tsx:212 #: src/components/HashtagActionBar.tsx:219 msgid "Failed to remove from sidebar" msgstr "사이드바에서 제거하지 못했습니다." #: src/components/MarkdownEditor.tsx:192 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:461 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 msgid "Failed to render preview" msgstr "미리보기 렌더링에 실패했습니다" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:319 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:325 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:320 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "Failed to revoke passkey" msgstr "패스키 취소 실패" -#: src/routes/(root)/[handle]/settings/language.tsx:160 +#: src/routes/(root)/[handle]/settings/language.tsx:161 msgid "Failed to save language preferences" msgstr "언어 설정 저장 실패" -#: src/routes/(root)/[handle]/settings/preferences.tsx:141 +#: src/routes/(root)/[handle]/settings/preferences.tsx:142 msgid "Failed to save preferences" msgstr "환경 설정 저장 실패" -#: src/routes/(root)/[handle]/settings/index.tsx:293 -#: src/routes/(root)/[handle]/settings/index.tsx:335 +#: src/routes/(root)/[handle]/settings/index.tsx:294 +#: src/routes/(root)/[handle]/settings/index.tsx:336 msgid "Failed to save settings" msgstr "설정 저장 실패" -#: src/routes/(root)/[handle]/invite/[id].tsx:185 +#: src/routes/(root)/[handle]/invite/[id].tsx:190 msgid "Failed to send email" msgstr "이메일 전송에 실패했습니다" -#: src/routes/(root)/[handle]/settings/invite.tsx:248 -#: src/routes/(root)/[handle]/settings/invite.tsx:268 +#: src/routes/(root)/[handle]/settings/invite.tsx:249 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "Failed to send invitation" msgstr "초대장 발송에 실패했습니다" @@ -1147,8 +1249,8 @@ msgstr "콘텐츠 공유 실패" msgid "Failed to sign out: {0}" msgstr "로그아웃 실패: {0}" -#: src/components/BlockedAccountsList.tsx:98 -#: src/components/BlockedAccountsList.tsx:104 +#: src/components/BlockedAccountsList.tsx:96 +#: src/components/BlockedAccountsList.tsx:102 #: src/components/ProfileActionMenu.tsx:258 #: src/components/ProfileActionMenu.tsx:264 msgid "Failed to unblock this user" @@ -1160,8 +1262,8 @@ msgstr "사용자 차단을 해제하지 못했습니다" msgid "Failed to unfollow" msgstr "언팔로에 실패했습니다" -#: src/components/MutedAccountsList.tsx:97 -#: src/components/MutedAccountsList.tsx:101 +#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:99 #: src/components/ProfileActionMenu.tsx:300 #: src/components/ProfileActionMenu.tsx:308 msgid "Failed to unmute this user" @@ -1177,11 +1279,16 @@ msgstr "콘텐츠 고정을 해제하지 못했습니다" msgid "Failed to unshare post" msgstr "공유 취소 실패" +#: src/components/NewsStoryCard.tsx:87 +#: src/components/NewsStoryCard.tsx:91 +msgid "Failed to update penalty." +msgstr "패널티를 변경하지 못했습니다." + #: src/components/WebPushNotificationSettings.tsx:354 msgid "Failed to update push notification privacy" msgstr "푸시 알림 개인정보 설정을 업데이트하지 못했습니다" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:366 msgid "Failed to update the article. Please try again." msgstr "게시글을 수정하지 못했습니다. 다시 시도해 주세요." @@ -1190,8 +1297,8 @@ msgstr "게시글을 수정하지 못했습니다. 다시 시도해 주세요." #~ msgstr "단문 수정에 실패했습니다. 다시 시도해 주세요." #: src/components/article-composer/ArticleComposerForm.tsx:38 -#: src/components/NoteComposer.tsx:651 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:260 +#: src/components/NoteComposer.tsx:657 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:261 msgid "Failed to upload image" msgstr "이미지 업로드에 실패했습니다" @@ -1199,7 +1306,7 @@ msgstr "이미지 업로드에 실패했습니다" msgid "Failed to vote" msgstr "투표에 실패했습니다" -#: src/components/AppSidebar.tsx:354 +#: src/components/AppSidebar.tsx:377 msgid "Fediverse" msgstr "연합우주" @@ -1207,10 +1314,14 @@ msgstr "연합우주" msgid "Fediverse handle" msgstr "연합우주 핸들" -#: src/components/AppSidebar.tsx:245 +#: src/components/AppSidebar.tsx:268 msgid "Feed" msgstr "피드" +#: src/components/NewsStoryHeader.tsx:113 +msgid "First shared" +msgstr "최초 공유" + #: src/components/FollowButton.tsx:195 #: src/components/HashtagActionBar.tsx:237 msgid "Follow" @@ -1253,7 +1364,7 @@ msgstr "당신을 팔로우합니다" msgid "Formatting" msgstr "서식" -#: src/components/NoteComposer.tsx:1337 +#: src/components/NoteComposer.tsx:1348 msgid "Generating…" msgstr "생성 중…" @@ -1261,14 +1372,14 @@ msgstr "생성 중…" msgid "Get browser notifications" msgstr "브라우저 알림 받기" -#: src/components/AppSidebar.tsx:1015 +#: src/components/AppSidebar.tsx:1061 msgid "GitHub repository" msgstr "GitHub 저장소" -#: src/routes/(root)/[handle]/bookmarks.tsx:108 -#: src/routes/(root)/[handle]/drafts/[id].tsx:59 -#: src/routes/(root)/[handle]/drafts/index.tsx:225 -#: src/routes/(root)/[handle]/drafts/new.tsx:69 +#: src/routes/(root)/[handle]/bookmarks.tsx:109 +#: src/routes/(root)/[handle]/drafts/[id].tsx:61 +#: src/routes/(root)/[handle]/drafts/index.tsx:227 +#: src/routes/(root)/[handle]/drafts/new.tsx:71 msgid "Go back" msgstr "돌아가기" @@ -1280,13 +1391,13 @@ msgstr "홈으로 가기" msgid "Go to Drafts" msgstr "초고로" -#: src/routes/(root)/[handle]/bookmarks.tsx:114 +#: src/routes/(root)/[handle]/bookmarks.tsx:115 msgid "Go to my bookmarks" msgstr "내 북마크로" -#: src/routes/(root)/[handle]/drafts/[id].tsx:64 -#: src/routes/(root)/[handle]/drafts/index.tsx:230 -#: src/routes/(root)/[handle]/drafts/new.tsx:74 +#: src/routes/(root)/[handle]/drafts/[id].tsx:66 +#: src/routes/(root)/[handle]/drafts/index.tsx:232 +#: src/routes/(root)/[handle]/drafts/new.tsx:76 msgid "Go to my drafts" msgstr "내 초고로" @@ -1294,8 +1405,8 @@ msgstr "내 초고로" msgid "Grants one extra invitation to the most active accounts (the top third by post count) since the last regeneration cutoff." msgstr "마지막 재발급 시점 이후 가장 활발한 계정(콘텐츠 수 상위 3분의 1)에게 초대장 1장을 추가로 부여합니다." -#: src/components/AppSidebar.tsx:323 -#: src/components/AppSidebar.tsx:446 +#: src/components/AppSidebar.tsx:346 +#: src/components/AppSidebar.tsx:469 #: src/routes/(root).tsx:134 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/markdown.tsx:40 @@ -1324,6 +1435,19 @@ msgstr "Hackers' Pub: 관리 · 초대" msgid "Hackers' Pub: Admin · Media" msgstr "Hackers' Pub: 관리 · 미디어" +#: src/routes/(root)/admin/news.tsx:320 +msgid "Hackers' Pub: Admin · News" +msgstr "Hackers' Pub: 관리 · 뉴스" + +#: src/routes/(root)/admin/news.tsx:124 +#~ msgid "Hackers' Pub: Admin · News scores" +#~ msgstr "Hackers' Pub: 관리 · 뉴스 점수" + +#: src/routes/(root)/news/[link_id]/index.tsx:56 +#: src/routes/(root)/news/index.tsx:37 +msgid "Hackers' Pub: News" +msgstr "Hackers' Pub: 뉴스" + #: src/routes/(root)/notifications.tsx:47 msgid "Hackers' Pub: Notifications" msgstr "Hackers' Pub: 알림" @@ -1346,6 +1470,10 @@ msgstr "제목 3" msgid "Hide" msgstr "숨기기" +#: src/routes/(root)/admin/news.tsx:384 +msgid "Hide links matching a URL pattern from the news feed list (every sort order). Their discussion pages stay reachable by direct link. Patterns use the URLPattern syntax, e.g. https://example.com/* or https://*.example.com/*." +msgstr "URL 패턴에 맞는 링크를 뉴스 피드 목록(모든 정렬)에서 숨깁니다. 해당 링크의 토론 페이지는 직접 URL로 접근할 수 있습니다. 패턴은 URLPattern 문법을 따릅니다. 예: https://example.com/* 또는 https://*.example.com/*." + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Hide preview" #~ msgstr "미리보기 숨기기" @@ -1358,23 +1486,23 @@ msgstr "숨기기" msgid "I have read and agree to the Code of conduct." msgstr "Hackers' Pub의 행동 강령을 읽고 동의합니다." -#: src/routes/(root)/[handle]/settings/preferences.tsx:186 +#: src/routes/(root)/[handle]/settings/preferences.tsx:187 msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "활성화하면 AI가 글의 요약을 생성합니다. 비활성화 시 글의 처음 몇 줄이 요약으로 사용됩니다." #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1013 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1014 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:548 msgid "If you have a fediverse account, you can reply to this article from your own instance. Search {0} on your instance and reply to it." msgstr "연합우주(fediverse) 계정이 있으시다면, 이 게시글에 댓글을 달 수 있습니다. 사용하시는 인스턴스의 검색창에 {0}로 검색하신 뒤, 해당 게시글에 댓글을 남기시면 됩니다." #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:393 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:394 msgid "If you have a fediverse account, you can reply to this note from your own instance. Search {0} on your instance and reply to it." msgstr "연합우주(fediverse) 계정이 있으시다면, 이 단문에 댓글을 달 수 있습니다. 사용하시는 인스턴스의 검색창에 {0}로 검색하신 뒤, 해당 단문에 댓글을 남기시면 됩니다." #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:471 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:472 msgid "If you have a fediverse account, you can reply to this post from your own instance. Search {0} on your instance and reply to it." msgstr "연합우주 계정이 있으시다면, 이 콘텐츠에 댓글을 달 수 있습니다. 사용하시는 인스턴스에서 {0}을 검색한 뒤 댓글을 남기세요." @@ -1397,42 +1525,42 @@ msgstr "올바른 연합우주 핸들 형식이 아닙니다." #: src/components/article-composer/ArticleComposerContext.tsx:321 #: src/components/article-composer/ArticleComposerContext.tsx:394 #: src/components/article-composer/ArticleComposerContext.tsx:459 -#: src/components/NoteComposer.tsx:843 -#: src/components/NoteComposer.tsx:893 -#: src/routes/(root)/[handle]/drafts/index.tsx:184 +#: src/components/NoteComposer.tsx:849 +#: src/components/NoteComposer.tsx:904 +#: src/routes/(root)/[handle]/drafts/index.tsx:185 msgid "Invalid input: {0}" msgstr "잘못된 입력: {0}" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:349 msgid "Invalid input: {inputPath}" msgstr "잘못된 입력: {inputPath}" #. placeholder {0}: link.inviter.name ?? link.inviter.username -#: src/routes/(root)/[handle]/invite/[id].tsx:237 +#: src/routes/(root)/[handle]/invite/[id].tsx:242 msgid "Invitation from {0}" msgstr "{0} 님의 초대" -#: src/routes/(root)/[handle]/settings/invite.tsx:379 +#: src/routes/(root)/[handle]/settings/invite.tsx:380 msgid "Invitation language" msgstr "초대장 언어" -#: src/routes/(root)/[handle]/settings/invite.tsx:531 +#: src/routes/(root)/[handle]/settings/invite.tsx:532 msgid "Invitation link created" msgstr "초대 링크가 생성되었습니다" -#: src/routes/(root)/[handle]/settings/invite.tsx:582 +#: src/routes/(root)/[handle]/settings/invite.tsx:583 msgid "Invitation link deleted" msgstr "초대 링크가 삭제되었습니다" -#: src/routes/(root)/[handle]/settings/invite.tsx:626 +#: src/routes/(root)/[handle]/settings/invite.tsx:627 msgid "Invitation links" msgstr "초대 링크" -#: src/routes/(root)/[handle]/settings/invite.tsx:258 +#: src/routes/(root)/[handle]/settings/invite.tsx:259 msgid "Invitation sent" msgstr "초대장이 발송되었습니다" -#: src/components/AppSidebar.tsx:773 +#: src/components/AppSidebar.tsx:796 #: src/routes/(root)/admin/invitations.tsx:148 msgid "Invitations" msgstr "초대" @@ -1441,13 +1569,13 @@ msgstr "초대" msgid "Invitations left" msgstr "남은 초대장" -#: src/components/AppSidebar.tsx:642 +#: src/components/AppSidebar.tsx:665 #: src/components/SettingsTabs.tsx:69 -#: src/routes/(root)/[handle]/settings/invite.tsx:295 +#: src/routes/(root)/[handle]/settings/invite.tsx:296 msgid "Invite" msgstr "초대" -#: src/routes/(root)/[handle]/settings/invite.tsx:304 +#: src/routes/(root)/[handle]/settings/invite.tsx:305 msgid "Invite a friend" msgstr "친구 초대하기" @@ -1463,23 +1591,27 @@ msgstr "초대한 사람" msgid "Italic" msgstr "기울임" -#: src/routes/(root)/[handle]/settings/index.tsx:474 +#: src/routes/(root)/[handle]/settings/index.tsx:475 msgid "John Doe" msgstr "홍길동" +#: src/components/NewsDiscussionComposer.tsx:41 +msgid "Join the discussion about this story." +msgstr "이 이야기에 대한 토론에 참여해 보세요." + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/settings/invite.tsx:921 +#: src/routes/(root)/[handle]/settings/invite.tsx:922 msgid "Joined on {0}" msgstr "{0}에 가입" -#: src/components/NoteComposeModal.tsx:132 +#: src/components/NoteComposeModal.tsx:133 msgid "Keep editing" msgstr "계속 작성" #: src/components/article-composer/ArticleComposerPublishFields.tsx:53 #: src/components/LanguageList.tsx:33 #: src/components/LanguageSelect.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:489 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:490 msgid "Language" msgstr "언어" @@ -1487,7 +1619,7 @@ msgstr "언어" msgid "Language code" msgstr "언어 코드" -#: src/routes/(root)/[handle]/settings/language.tsx:82 +#: src/routes/(root)/[handle]/settings/language.tsx:83 msgid "Language settings" msgstr "언어 설정" @@ -1495,16 +1627,25 @@ msgstr "언어 설정" msgid "Languages" msgstr "언어" +#: src/components/NewsStoryCard.tsx:205 +#: src/components/NewsStoryHeader.tsx:106 +msgid "Last active" +msgstr "마지막 활동" + #: src/components/admin/AdminAccountsTable.tsx:278 msgid "Last activity" msgstr "마지막 활동" +#: src/routes/(root)/admin/news.tsx:360 +msgid "Last recomputed:" +msgstr "마지막 계산:" + #: src/routes/(root)/admin/invitations.tsx:160 msgid "Last regenerated:" msgstr "마지막 재발급:" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:450 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:451 msgid "Last used {0}" msgstr "{0}에 마지막 사용" @@ -1520,21 +1661,28 @@ msgstr "링크 저자:" #~ msgid "Link author: " #~ msgstr "링크 저자: " -#: src/routes/(root)/[handle]/invite/[id].tsx:255 +#: src/routes/(root)/[handle]/invite/[id].tsx:260 msgid "Link expired" msgstr "링크가 만료되었습니다" -#: src/routes/(root)/[handle]/settings/index.tsx:560 +#: src/routes/(root)/[handle]/settings/index.tsx:561 msgid "Link name" msgstr "링크 이름" +#: src/routes/(root)/admin/news.tsx:465 +msgid "Links a moderator has demoted in the popular feed. Clear a penalty to restore a link's normal ranking." +msgstr "모더레이터가 인기 피드에서 강등한 링크입니다. 패널티를 해제하면 링크의 순위가 원래대로 돌아갑니다." + +#: src/routes/(root)/news/index.tsx:41 +msgid "Links circulating across the fediverse, ranked by how much they are being shared and discussed." +msgstr "연합우주 곳곳에서 퍼지는 링크를 공유·토론이 활발한 순으로 모아 보여줍니다." + #: src/components/ui/markdown-editor.tsx:291 msgid "List" msgstr "목록" -#: src/components/BlockedAccountsList.tsx:202 -#: src/components/MutedAccountsList.tsx:199 -#: src/routes/(root)/[handle]/drafts/index.tsx:348 +#: src/components/AccountListBase.tsx:115 +#: src/routes/(root)/[handle]/drafts/index.tsx:350 msgid "Load more" msgstr "더 보기" @@ -1558,7 +1706,7 @@ msgstr "팔로워 더 불러오기" msgid "Load more following" msgstr "팔로잉 더 불러오기" -#: src/routes/(root)/[handle]/settings/invite.tsx:950 +#: src/routes/(root)/[handle]/settings/invite.tsx:951 msgid "Load more invitees" msgstr "초대받은 사람 더 보기" @@ -1570,7 +1718,7 @@ msgstr "단문 더 불러오기" msgid "Load more notifications" msgstr "알림 더 불러오기" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:497 msgid "Load more passkeys" msgstr "패스키 더 불러오기" @@ -1582,8 +1730,9 @@ msgstr "패스키 더 불러오기" msgid "Load more posts" msgstr "콘텐츠 더 불러오기" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:197 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:204 +#: src/components/NewsDiscussionThread.tsx:378 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:205 msgid "Load more quotes" msgstr "인용 더 불러오기" @@ -1591,15 +1740,20 @@ msgstr "인용 더 불러오기" #~ msgid "Load more reactors (+{0})" #~ msgstr "반응자 더 불러오기 (+{0})" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:673 +#: src/components/NewsDiscussionThread.tsx:399 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:674 msgid "Load more replies" msgstr "댓글 더 불러오기" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:207 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:216 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:208 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:217 msgid "Load more shares" msgstr "공유 더 불러오기" +#: src/components/NewsList.tsx:139 +msgid "Load more stories" +msgstr "뉴스 더 불러오기" + #: src/components/article-composer/ArticleComposer.tsx:31 msgid "Loading draft…" msgstr "초고 불러오는 중…" @@ -1624,7 +1778,7 @@ msgstr "팔로워 불러오는 중…" msgid "Loading more following…" msgstr "팔로잉 불러오는 중…" -#: src/routes/(root)/[handle]/settings/invite.tsx:944 +#: src/routes/(root)/[handle]/settings/invite.tsx:945 msgid "Loading more invitees…" msgstr "초대받은 사람들을 불러오는 중…" @@ -1636,7 +1790,7 @@ msgstr "단문 불러오는 중…" msgid "Loading more notifications" msgstr "알림을 더 불러오는 중…" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:493 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:494 msgid "Loading more passkeys…" msgstr "패스키를 더 불러오는 중…" @@ -1648,8 +1802,8 @@ msgstr "패스키를 더 불러오는 중…" msgid "Loading more posts…" msgstr "콘텐츠 불러오는 중…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:191 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:192 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:199 msgid "Loading more quotes…" msgstr "인용을 더 불러오는 중…" @@ -1657,21 +1811,28 @@ msgstr "인용을 더 불러오는 중…" msgid "Loading more reactors…" msgstr "반응자를 더 불러오는 중…" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:667 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:668 msgid "Loading more replies…" msgstr "댓글을 더 불러오는 중…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:201 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:210 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:202 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:211 msgid "Loading more shares…" msgstr "공유를 더 불러오는 중…" -#: src/components/BlockedAccountsList.tsx:196 -#: src/components/MutedAccountsList.tsx:193 +#: src/components/NewsDiscussion.tsx:79 +msgid "Loading more sharing posts…" +msgstr "공유 콘텐츠를 더 불러오는 중…" + +#: src/components/NewsList.tsx:141 +msgid "Loading more stories…" +msgstr "뉴스를 더 불러오는 중…" + +#: src/components/AccountListBase.tsx:109 msgid "Loading more…" msgstr "더 불러오는 중…" -#: src/components/NoteComposer.tsx:1066 +#: src/components/NoteComposer.tsx:1077 msgid "Loading quoted post…" msgstr "인용 콘텐츠를 불러오는 중…" @@ -1679,13 +1840,13 @@ msgstr "인용 콘텐츠를 불러오는 중…" msgid "Loading search results…" msgstr "검색 결과를 불러오는 중…" -#: src/components/NoteComposer.tsx:1015 -#: src/routes/(root)/[handle]/drafts/index.tsx:342 +#: src/components/NoteComposer.tsx:1026 +#: src/routes/(root)/[handle]/drafts/index.tsx:344 #: src/routes/(root)/sign/up/[token].tsx:465 msgid "Loading…" msgstr "로딩 중…" -#: src/routes/(root)/[handle]/settings/preferences.tsx:226 +#: src/routes/(root)/[handle]/settings/preferences.tsx:227 msgid "Locked to \"Only me\" because your default note privacy restricts visibility." msgstr "기본 단문 공개 범위가 제한되어 있어 “나만”으로 고정됩니다." @@ -1702,12 +1863,12 @@ msgid "Markdown guide" msgstr "Markdown 가이드" #: src/components/article-composer/ArticleComposerForm.tsx:90 -#: src/components/NoteComposer.tsx:1232 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:417 +#: src/components/NoteComposer.tsx:1243 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:418 msgid "Markdown supported" msgstr "Markdown 사용 가능" -#: src/components/AppSidebar.tsx:796 +#: src/components/AppSidebar.tsx:819 #: src/routes/(root)/admin/media.tsx:152 msgid "Media" msgstr "미디어" @@ -1718,6 +1879,11 @@ msgstr "미디어" msgid "Mentioned only" msgstr "언급된 사용자만" +#: src/components/NewsStoryCard.tsx:225 +#: src/components/NewsStoryCard.tsx:243 +msgid "Moderate" +msgstr "관리" + #: src/components/PostEngagementBar.tsx:436 #: src/components/PostEngagementBar.tsx:437 msgid "More engagement views" @@ -1747,7 +1913,7 @@ msgstr "복수 선택" msgid "Mute" msgstr "뮤트" -#: src/routes/(root)/[handle]/settings/blocks.tsx:84 +#: src/routes/(root)/[handle]/settings/blocks.tsx:85 msgid "Muted accounts" msgstr "뮤트한 계정" @@ -1755,7 +1921,7 @@ msgstr "뮤트한 계정" #~ msgid "Muted accounts are hidden from your feeds and stop notifying you, but you can still visit their profiles. Muting is private and is never federated." #~ msgstr "뮤트한 계정은 타임라인에 표시되지 않고 알림도 오지 않습니다. 다만 프로필은 계속 방문할 수 있습니다. 뮤트는 비공개이며 연합되지 않습니다." -#: src/routes/(root)/[handle]/settings/blocks.tsx:86 +#: src/routes/(root)/[handle]/settings/blocks.tsx:87 msgid "Muted accounts are hidden from your feeds and stop notifying you, except for replies and mentions from accounts you follow. You can still visit their profiles, and muting is private and never federated." msgstr "뮤트한 계정은 타임라인에 표시되지 않고 알림도 오지 않습니다. 다만 팔로우하는 계정의 댓글이나 언급은 알림이 옵니다. 프로필은 계속 방문할 수 있으며, 뮤트는 비공개이고 연합되지 않습니다." @@ -1763,11 +1929,11 @@ msgstr "뮤트한 계정은 타임라인에 표시되지 않고 알림도 오지 msgid "Mutes & blocks" msgstr "뮤트와 차단" -#: src/routes/(root)/[handle]/settings/blocks.tsx:77 +#: src/routes/(root)/[handle]/settings/blocks.tsx:78 msgid "Mutes and blocks" msgstr "뮤트와 차단" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:375 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:376 msgid "My passkey" msgstr "내 패스키" @@ -1783,21 +1949,25 @@ msgstr "이름이 너무 깁니다. 최대 길이는 50자입니다." msgid "Native name" msgstr "현지명" +#: src/routes/(root)/admin/news.tsx:365 +msgid "never" +msgstr "없음" + #: src/routes/(root)/admin/invitations.tsx:167 msgid "Never" msgstr "없음" -#: src/routes/(root)/[handle]/settings/invite.tsx:755 -#: src/routes/(root)/[handle]/settings/invite.tsx:821 +#: src/routes/(root)/[handle]/settings/invite.tsx:756 +#: src/routes/(root)/[handle]/settings/invite.tsx:822 msgid "Never expires" msgstr "유효기간 없음" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:446 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:447 msgid "Never used" msgstr "사용된 적 없음" -#: src/routes/(root)/[handle]/drafts/index.tsx:251 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/index.tsx:253 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "New article" msgstr "새 게시글" @@ -1810,6 +1980,22 @@ msgstr "이제 Hackers' Pub이 열려 있지 않아도 새 알림이 표시될 msgid "New posts available — click to load" msgstr "새 콘텐츠가 있습니다 — 클릭하여 불러오기" +#: src/components/NewsList.tsx:89 +msgid "Newest" +msgstr "최신" + +#: src/components/AppSidebar.tsx:244 +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:337 +#: src/routes/(root)/news/index.tsx:39 +msgid "News" +msgstr "뉴스" + +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:139 +#~ msgid "News scores" +#~ msgstr "뉴스 점수" + #: src/components/ImageLightbox.tsx:126 msgid "Next image" msgstr "다음 이미지" @@ -1822,10 +2008,14 @@ msgstr "아직 북마크가 없습니다" msgid "No draft to delete" msgstr "삭제할 초고가 없습니다" -#: src/routes/(root)/[handle]/drafts/index.tsx:262 +#: src/routes/(root)/[handle]/drafts/index.tsx:264 msgid "No drafts yet. Create your first article!" msgstr "아직 초고가 없습니다. 첫 게시글을 작성하세요!" +#: src/routes/(root)/admin/news.tsx:428 +msgid "No exclusion patterns yet." +msgstr "아직 제외 패턴이 없습니다." + #: src/components/ActorFollowerList.tsx:92 msgid "No followers found" msgstr "팔로워가 없습니다" @@ -1834,9 +2024,9 @@ msgstr "팔로워가 없습니다" msgid "No following found" msgstr "팔로잉하는 사용자가 없습니다" -#: src/routes/(root)/[handle]/invite/[id].tsx:265 -#: src/routes/(root)/[handle]/settings/invite.tsx:410 -#: src/routes/(root)/[handle]/settings/invite.tsx:831 +#: src/routes/(root)/[handle]/invite/[id].tsx:270 +#: src/routes/(root)/[handle]/settings/invite.tsx:411 +#: src/routes/(root)/[handle]/settings/invite.tsx:832 msgid "No invitations left" msgstr "남은 초대장이 없습니다" @@ -1852,16 +2042,24 @@ msgstr "게시글이 없습니다" msgid "No notes found" msgstr "단문이 없습니다" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:171 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:178 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:172 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:179 msgid "No one has quoted this yet." msgstr "아직 아무도 인용하지 않았습니다." -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:184 +#: src/components/NewsDiscussion.tsx:86 +msgid "No one has shared this link in a public post yet." +msgstr "아직 이 링크를 공개 글로 공유한 사람이 없습니다." + +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:185 msgid "No one has shared this yet." msgstr "아직 아무도 공유하지 않았습니다." +#: src/routes/(root)/admin/news.tsx:473 +msgid "No penalized links." +msgstr "패널티가 적용된 링크가 없습니다." + #: src/components/ActorPostList.tsx:140 #: src/components/ActorSharedPostList.tsx:93 #: src/components/PersonalTimeline.tsx:289 @@ -1874,8 +2072,8 @@ msgstr "콘텐츠가 없습니다" msgid "No previews" msgstr "미리보기 없음" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:189 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:190 msgid "No reactions yet." msgstr "아직 반응이 없습니다." @@ -1883,6 +2081,10 @@ msgstr "아직 반응이 없습니다." msgid "No reactors loaded." msgstr "불러온 반응자가 없습니다." +#: src/components/NewsList.tsx:148 +msgid "No shared links yet. Once links start circulating across the fediverse, they will appear here." +msgstr "아직 공유된 링크가 없습니다. 연합우주에서 링크가 퍼지기 시작하면 여기에 표시됩니다." + #: src/routes/(root)/sign/index.tsx:223 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pub에 해당 계정이 없습니다. 다시 시도해주세요." @@ -1891,16 +2093,29 @@ msgstr "Hackers' Pub에 해당 계정이 없습니다. 다시 시도해주세요 msgid "No user URI provided." msgstr "사용자 URI가 제공되지 않았습니다." +#: src/routes/(root)/admin/news.tsx:303 +msgid "Not authorized to clear penalties." +msgstr "패널티를 해제할 권한이 없습니다." + #: src/routes/(root)/admin/media.tsx:117 msgid "Not authorized to delete orphan media." msgstr "연결되지 않은 미디어를 삭제할 권한이 없습니다." +#: src/routes/(root)/admin/news.tsx:244 +#: src/routes/(root)/admin/news.tsx:274 +msgid "Not authorized to manage exclusions." +msgstr "제외 패턴을 관리할 권한이 없습니다." + +#: src/routes/(root)/admin/news.tsx:203 +msgid "Not authorized to recompute news scores." +msgstr "뉴스 점수를 다시 계산할 권한이 없습니다." + #: src/routes/(root)/admin/invitations.tsx:116 msgid "Not authorized to regenerate invitations." msgstr "초대장을 재발급할 권한이 없습니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:222 -#: src/routes/(root)/[handle]/invite/[id].tsx:225 +#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:230 msgid "Not found" msgstr "찾을 수 없음" @@ -1912,26 +2127,30 @@ msgstr "찾을 수 없음" #~ msgid "Note" #~ msgstr "단문" -#: src/components/NoteComposer.tsx:885 +#: src/routes/(root)/admin/news.tsx:407 +msgid "Note (optional)" +msgstr "메모 (선택)" + +#: src/components/NoteComposer.tsx:896 msgid "Note created successfully" msgstr "단문이 작성되었습니다" #. placeholder {0}: "REL_ME_ATTR" -#: src/routes/(root)/[handle]/settings/index.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:524 msgid "Note that you can verify your links belong to you by making the linked pages also link to your Hackers' Pub profile with {0} attribute." msgstr "링크가 실제로 당신의 것인지 인증할 수 있습니다. 링크한 페이지에서도 {0} 속성을 사용해 당신의 Hackers' Pub 프로필로 링크를 걸어 주세요." -#: src/components/NoteComposer.tsx:833 +#: src/components/NoteComposer.tsx:839 msgid "Note updated" msgstr "단문이 수정되었습니다." #: src/components/ProfileTabs.tsx:44 -#: src/routes/(root)/[handle]/bookmarks.tsx:137 +#: src/routes/(root)/[handle]/bookmarks.tsx:138 msgid "Notes" msgstr "단문" #: src/components/MarkdownEditor.tsx:193 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:463 msgid "Nothing to preview" msgstr "미리볼 내용이 없습니다" @@ -1944,7 +2163,7 @@ msgstr "알림 권한이 허용되지 않았습니다." msgid "Notification preview privacy" msgstr "알림 미리보기 개인정보" -#: src/components/AppSidebar.tsx:574 +#: src/components/AppSidebar.tsx:597 msgid "Notifications" msgstr "알림" @@ -1957,7 +2176,7 @@ msgstr "이 사이트의 알림이 차단되어 있습니다." msgid "Notifications are blocked in your browser settings." msgstr "브라우저 설정에서 알림이 차단되어 있습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:782 +#: src/routes/(root)/[handle]/settings/invite.tsx:783 msgid "Number of invitations" msgstr "초대 횟수" @@ -1982,7 +2201,7 @@ msgstr "또는" msgid "Or enter the code from the email" msgstr "또는 이메일의 코드를 입력하세요" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:877 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:878 msgid "Other languages" msgstr "다른 언어" @@ -1994,31 +2213,39 @@ msgstr "페이지를 찾을 수 없습니다" msgid "Passkey authentication failed" msgstr "패스키 인증 실패" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:370 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:371 msgid "Passkey name" msgstr "패스키 이름" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:265 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:266 msgid "Passkey registered successfully" msgstr "패스키 등록 성공" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:312 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 msgid "Passkey revoked" msgstr "패스키 취소됨" #: src/components/SettingsTabs.tsx:77 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:355 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:356 msgid "Passkeys" msgstr "패스키" -#: src/routes/(root)/[handle]/bookmarks.tsx:98 -#: src/routes/(root)/[handle]/bookmarks.tsx:101 -#: src/routes/(root)/[handle]/drafts/[id].tsx:50 -#: src/routes/(root)/[handle]/drafts/[id].tsx:51 -#: src/routes/(root)/[handle]/drafts/index.tsx:213 -#: src/routes/(root)/[handle]/drafts/index.tsx:216 -#: src/routes/(root)/[handle]/drafts/new.tsx:60 -#: src/routes/(root)/[handle]/drafts/new.tsx:61 +#: src/routes/(root)/admin/news.tsx:463 +msgid "Penalized links" +msgstr "패널티가 적용된 링크" + +#: src/routes/(root)/admin/news.tsx:297 +msgid "Penalty cleared." +msgstr "패널티를 해제했습니다." + +#: src/routes/(root)/[handle]/bookmarks.tsx:99 +#: src/routes/(root)/[handle]/bookmarks.tsx:102 +#: src/routes/(root)/[handle]/drafts/[id].tsx:52 +#: src/routes/(root)/[handle]/drafts/[id].tsx:53 +#: src/routes/(root)/[handle]/drafts/index.tsx:215 +#: src/routes/(root)/[handle]/drafts/index.tsx:218 +#: src/routes/(root)/[handle]/drafts/new.tsx:62 +#: src/routes/(root)/[handle]/drafts/new.tsx:63 msgid "Permission denied" msgstr "권한이 거부되었습니다" @@ -2026,21 +2253,21 @@ msgstr "권한이 거부되었습니다" msgid "Pin to profile" msgstr "프로필에 고정" -#: src/routes/(root)/[handle]/(profile)/index.tsx:302 +#: src/routes/(root)/[handle]/(profile)/index.tsx:305 msgid "Pinned posts" msgstr "고정된 콘텐츠" -#: src/routes/(root)/[handle]/settings/index.tsx:187 +#: src/routes/(root)/[handle]/settings/index.tsx:188 msgid "Please choose an image file smaller than 5 MiB." msgstr "5 MiB 미만의 이미지 파일을 선택해주세요." -#: src/routes/(root)/[handle]/settings/invite.tsx:249 -#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:250 +#: src/routes/(root)/[handle]/settings/invite.tsx:541 msgid "Please correct the errors and try again." msgstr "오류를 수정하고 다시 시도해주세요." #: src/components/article-composer/ArticleComposerForm.tsx:53 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:383 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:384 msgid "Please enter a title for your article." msgstr "게시글 제목을 입력해주세요." @@ -2048,9 +2275,9 @@ msgstr "게시글 제목을 입력해주세요." msgid "Please enter your Fediverse handle." msgstr "연합우주 핸들을 입력해 주세요." -#: src/routes/(root)/[handle]/drafts/[id].tsx:55 -#: src/routes/(root)/[handle]/drafts/index.tsx:220 -#: src/routes/(root)/[handle]/drafts/new.tsx:65 +#: src/routes/(root)/[handle]/drafts/[id].tsx:57 +#: src/routes/(root)/[handle]/drafts/index.tsx:222 +#: src/routes/(root)/[handle]/drafts/new.tsx:67 msgid "Please sign in to access this page" msgstr "이 페이지에 접근하려면 로그인하세요" @@ -2062,6 +2289,10 @@ msgstr "투표하려면 로그인하세요" msgid "Poll closed" msgstr "투표가 마감되었습니다" +#: src/components/NewsList.tsx:88 +msgid "Popular" +msgstr "인기" + #: src/components/PostActionMenu.tsx:291 msgid "Post deleted" msgstr "콘텐츠가 삭제되었습니다" @@ -2079,27 +2310,27 @@ msgstr "콘텐츠 고정을 해제했습니다" msgid "Posts" msgstr "콘텐츠" -#: src/routes/(root)/[handle]/settings/preferences.tsx:183 +#: src/routes/(root)/[handle]/settings/preferences.tsx:184 msgid "Prefer AI-generated summary" msgstr "AI 생성 요약 선호" #: src/components/SettingsTabs.tsx:53 -#: src/routes/(root)/[handle]/settings/preferences.tsx:169 #: src/routes/(root)/[handle]/settings/preferences.tsx:170 +#: src/routes/(root)/[handle]/settings/preferences.tsx:171 msgid "Preferences" msgstr "환경 설정" -#: src/routes/(root)/[handle]/invite/[id].tsx:387 +#: src/routes/(root)/[handle]/invite/[id].tsx:392 msgid "Preferred language" msgstr "선호 언어" -#: src/routes/(root)/[handle]/settings/language.tsx:83 +#: src/routes/(root)/[handle]/settings/language.tsx:84 msgid "Preferred languages" msgstr "선호 언어" #: src/components/article-composer/ArticleComposerForm.tsx:111 #: src/components/MarkdownEditor.tsx:164 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:426 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:427 msgid "Preview" msgstr "미리보기" @@ -2111,7 +2342,7 @@ msgstr "이전 이미지" msgid "Priority" msgstr "우선순위" -#: src/components/AppSidebar.tsx:982 +#: src/components/AppSidebar.tsx:1028 #: src/routes/(root)/privacy.tsx:40 msgid "Privacy policy" msgstr "개인정보 처리방침" @@ -2124,8 +2355,8 @@ msgstr "프로필" msgid "Profile actions" msgstr "프로필 작업" -#: src/routes/(root)/[handle]/settings/index.tsx:124 #: src/routes/(root)/[handle]/settings/index.tsx:125 +#: src/routes/(root)/[handle]/settings/index.tsx:126 msgid "Profile settings" msgstr "프로필 설정" @@ -2155,8 +2386,8 @@ msgstr "공개하는 중…" msgid "Push notification privacy updated" msgstr "푸시 알림 개인정보 설정을 업데이트했습니다" -#: src/routes/(root)/[handle]/settings/invite.tsx:647 -#: src/routes/(root)/[handle]/settings/invite.tsx:713 +#: src/routes/(root)/[handle]/settings/invite.tsx:648 +#: src/routes/(root)/[handle]/settings/invite.tsx:714 msgid "QR code" msgstr "QR 코드" @@ -2170,7 +2401,7 @@ msgstr "검색어를 비울 수 없습니다" msgid "Quiet public" msgstr "조용히 공개" -#: src/components/NoteComposeModal.tsx:71 +#: src/components/NoteComposeModal.tsx:72 #: src/components/PostEngagementBar.tsx:270 #: src/components/ui/markdown-editor.tsx:289 msgid "Quote" @@ -2194,8 +2425,8 @@ msgid "Quoted post hidden" msgstr "인용 원문이 가려짐" #: src/components/EngagementTabs.tsx:46 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:100 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:111 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:101 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:112 msgid "Quotes" msgstr "인용" @@ -2218,8 +2449,8 @@ msgid "reaction" msgstr "반응" #: src/components/EngagementTabs.tsx:55 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:159 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:173 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:160 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:174 msgid "Reactions" msgstr "반응" @@ -2232,6 +2463,10 @@ msgstr "게시글 전체 읽기" #~ msgid "Read the full Code of conduct" #~ msgstr "행동 강령 전문을 읽으세요." +#: src/routes/(root)/admin/news.tsx:344 +msgid "Rebuilds the popularity score of every shared link from scratch. The operation is idempotent and safe to run at any time; scores normally stay fresh on their own, so this is mainly a manual backstop and a development tool." +msgstr "공유된 모든 링크의 인기 점수를 처음부터 다시 계산합니다. 이 작업은 멱등하며 언제 실행해도 안전합니다. 점수는 평소 자동으로 최신 상태를 유지하므로, 이 기능은 주로 수동 보완 수단이자 개발용 도구입니다." + #: src/components/WebPushPromptBanner.tsx:252 msgid "Receive new notifications immediately, even when this tab is closed." msgstr "이 탭을 닫아도 새 알림을 즉시 받습니다." @@ -2240,10 +2475,19 @@ msgstr "이 탭을 닫아도 새 알림을 즉시 받습니다." msgid "Receive notifications immediately through this browser, even when this tab is closed." msgstr "이 브라우저로 알림을 즉시 받습니다. 이 탭을 닫아도 알림을 받을 수 있습니다." -#: src/components/AppSidebar.tsx:901 +#: src/components/AppSidebar.tsx:947 msgid "Recent drafts" msgstr "최근 초고" +#: src/routes/(root)/admin/news.tsx:342 +#: src/routes/(root)/admin/news.tsx:375 +msgid "Recompute news scores" +msgstr "뉴스 점수 다시 계산" + +#: src/routes/(root)/admin/news.tsx:374 +msgid "Recomputing…" +msgstr "다시 계산하는 중…" + #: src/routes/(root)/admin/invitations.tsx:203 msgid "Regenerate" msgstr "재발급" @@ -2260,23 +2504,23 @@ msgstr "초대장 재발급" msgid "Regenerating…" msgstr "재발급 중…" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Register" msgstr "등록" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:362 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:363 msgid "Register a passkey" msgstr "패스키 등록" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:364 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:365 msgid "Register a passkey to sign in to your account. You can use a passkey instead of receiving a sign-in link by email." msgstr "계정에 패스키를 등록하세요. 이메일로 로그인 링크를 받는 대신 패스키를 사용할 수 있습니다." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:393 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:394 msgid "Registered passkeys" msgstr "등록된 패스키" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Registering…" msgstr "등록중…" @@ -2287,6 +2531,7 @@ msgid "Remote follow" msgstr "원격 팔로" #: src/components/LanguageList.tsx:225 +#: src/routes/(root)/admin/news.tsx:451 msgid "Remove" msgstr "제거" @@ -2304,13 +2549,13 @@ msgstr "북마크 해제" msgid "Remove from sidebar" msgstr "사이드바에서 제거" -#: src/components/NoteComposer.tsx:1358 -#: src/components/NoteComposer.tsx:1359 +#: src/components/NoteComposer.tsx:1369 +#: src/components/NoteComposer.tsx:1370 msgid "Remove image" msgstr "이미지 제거" -#: src/components/NoteComposer.tsx:1113 -#: src/components/NoteComposer.tsx:1114 +#: src/components/NoteComposer.tsx:1124 +#: src/components/NoteComposer.tsx:1125 msgid "Remove quote" msgstr "인용 삭제" @@ -2320,11 +2565,11 @@ msgstr "생성된 지 충분히 오래되었고 프로필 사진, 단문, 게시 #: src/components/article-composer/ArticleComposerForm.tsx:134 #: src/components/MarkdownEditor.tsx:181 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:452 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:453 msgid "Rendering…" msgstr "렌더링 중…" -#: src/components/NoteComposeModal.tsx:70 +#: src/components/NoteComposeModal.tsx:71 #: src/components/PostEngagementBar.tsx:258 msgid "Reply" msgstr "댓글" @@ -2333,20 +2578,20 @@ msgstr "댓글" msgid "Replying is not available for this post" msgstr "이 콘텐츠에는 댓글을 달 수 없습니다" -#: src/components/NoteComposer.tsx:1007 +#: src/components/NoteComposer.tsx:1018 msgid "Replying to" msgstr "댓글 대상" -#: src/components/AppSidebar.tsx:498 +#: src/components/AppSidebar.tsx:521 msgid "Return to old UI" msgstr "예전 UI로 돌아가기" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:475 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:526 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:476 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:527 msgid "Revoke" msgstr "취소" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:515 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:516 msgid "Revoke passkey" msgstr "패스키를 취소" @@ -2359,14 +2604,14 @@ msgstr "인용 취소" msgid "Revoke this quote?" msgstr "이 인용을 취소하시겠습니까?" -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Save" msgstr "저장" -#: src/components/NoteComposer.tsx:1412 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 +#: src/components/NoteComposer.tsx:1423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 msgid "Save changes" msgstr "변경사항 저장" @@ -2379,16 +2624,16 @@ msgid "Save draft to see preview" msgstr "미리보기를 보려면 초고로 저장하세요" #: src/components/article-composer/ArticleComposerActions.tsx:36 -#: src/components/NoteComposer.tsx:1413 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/components/NoteComposer.tsx:1424 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Saving…" msgstr "저장 중…" #: src/components/admin/AdminAccountsTable.tsx:202 -#: src/components/AppSidebar.tsx:377 +#: src/components/AppSidebar.tsx:400 #: src/components/SearchForm.tsx:65 #: src/components/SearchForm.tsx:80 msgid "Search" @@ -2423,16 +2668,16 @@ msgstr "선택지를 선택하세요" msgid "Select options" msgstr "선택지를 선택하세요" -#: src/routes/(root)/[handle]/settings/language.tsx:84 +#: src/routes/(root)/[handle]/settings/language.tsx:85 msgid "Select your preferred languages in order of preference. This will help tailor content to your preferences." msgstr "선호하는 언어를 우선순위에 따라 선택하세요. 콘텐츠를 사용자의 취향에 맞게 조정하는 데 도움이 됩니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:413 +#: src/routes/(root)/[handle]/settings/invite.tsx:414 msgid "Send" msgstr "보내기" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 -#: src/routes/(root)/[handle]/settings/invite.tsx:412 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 +#: src/routes/(root)/[handle]/settings/invite.tsx:413 msgid "Sending…" msgstr "보내는 중…" @@ -2444,11 +2689,11 @@ msgstr "민감한 콘텐츠" msgid "Separate tags with spaces. Tags help readers discover your article." msgstr "태그는 공백으로 구분합니다. 태그는 독자가 게시글을 찾는 데 도움이 됩니다." -#: src/routes/(root)/[handle]/settings/preferences.tsx:171 +#: src/routes/(root)/[handle]/settings/preferences.tsx:172 msgid "Set your personal preferences." msgstr "개인의 환경 설정을 설정하세요." -#: src/components/AppSidebar.tsx:674 +#: src/components/AppSidebar.tsx:697 msgid "Settings" msgstr "설정" @@ -2456,10 +2701,19 @@ msgstr "설정" msgid "Share" msgstr "공유" +#: src/components/NewsStoryCard.tsx:198 +#: src/components/NewsStoryHeader.tsx:132 +msgid "Share this link" +msgstr "이 링크 공유" + +#: src/components/NewsDiscussionComposer.tsx:30 +msgid "Share your opinion on this story…" +msgstr "이 이야기에 의견을 남겨 보세요…" + #: src/components/EngagementTabs.tsx:37 #: src/components/ProfileTabs.tsx:54 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:101 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:114 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:102 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:115 msgid "Shares" msgstr "공유" @@ -2474,6 +2728,15 @@ msgstr "이 콘텐츠는 공유할 수 없습니다." msgid "Show" msgstr "표시" +#. placeholder {0}: childCount() +#: src/components/NewsDiscussionThread.tsx:356 +msgid "Show {0} more in this thread" +msgstr "이 스레드에서 {0}개 더 보기" + +#: src/components/NewsDiscussion.tsx:77 +msgid "Show more sharing posts" +msgstr "공유 콘텐츠 더 보기" + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Show preview" #~ msgstr "미리보기 표시" @@ -2482,7 +2745,7 @@ msgstr "표시" msgid "Show sensitive content" msgstr "민감한 콘텐츠 표시" -#: src/components/AppSidebar.tsx:535 +#: src/components/AppSidebar.tsx:558 #: src/routes/(root)/sign/index.tsx:382 msgid "Sign in" msgstr "로그인" @@ -2491,6 +2754,10 @@ msgstr "로그인" msgid "Sign in to Hackers' Pub" msgstr "Hackers' Pub 로그인" +#: src/components/NewsDiscussionComposer.tsx:44 +msgid "Sign in to post" +msgstr "로그인하고 작성" + #: src/components/QuestionCard.tsx:394 msgid "Sign in to vote" msgstr "로그인 후 투표" @@ -2499,11 +2766,11 @@ msgstr "로그인 후 투표" msgid "Sign in with passkey" msgstr "패스키를 사용하여 로그인" -#: src/components/AppSidebar.tsx:964 +#: src/components/AppSidebar.tsx:1010 msgid "Sign out" msgstr "로그아웃" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 #: src/routes/(root)/sign/up/[token].tsx:494 msgid "Sign up" msgstr "가입" @@ -2536,7 +2803,7 @@ msgstr "슬러그 (URL)" msgid "Slug cannot be empty" msgstr "슬러그는 비워둘 수 없습니다" -#: src/components/NoteComposer.tsx:613 +#: src/components/NoteComposer.tsx:619 msgid "Some images were skipped because the limit of {MAX_MEDIA} was reached" msgstr "이미지 {MAX_MEDIA}개 제한에 도달하여 일부 이미지가 건너뛰어졌습니다" @@ -2551,22 +2818,22 @@ msgstr "문제가 발생했습니다. 다시 시도해주세요." #: src/components/article-composer/ArticleComposerContext.tsx:309 #: src/components/article-composer/ArticleComposerContext.tsx:384 #: src/components/article-composer/ArticleComposerContext.tsx:449 -#: src/components/NoteComposer.tsx:832 -#: src/components/NoteComposer.tsx:884 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:331 -#: src/routes/(root)/[handle]/drafts/index.tsx:174 +#: src/components/NoteComposer.tsx:838 +#: src/components/NoteComposer.tsx:895 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/drafts/index.tsx:175 msgid "Success" msgstr "성공" -#: src/routes/(root)/[handle]/settings/language.tsx:147 +#: src/routes/(root)/[handle]/settings/language.tsx:148 msgid "Successfully saved language preferences" msgstr "언어 설정이 성공적으로 저장되었습니다" -#: src/routes/(root)/[handle]/settings/preferences.tsx:133 +#: src/routes/(root)/[handle]/settings/preferences.tsx:134 msgid "Successfully saved preferences" msgstr "환경 설정이 성공적으로 저장되었습니다" -#: src/routes/(root)/[handle]/settings/index.tsx:328 +#: src/routes/(root)/[handle]/settings/index.tsx:329 msgid "Successfully saved settings" msgstr "설정이 성공적으로 저장되었습니다" @@ -2575,9 +2842,9 @@ msgid "Summarized by LLM" msgstr "LLM 요약" #: src/components/DocumentView.tsx:38 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:721 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:729 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1056 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:722 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:730 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1057 msgid "Table of contents" msgstr "목차" @@ -2586,8 +2853,8 @@ msgstr "목차" #~ msgstr "태그" #: src/components/article-composer/ArticleComposerForm.tsx:158 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:478 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1065 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:479 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1066 msgid "Tags" msgstr "태그" @@ -2595,65 +2862,73 @@ msgstr "태그" msgid "Tell us about yourself…" msgstr "당신에 대해서 이야기 해주세요…" +#: src/routes/(root)/admin/news.tsx:237 +msgid "That is not a valid URL pattern." +msgstr "올바른 URL 패턴이 아닙니다." + #: src/components/WebPushNotificationSettings.tsx:158 #: src/components/WebPushPromptBanner.tsx:93 msgid "The browser did not provide a complete push subscription." msgstr "브라우저가 완전한 푸시 알림 구독 정보를 제공하지 않았습니다." -#: src/routes/(root)/[handle]/settings/preferences.tsx:200 +#: src/routes/(root)/[handle]/settings/preferences.tsx:201 msgid "The default privacy setting for your notes." msgstr "단문의 기본 공개 설정입니다." -#: src/routes/(root)/[handle]/settings/preferences.tsx:212 +#: src/routes/(root)/[handle]/settings/preferences.tsx:213 msgid "The default privacy setting for your shares." msgstr "공유의 기본 공개 설정입니다." -#: src/routes/(root)/[handle]/settings/preferences.tsx:227 +#: src/routes/(root)/[handle]/settings/preferences.tsx:228 msgid "The default quote permission for your notes." msgstr "단문의 기본 인용 권한 설정입니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:169 -#: src/routes/(root)/[handle]/settings/invite.tsx:352 +#: src/routes/(root)/[handle]/invite/[id].tsx:174 +#: src/routes/(root)/[handle]/settings/invite.tsx:353 msgid "The email address is invalid." msgstr "이메일 주소가 유효하지 않습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:347 +#: src/routes/(root)/[handle]/settings/invite.tsx:348 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "이메일 주소는 초대장을 받을 때 뿐만 아니라, 계정에 로그인할 때도 사용됩니다." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:395 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:396 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "다음 패스키들이 계정에 등록되어 있습니다. 이들을 사용하여 계정에 로그인할 수 있습니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:187 +#: src/routes/(root)/[handle]/invite/[id].tsx:192 msgid "The invitation email could not be sent. Please try again later." msgstr "초대 이메일을 보내지 못했습니다. 나중에 다시 시도해 주세요." -#: src/routes/(root)/[handle]/settings/invite.tsx:259 +#: src/routes/(root)/[handle]/settings/invite.tsx:260 msgid "The invitation has been sent successfully." msgstr "초대장이 성공적으로 발송되었습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:590 +#: src/routes/(root)/[handle]/settings/invite.tsx:591 msgid "The invitation link could not be found or you are not authorized to delete it." msgstr "초대 링크를 찾을 수 없거나 삭제할 권한이 없습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:612 +#: src/routes/(root)/[handle]/settings/invite.tsx:613 msgid "The invitation link has been copied to the clipboard." msgstr "초대 링크가 클립보드에 복사되었습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:532 +#: src/routes/(root)/[handle]/settings/invite.tsx:533 msgid "The invitation link has been created successfully." msgstr "초대 링크가 성공적으로 생성되었습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:583 +#: src/routes/(root)/[handle]/settings/invite.tsx:584 msgid "The invitation link has been deleted successfully." msgstr "초대 링크가 성공적으로 삭제되었습니다." +#: src/components/NewsDiscussionComposer.tsx:34 +msgid "The link to this story is added to your post automatically." +msgstr "이 이야기의 링크가 콘텐츠에 자동으로 추가됩니다." + #: src/components/NotFoundPage.tsx:45 msgid "The page you're looking for doesn't exist or has been moved." msgstr "찾으시는 페이지가 존재하지 않거나 이동되었습니다." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:314 msgid "The passkey has been successfully revoked." msgstr "성공적으로 패스키를 취소했습니다." @@ -2671,7 +2946,7 @@ msgstr "가입 링크가 유효하지 않습니다. 이메일로 받은 링크 #. placeholder {0}: "GITHUB_REPOSITORY" #. placeholder {1}: "AGPL-3.0" -#: src/components/AppSidebar.tsx:1006 +#: src/components/AppSidebar.tsx:1052 msgid "The source code of this website is available on {0} under the {1} license." msgstr "이 웹사이트의 소스 코드는 {1} 라이선스로 {0}에서 배포됩니다." @@ -2679,7 +2954,7 @@ msgstr "이 웹사이트의 소스 코드는 {1} 라이선스로 {0}에서 배 msgid "The title will appear at the top of your article and in link previews." msgstr "제목은 게시글 상단과 링크 미리보기에 표시됩니다." -#: src/routes/(root)/[handle]/settings/index.tsx:603 +#: src/routes/(root)/[handle]/settings/index.tsx:604 msgid "The URL of the link, e.g., https://github.com/yourhandle." msgstr "링크의 URL. 예: https://github.com/yourhandle." @@ -2697,26 +2972,26 @@ msgstr "이 작업은 되돌릴 수 없습니다. 이 콘텐츠는 영구적으 msgid "This browser does not support Web Push." msgstr "이 브라우저는 Web Push를 지원하지 않습니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:173 -#: src/routes/(root)/[handle]/invite/[id].tsx:177 +#: src/routes/(root)/[handle]/invite/[id].tsx:178 +#: src/routes/(root)/[handle]/invite/[id].tsx:182 msgid "This email is already associated with an existing account." msgstr "이 이메일 주소는 이미 기존 계정에 연결되어 있습니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:232 msgid "This invitation link does not exist or has been deleted." msgstr "이 초대 링크는 존재하지 않거나 삭제되었습니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:161 -#: src/routes/(root)/[handle]/invite/[id].tsx:257 +#: src/routes/(root)/[handle]/invite/[id].tsx:166 +#: src/routes/(root)/[handle]/invite/[id].tsx:262 msgid "This invitation link has expired." msgstr "이 초대 링크는 만료되었습니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:164 -#: src/routes/(root)/[handle]/invite/[id].tsx:267 +#: src/routes/(root)/[handle]/invite/[id].tsx:169 +#: src/routes/(root)/[handle]/invite/[id].tsx:272 msgid "This invitation link has no remaining invitations." msgstr "이 초대 링크에 남은 초대가 없습니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:159 +#: src/routes/(root)/[handle]/invite/[id].tsx:164 msgid "This invitation link was not found." msgstr "이 초대 링크를 찾을 수 없습니다." @@ -2748,7 +3023,7 @@ msgstr "이 서버에는 아직 Web Push가 설정되어 있지 않습니다." msgid "This service does not support remote follow." msgstr "이 서비스는 원격 팔로를 지원하지 않습니다." -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:552 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:553 msgid "This usually takes about a minute. The page will update automatically when the translation is ready." msgstr "보통 1분 정도 소요됩니다. 번역이 완료되면 페이지가 자동으로 업데이트됩니다." @@ -2765,7 +3040,7 @@ msgid "Timeline" msgstr "타임라인" #: src/components/article-composer/ArticleComposerForm.tsx:49 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:380 msgid "Title" msgstr "제목" @@ -2789,7 +3064,7 @@ msgstr "전체: {0}" #. placeholder {0}: "LANGUAGE" #: src/components/ArticleCard.tsx:350 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:861 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:862 msgid "Translated from {0}" msgstr "{0}에서 번역됨" @@ -2797,18 +3072,18 @@ msgstr "{0}에서 번역됨" #~ msgid "Translating to {0}…" #~ msgstr "{0}로 번역 중…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:548 msgid "Translating to {name}…" msgstr "{name}로 번역 중…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:543 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:544 msgid "Translating…" msgstr "번역 중…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:378 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:401 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:410 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:588 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:402 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:411 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:589 msgid "Translation request failed" msgstr "번역 요청 실패" @@ -2816,17 +3091,17 @@ msgstr "번역 요청 실패" #~ msgid "Translation request failed for {0}" #~ msgstr "{0}로의 번역 요청에 실패했습니다" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:593 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:594 msgid "Translation request failed for {name}" msgstr "{name}로의 번역 요청에 실패했습니다" #: src/app.tsx:125 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:601 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:602 msgid "Try again" msgstr "다시 시도" #: src/components/article-composer/ArticleComposerForm.tsx:162 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:482 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:483 msgid "Type tags separated by spaces" msgstr "공백으로 구분된 태그 입력" @@ -2844,7 +3119,7 @@ msgstr "반응을 추가할 수 없습니다. 다시 시도해주세요." msgid "Unable to remove reaction. Please try again." msgstr "반응을 제거할 수 없습니다. 다시 시도해주세요." -#: src/components/BlockedAccountsList.tsx:181 +#: src/components/BlockedAccountsList.tsx:117 #: src/components/ProfileActionMenu.tsx:377 #: src/components/ProfileActionMenu.tsx:405 #: src/components/ProfileActionMenu.tsx:413 @@ -2860,7 +3135,7 @@ msgstr "사용자 차단을 해제하시겠습니까?" msgid "Unfollow" msgstr "언팔로" -#: src/components/MutedAccountsList.tsx:178 +#: src/components/MutedAccountsList.tsx:114 #: src/components/ProfileActionMenu.tsx:361 msgid "Unmute" msgstr "뮤트 해제" @@ -2873,23 +3148,27 @@ msgstr "프로필에서 고정 해제" msgid "Unshare" msgstr "공유 취소" -#: src/routes/(root)/[handle]/settings/index.tsx:126 +#: src/routes/(root)/[handle]/settings/index.tsx:127 msgid "Update your profile information, including your avatar, username, display name, bio, and links." msgstr "프로필 사진, 아이디, 이름, 약력, 링크 등의 프로필 정보를 업데이트하세요." #. placeholder {0}: new Date(edge.node.updated).toLocaleDateString() -#: src/routes/(root)/[handle]/drafts/index.tsx:304 +#: src/routes/(root)/[handle]/drafts/index.tsx:306 msgid "Updated {0}" msgstr "{0}에 업데이트됨" -#: src/components/NoteComposer.tsx:1274 +#: src/components/NoteComposer.tsx:1285 msgid "Upload progress" msgstr "업로드 진행률" -#: src/routes/(root)/[handle]/settings/index.tsx:585 +#: src/routes/(root)/[handle]/settings/index.tsx:586 msgid "URL" msgstr "URL" +#: src/routes/(root)/admin/news.tsx:394 +msgid "URL pattern" +msgstr "URL 패턴" + #: src/components/ProfileActionMenu.tsx:277 msgid "User blocked" msgstr "사용자를 차단했습니다" @@ -2902,17 +3181,17 @@ msgstr "사용자를 뮤트했습니다" msgid "User not found." msgstr "사용자 정보를 찾을 수 없습니다." -#: src/components/BlockedAccountsList.tsx:95 +#: src/components/BlockedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:259 msgid "User unblocked" msgstr "사용자 차단을 해제했습니다" -#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:304 msgid "User unmuted" msgstr "사용자 뮤트를 해제했습니다" -#: src/routes/(root)/[handle]/settings/index.tsx:413 +#: src/routes/(root)/[handle]/settings/index.tsx:414 #: src/routes/(root)/sign/up/[token].tsx:331 msgid "Username" msgstr "아이디" @@ -2933,7 +3212,7 @@ msgstr "아이디는 필수입니다." msgid "Username is too long. Maximum length is 15 characters." msgstr "아이디가 너무 깁니다. 최대 길이는 15자입니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:435 +#: src/routes/(root)/[handle]/settings/invite.tsx:436 msgid "Users you have invited" msgstr "초대한 사용자" @@ -2947,7 +3226,7 @@ msgstr "{1}에 {0} 님이 이 링크의 소유자임이 확인됨" msgid "Verifying your invitation…" msgstr "초대장을 확인하고 있습니다…" -#: src/components/AppSidebar.tsx:927 +#: src/components/AppSidebar.tsx:973 msgid "View all drafts →" msgstr "모든 초고 보기 →" @@ -3015,7 +3294,7 @@ msgstr "투표함" msgid "Voting…" msgstr "투표 중…" -#: src/components/NoteComposer.tsx:611 +#: src/components/NoteComposer.tsx:617 msgid "Warning" msgstr "경고" @@ -3023,11 +3302,11 @@ msgstr "경고" msgid "We couldn't reach the server" msgstr "서버에 연결하지 못했습니다" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:598 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:599 msgid "We couldn't reach the translation service. Try again, or come back in a few minutes." msgstr "번역 서비스에 연결하지 못했습니다. 다시 시도하거나 몇 분 후에 다시 방문해 주세요." -#: src/routes/(root)/[handle]/settings/index.tsx:566 +#: src/routes/(root)/[handle]/settings/index.tsx:567 msgid "Website" msgstr "웹사이트" @@ -3039,7 +3318,7 @@ msgstr "Hackers' Pub에 오신 것을 환영합니다! 가입을 완료하려면 msgid "What is Hackers' Pub?" msgstr "Hackers' Pub이란?" -#: src/components/NoteComposer.tsx:1153 +#: src/components/NoteComposer.tsx:1164 msgid "What's on your mind?" msgstr "무슨 생각 해요?" @@ -3051,25 +3330,25 @@ msgstr "활성화하면 AI가 이 게시글을 다른 언어로 자동 번역할 #~ msgid "Who can quote this note" #~ msgstr "이 단문의 인용 허용 범위" -#: src/components/AppSidebar.tsx:285 +#: src/components/AppSidebar.tsx:308 msgid "Without shares" msgstr "공유 제외" #: src/components/article-composer/ArticleComposerForm.tsx:108 #: src/components/MarkdownEditor.tsx:161 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:424 msgid "Write" msgstr "작성" -#: src/components/NoteComposeModal.tsx:109 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1004 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:384 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:462 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:538 +#: src/components/NoteComposeModal.tsx:110 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1005 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:385 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:463 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:539 msgid "Write a reply…" msgstr "댓글을 입력하세요…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:437 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:438 msgid "Write your article here." msgstr "여기에 게시글을 작성하세요." @@ -3094,53 +3373,53 @@ msgstr "이 사용자에게서 차단당했습니다. 이 사용자를 팔로하 msgid "You are blocking this user. They can't follow you or see your posts." msgstr "이 사용자를 차단했습니다. 이 사용자는 회원님을 팔로하거나 콘텐츠를 볼 수 없습니다." -#: src/components/NoteComposer.tsx:602 +#: src/components/NoteComposer.tsx:608 msgid "You can attach up to {MAX_MEDIA} images" msgstr "이미지는 최대 {MAX_MEDIA}장까지 첨부할 수 있습니다" -#: src/routes/(root)/[handle]/settings/index.tsx:448 +#: src/routes/(root)/[handle]/settings/index.tsx:449 msgid "You can change it only once, and the old username will become available to others." msgstr "아이디는 단 한 번만 변경할 수 있으며, 변경하기 전 아이디는 다른 사람이 사용할 수 있게 됩니다." -#: src/routes/(root)/[handle]/settings/index.tsx:614 +#: src/routes/(root)/[handle]/settings/index.tsx:615 msgid "You can leave this empty to remove the link." msgstr "링크를 삭제하려면 이곳을 비워두세요." -#: src/routes/(root)/[handle]/settings/invite.tsx:397 -#: src/routes/(root)/[handle]/settings/invite.tsx:802 +#: src/routes/(root)/[handle]/settings/invite.tsx:398 +#: src/routes/(root)/[handle]/settings/invite.tsx:803 msgid "You can leave this field empty." msgstr "이 필드는 비워둘 수 있습니다." -#: src/routes/(root)/[handle]/drafts/new.tsx:64 +#: src/routes/(root)/[handle]/drafts/new.tsx:66 msgid "You can only create drafts for your own account" msgstr "자신의 계정에 대한 초고만 만들 수 있습니다" -#: src/routes/(root)/[handle]/drafts/[id].tsx:54 +#: src/routes/(root)/[handle]/drafts/[id].tsx:56 msgid "You can only edit your own drafts" msgstr "자신의 초고만 수정할 수 있습니다" -#: src/routes/(root)/[handle]/bookmarks.tsx:103 +#: src/routes/(root)/[handle]/bookmarks.tsx:104 msgid "You can only view your own bookmarks" msgstr "자신의 북마크만 볼 수 있습니다" -#: src/routes/(root)/[handle]/drafts/index.tsx:219 +#: src/routes/(root)/[handle]/drafts/index.tsx:221 msgid "You can only view your own drafts" msgstr "자신의 초고만 볼 수 있습니다" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:404 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:405 msgid "You don't have any passkeys registered yet." msgstr "등록된 패스키가 아직 없습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:309 -#: src/routes/(root)/[handle]/settings/invite.tsx:420 +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:421 msgid "You have no invitations left. Please wait until you receive more." msgstr "남은 초대장이 없습니다. 추가로 받을 때까지 기다려주세요." -#: src/components/BlockedAccountsList.tsx:116 +#: src/components/BlockedAccountsList.tsx:119 msgid "You haven't blocked anyone." msgstr "차단한 계정이 없습니다." -#: src/components/MutedAccountsList.tsx:113 +#: src/components/MutedAccountsList.tsx:116 msgid "You haven't muted anyone." msgstr "뮤트한 계정이 없습니다." @@ -3152,20 +3431,20 @@ msgstr "뮤트한 계정이 없습니다." msgid "You must be signed in" msgstr "로그인이 필요합니다" -#: src/components/NoteComposer.tsx:901 +#: src/components/NoteComposer.tsx:912 msgid "You must be signed in to create a note" msgstr "단문을 작성하려면 로그인해야 합니다" #: src/components/article-composer/ArticleComposerContext.tsx:467 -#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:193 msgid "You must be signed in to delete a draft" msgstr "초고를 삭제하려면 로그인해야 합니다" -#: src/components/NoteComposer.tsx:851 +#: src/components/NoteComposer.tsx:857 msgid "You must be signed in to edit a note" msgstr "단문을 수정하려면 로그인해야 합니다." -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:357 msgid "You must be signed in to edit an article" msgstr "게시글을 수정하려면 로그인해야 합니다" @@ -3185,15 +3464,15 @@ msgstr "당신을 초대한 분" msgid "You'll automatically follow each other when you sign up." msgstr "가입 시 자동으로 서로 팔로우 하게 됩니다." -#: src/routes/(root)/[handle]/invite/[id].tsx:276 +#: src/routes/(root)/[handle]/invite/[id].tsx:281 msgid "You've been invited to Hackers' Pub" msgstr "Hackers' Pub에 초대되었습니다" -#: src/routes/(root)/[handle]/settings/index.tsx:353 +#: src/routes/(root)/[handle]/settings/index.tsx:354 msgid "Your avatar will be displayed on your profile and in your posts. You can upload a PNG, JPEG, GIF, or WebP image up to 5 MiB in size." msgstr "프로필 사진은 프로필과 콘텐츠에 표시됩니다. PNG, JPEG, GIF, WebP 형식의 이미지를 5MiB 이하로 업로드할 수 있습니다." -#: src/routes/(root)/[handle]/settings/index.tsx:492 +#: src/routes/(root)/[handle]/settings/index.tsx:493 msgid "Your bio will be displayed on your profile. You can use Markdown to format it." msgstr "약력은 프로필에 표시됩니다. Markdown을 사용할 수 있습니다." @@ -3209,36 +3488,36 @@ msgstr "연결이 불안정한 것 같습니다. 네트워크를 확인하고 msgid "Your email address will be used to sign in to your account." msgstr "이메일 주소는 계정에 로그인할 때 사용됩니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:400 +#: src/routes/(root)/[handle]/settings/invite.tsx:401 msgid "Your friend will see this message in the invitation email." msgstr "초대장을 받는 친구가 볼 수 있는 메시지입니다." -#: src/routes/(root)/[handle]/settings/index.tsx:478 +#: src/routes/(root)/[handle]/settings/index.tsx:479 #: src/routes/(root)/sign/up/[token].tsx:393 msgid "Your name will be displayed on your profile and in your posts." msgstr "이름은 프로필과 콘텐츠에 표시됩니다." -#: src/routes/(root)/[handle]/settings/passkeys.tsx:267 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:268 msgid "Your passkey has been registered and can now be used for authentication." msgstr "패스키가 등록되었습니다. 이제 이 패스키를 사용하여 로그인 할 수 있습니다." -#: src/routes/(root)/[handle]/settings/preferences.tsx:134 +#: src/routes/(root)/[handle]/settings/preferences.tsx:135 msgid "Your preferences have been updated successfully." msgstr "환경 설정이 성공적으로 업데이트되었습니다." -#: src/routes/(root)/[handle]/settings/language.tsx:148 +#: src/routes/(root)/[handle]/settings/language.tsx:149 msgid "Your preferred languages have been updated." msgstr "선호 언어가 업데이트되었습니다." -#: src/routes/(root)/[handle]/settings/index.tsx:329 +#: src/routes/(root)/[handle]/settings/index.tsx:330 msgid "Your profile settings have been updated successfully." msgstr "프로필 설정이 성공적으로 업데이트되었습니다." -#: src/components/NoteComposeModal.tsx:128 +#: src/components/NoteComposeModal.tsx:129 msgid "Your unsaved draft will be lost." msgstr "저장되지 않은 초고가 사라집니다." -#: src/routes/(root)/[handle]/settings/index.tsx:445 +#: src/routes/(root)/[handle]/settings/index.tsx:446 #: src/routes/(root)/sign/up/[token].tsx:367 msgid "Your username will be used to create your profile URL and your fediverse handle." msgstr "아이디는 프로필 URL과 연합우주(fediverse) 핸들로 사용됩니다." diff --git a/web-next/src/locales/zh-CN/messages.po b/web-next/src/locales/zh-CN/messages.po index 10ca702a0..082113c19 100644 --- a/web-next/src/locales/zh-CN/messages.po +++ b/web-next/src/locales/zh-CN/messages.po @@ -14,7 +14,7 @@ msgstr "" "Plural-Forms: \n" #. placeholder {0}: article.replies?.edges.length ?? 0 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:991 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:992 msgid "{0, plural, one {# comment} other {# comments}}" msgstr "{0, plural, other {# 条评论}}" @@ -44,11 +44,16 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {关注 # 人}}" #. placeholder {0}: link.invitationsLeft -#: src/routes/(root)/[handle]/invite/[id].tsx:321 -#: src/routes/(root)/[handle]/settings/invite.tsx:742 +#: src/routes/(root)/[handle]/invite/[id].tsx:326 +#: src/routes/(root)/[handle]/settings/invite.tsx:743 msgid "{0, plural, one {# invitation left} other {# invitations left}}" msgstr "{0, plural, other {剩余 # 次邀请}}" +#. placeholder {0}: status()?.scoredLinkCount ?? 0 +#: src/routes/(root)/admin/news.tsx:350 +msgid "{0, plural, one {# link is currently in the news feed.} other {# links are currently in the news feed.}}" +msgstr "{0, plural, one {当前新闻订阅流中有 # 个链接。} other {当前新闻订阅流中有 # 个链接。}}" + #. placeholder {0}: count() #: src/routes/(root)/admin/media.tsx:172 msgid "{0, plural, one {# orphan medium can be deleted.} other {# orphan media can be deleted.}}" @@ -77,7 +82,7 @@ msgid "{0, plural, one {# voter} other {# voters}}" msgstr "{0, plural, other {# 位投票者}}" #. placeholder {0}: edge.node.tags.length - 3 -#: src/routes/(root)/[handle]/drafts/index.tsx:293 +#: src/routes/(root)/[handle]/drafts/index.tsx:295 msgid "{0, plural, one {+1 more} other {+# more}}" msgstr "{0, plural, other {还有#个}}" @@ -87,7 +92,7 @@ msgid "{0, plural, one {Deleted # orphan medium.} other {Deleted # orphan media. msgstr "{0, plural, other {已删除 # 个孤立媒体。}}" #. placeholder {0}: account.invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:311 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, other {邀请你的朋友加入 Hackers' Pub。你可以邀请最多 # 个人。}}" @@ -96,13 +101,18 @@ msgstr "{0, plural, other {邀请你的朋友加入 Hackers' Pub。你可以邀 msgid "{0, plural, one {Load # more reactor} other {Load # more reactors}}" msgstr "{0, plural, one {加载更多反应者 (#)} other {加载更多反应者 (#)}}" +#. placeholder {0}: result.linksUpdated! +#: src/routes/(root)/admin/news.tsx:190 +msgid "{0, plural, one {Recomputed # link.} other {Recomputed # links.}}" +msgstr "{0, plural, one {已重新计算 # 个链接的评分。} other {已重新计算 # 个链接的评分。}}" + #. placeholder {0}: result.accountsAffected! #: src/routes/(root)/admin/invitations.tsx:97 msgid "{0, plural, one {Regenerated invitations for # account.} other {Regenerated invitations for # accounts.}}" msgstr "{0, plural, other {已为 # 个账号重新发放邀请。}}" #. placeholder {0}: account.inviteesCount.totalCount -#: src/routes/(root)/[handle]/settings/invite.tsx:438 +#: src/routes/(root)/[handle]/settings/invite.tsx:439 msgid "{0, plural, one {You have invited total # person so far.} other {You have invited total # people so far.}}" msgstr "{0, plural, other {您已经邀请了总共 # 人。}}" @@ -172,8 +182,23 @@ msgstr "{0} 和其他 {1} 人更新了你转发过的内容" msgid "{0} followed you" msgstr "{0} 关注了你" +#. placeholder {0}: s.sourceBreakdown.bluesky +#: src/components/NewsStoryHeader.tsx:124 +msgid "{0} from Bluesky" +msgstr "来自 Bluesky 的 {0} 次" + +#. placeholder {0}: s.sourceBreakdown.local +#: src/components/NewsStoryHeader.tsx:118 +msgid "{0} from Hackers' Pub" +msgstr "来自 Hackers' Pub 的 {0} 次" + +#. placeholder {0}: s.sourceBreakdown.remote +#: src/components/NewsStoryHeader.tsx:121 +msgid "{0} from the fediverse" +msgstr "来自联邦宇宙的 {0} 次" + #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:361 +#: src/routes/(root)/[handle]/settings/invite.tsx:362 msgid "{0} is already a member of Hackers' Pub." msgstr "{0} 已经是 Hackers' Pub 的成员。" @@ -229,47 +254,57 @@ msgstr "{0} 更新了你转发过的内容" #. placeholder {0}: post.actor.rawName ?? post.actor.username #. placeholder {1}: post.excerpt #. placeholder {1}: title() -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:237 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:283 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:238 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:284 msgid "{0}: {1}" msgstr "{0}:{1}" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/articles.tsx:86 -#: src/routes/(root)/[handle]/(profile)/articles.tsx:90 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:87 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:91 msgid "{0}'s articles" msgstr "{0}的文章" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/followers.tsx:77 -#: src/routes/(root)/[handle]/(profile)/followers.tsx:80 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:78 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:81 msgid "{0}'s followers" msgstr "{0}的粉丝" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/following.tsx:77 -#: src/routes/(root)/[handle]/(profile)/following.tsx:80 +#: src/routes/(root)/[handle]/(profile)/following.tsx:78 +#: src/routes/(root)/[handle]/(profile)/following.tsx:81 msgid "{0}'s following" msgstr "{0}的关注" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/notes.tsx:86 -#: src/routes/(root)/[handle]/(profile)/notes.tsx:90 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:87 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:91 msgid "{0}'s notes" msgstr "{0}的帖子" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/shares.tsx:86 -#: src/routes/(root)/[handle]/(profile)/shares.tsx:90 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:87 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:91 msgid "{0}'s shares" msgstr "{0}的转帖" +#: src/components/NewsStoryCard.tsx:107 +#: src/components/NewsStoryHeader.tsx:54 +msgid "{count, plural, one {# opinion} other {# opinions}}" +msgstr "{count, plural, other {# 条意见}}" + +#: src/components/NewsStoryCard.tsx:51 +#: src/components/NewsStoryHeader.tsx:53 +#~ msgid "{count, plural, one {# share} other {# shares}}" +#~ msgstr "{count, plural, one {# 次转帖} other {# 次转帖}}" + #: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:231 #: src/routes/(root)/[handle]/[noteId]/reactions.tsx:253 #~ msgid "+{0} more reactor(s) not shown" #~ msgstr "+{0} 个未显示" -#: src/routes/(root)/[handle]/settings/index.tsx:579 +#: src/routes/(root)/[handle]/settings/index.tsx:580 msgid "A name for the link that will be displayed on your profile, e.g., GitHub." msgstr "为显示在你个人资料页面的链接命名,例如 GitHub。" @@ -278,11 +313,11 @@ msgid "A sign-in link has been sent to your email. Please check your inbox (or s msgstr "登录链接已发送至您的邮箱。请检查收件箱。(或垃圾邮件文件夹)" #: src/components/admin/AdminAccountsTable.tsx:224 -#: src/components/AppSidebar.tsx:479 +#: src/components/AppSidebar.tsx:502 msgid "Account" msgstr "账户" -#: src/components/AppSidebar.tsx:750 +#: src/components/AppSidebar.tsx:773 #: src/routes/(root)/admin/index.tsx:95 msgid "Accounts" msgstr "账号" @@ -292,7 +327,8 @@ msgstr "账号" msgid "Actions" msgstr "操作" -#: src/routes/(root)/[handle]/settings/language.tsx:195 +#: src/routes/(root)/[handle]/settings/language.tsx:196 +#: src/routes/(root)/admin/news.tsx:421 msgid "Add" msgstr "添加" @@ -305,19 +341,23 @@ msgstr "添加{0}" msgid "Add to sidebar" msgstr "添加到侧边栏" -#: src/components/AppSidebar.tsx:728 +#: src/routes/(root)/admin/news.tsx:421 +msgid "Adding…" +msgstr "添加中…" + +#: src/components/AppSidebar.tsx:751 msgid "Admin" msgstr "管理" -#: src/routes/(root)/[handle]/bookmarks.tsx:135 +#: src/routes/(root)/[handle]/bookmarks.tsx:136 msgid "All" msgstr "全部" -#: src/components/NoteComposer.tsx:801 +#: src/components/NoteComposer.tsx:807 msgid "All images must finish uploading before posting" msgstr "发布前,所有图片必须完成上传。" -#: src/components/NoteComposer.tsx:809 +#: src/components/NoteComposer.tsx:815 msgid "All images require alt text" msgstr "所有图片均需提供替代文字。" @@ -329,17 +369,21 @@ msgstr "所有语言" msgid "All notifications" msgstr "所有通知" +#: src/components/NewsList.tsx:90 +msgid "All-time" +msgstr "全部时间" + #: src/components/article-composer/ArticleComposerPublishFields.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:506 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:507 msgid "Allow automatic translation by AI" msgstr "允许 AI 自动翻译" #. placeholder {0}: index() + 1 -#: src/components/NoteComposer.tsx:1287 +#: src/components/NoteComposer.tsx:1298 msgid "Alt text for image {0}" msgstr "图片 {0} 的替代文字" -#: src/components/NoteComposer.tsx:1299 +#: src/components/NoteComposer.tsx:1310 msgid "Alt text for visually impaired people (required)" msgstr "供视力障碍人士使用的替代文字(必填)" @@ -347,31 +391,31 @@ msgstr "供视力障碍人士使用的替代文字(必填)" msgid "An error occurred during signup. Please try again." msgstr "注册过程中发生错误。请重新尝试。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:280 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:281 msgid "An error occurred while registering your passkey." msgstr "在注册您的通行密钥时发生错误。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:328 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:329 msgid "An error occurred while revoking your passkey." msgstr "在撤销您的通行密钥时发生错误。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:143 +#: src/routes/(root)/[handle]/settings/preferences.tsx:144 msgid "An error occurred while saving your preferences. Please try again, or contact support if the problem persists." msgstr "保存设置时出现错误。请重试,如果问题持续存在,请联系支持。" -#: src/routes/(root)/[handle]/settings/language.tsx:162 +#: src/routes/(root)/[handle]/settings/language.tsx:163 msgid "An error occurred while saving your preferred languages. Please try again, or contact support if the problem persists." msgstr "保存语言偏好时出现错误。请重试,如果问题仍然存在,请联系支持。" -#: src/routes/(root)/[handle]/settings/index.tsx:296 -#: src/routes/(root)/[handle]/settings/index.tsx:337 +#: src/routes/(root)/[handle]/settings/index.tsx:297 +#: src/routes/(root)/[handle]/settings/index.tsx:338 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "保存设置时发生错误。请重试,如果问题仍然存在,请联系支持。" -#: src/routes/(root)/[handle]/invite/[id].tsx:198 -#: src/routes/(root)/[handle]/settings/invite.tsx:270 -#: src/routes/(root)/[handle]/settings/invite.tsx:551 -#: src/routes/(root)/[handle]/settings/invite.tsx:601 +#: src/routes/(root)/[handle]/invite/[id].tsx:203 +#: src/routes/(root)/[handle]/settings/invite.tsx:271 +#: src/routes/(root)/[handle]/settings/invite.tsx:552 +#: src/routes/(root)/[handle]/settings/invite.tsx:602 msgid "An unexpected error occurred. Please try again later." msgstr "发生了意外错误。请稍后再试。" @@ -386,7 +430,7 @@ msgstr "任何人都可以引用" msgid "Are you sure you want to block {0} ({1})? They won't be able to follow you or see your posts." msgstr "确定要屏蔽 {0}({1})吗?该用户将无法关注你或查看你的内容。" -#: src/routes/(root)/[handle]/drafts/index.tsx:156 +#: src/routes/(root)/[handle]/drafts/index.tsx:157 msgid "Are you sure you want to delete \"{draftTitle}\"? This action cannot be undone." msgstr "您确定要删除\"{draftTitle}\"吗?此操作无法撤销。" @@ -395,7 +439,7 @@ msgid "Are you sure you want to delete this draft? This action cannot be undone. msgstr "您确定要删除此草稿吗?此操作无法撤销。" #. placeholder {0}: passkeyToRevoke()?.name -#: src/routes/(root)/[handle]/settings/passkeys.tsx:517 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:518 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." msgstr "您确定要撤销通行密钥“{0}”吗?您将无法再使用它登录您的账户。" @@ -405,8 +449,8 @@ msgstr "您确定要撤销通行密钥“{0}”吗?您将无法再使用它登 msgid "Are you sure you want to unblock {0} ({1})? They will be able to follow you and see your posts." msgstr "确定要取消屏蔽 {0}({1})吗?该用户将能够关注你并查看你的内容。" -#: src/routes/(root)/[handle]/drafts/index.tsx:243 #: src/routes/(root)/[handle]/drafts/index.tsx:245 +#: src/routes/(root)/[handle]/drafts/index.tsx:247 msgid "Article drafts" msgstr "文章草稿" @@ -414,7 +458,7 @@ msgstr "文章草稿" msgid "Article published" msgstr "文章已发布" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:333 msgid "Article updated" msgstr "文章已更新" @@ -423,21 +467,21 @@ msgid "article-url-slug" msgstr "文章网址别名" #: src/components/ProfileTabs.tsx:51 -#: src/routes/(root)/[handle]/bookmarks.tsx:136 +#: src/routes/(root)/[handle]/bookmarks.tsx:137 msgid "Articles" msgstr "文章" -#: src/components/AppSidebar.tsx:308 +#: src/components/AppSidebar.tsx:331 msgid "Articles only" msgstr "仅文章" #. placeholder {0}: "CHANGED" -#: src/routes/(root)/[handle]/settings/index.tsx:454 +#: src/routes/(root)/[handle]/settings/index.tsx:455 msgid "As you have already changed it {0}, you can't change it again." msgstr "自打你已经把用户名换成 {0} 了,你再也改不了了。" -#: src/components/NoteComposer.tsx:1173 -#: src/components/NoteComposer.tsx:1174 +#: src/components/NoteComposer.tsx:1184 +#: src/components/NoteComposer.tsx:1185 msgid "Attach image" msgstr "附加图片" @@ -445,20 +489,20 @@ msgstr "附加图片" msgid "Authenticating…" msgstr "正在验证…" -#: src/components/NoteComposer.tsx:1318 +#: src/components/NoteComposer.tsx:1329 msgid "Auto-fill" msgstr "自动填写" -#: src/components/NoteComposer.tsx:1311 -#: src/components/NoteComposer.tsx:1312 +#: src/components/NoteComposer.tsx:1322 +#: src/components/NoteComposer.tsx:1323 msgid "Auto-fill alt text" msgstr "自动填写替代文字" -#: src/routes/(root)/[handle]/settings/index.tsx:351 +#: src/routes/(root)/[handle]/settings/index.tsx:352 msgid "Avatar" msgstr "头像" -#: src/routes/(root)/[handle]/settings/index.tsx:483 +#: src/routes/(root)/[handle]/settings/index.tsx:484 #: src/routes/(root)/sign/up/[token].tsx:403 msgid "Bio" msgstr "个人简介" @@ -477,11 +521,11 @@ msgstr "屏蔽" msgid "Block user?" msgstr "屏蔽用户?" -#: src/routes/(root)/[handle]/settings/blocks.tsx:98 +#: src/routes/(root)/[handle]/settings/blocks.tsx:99 msgid "Blocked accounts" msgstr "已屏蔽的账户" -#: src/routes/(root)/[handle]/settings/blocks.tsx:100 +#: src/routes/(root)/[handle]/settings/blocks.tsx:101 msgid "Blocked accounts cannot follow you or see your posts. Unlike muting, blocking is federated to the blocked account's instance." msgstr "被屏蔽的账户无法关注你或查看你的内容。与隐藏不同,屏蔽会联邦到被屏蔽账户所在的实例。" @@ -494,8 +538,8 @@ msgstr "粗体" msgid "Bookmark" msgstr "收藏" -#: src/components/AppSidebar.tsx:618 -#: src/routes/(root)/[handle]/bookmarks.tsx:125 +#: src/components/AppSidebar.tsx:641 +#: src/routes/(root)/[handle]/bookmarks.tsx:126 msgid "Bookmarks" msgstr "收藏" @@ -524,24 +568,33 @@ msgstr "已关闭浏览器通知" msgid "Browser notifications enabled" msgstr "已开启浏览器通知" +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:492 +msgid "Buried" +msgstr "已沉底" + +#: src/components/NewsStoryCard.tsx:258 +msgid "Bury" +msgstr "沉底" + #: src/components/article-composer/ArticleComposerActions.tsx:48 -#: src/components/NoteComposer.tsx:1346 -#: src/components/NoteComposer.tsx:1347 -#: src/components/NoteComposer.tsx:1390 +#: src/components/NoteComposer.tsx:1357 +#: src/components/NoteComposer.tsx:1358 +#: src/components/NoteComposer.tsx:1401 #: src/components/PostActionMenu.tsx:367 #: src/components/ProfileActionMenu.tsx:401 #: src/components/ProfileActionMenu.tsx:402 #: src/components/QuotedNoteCard.tsx:248 #: src/components/RemoteFollowButton.tsx:218 #: src/components/RemoteFollowButton.tsx:280 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:522 -#: src/routes/(root)/[handle]/settings/index.tsx:398 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:521 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:399 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:522 #: src/routes/(root)/authorize_interaction.tsx:273 msgid "Cancel" msgstr "取消" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:347 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 msgid "Cannot change the language because translations already exist" msgstr "由于已存在翻译,无法更改语言" @@ -549,11 +602,11 @@ msgstr "由于已存在翻译,无法更改语言" msgid "Check again" msgstr "重新检查" -#: src/routes/(root)/[handle]/invite/[id].tsx:244 +#: src/routes/(root)/[handle]/invite/[id].tsx:249 msgid "Check your email" msgstr "请检查你的邮箱" -#: src/routes/(root)/[handle]/invite/[id].tsx:246 +#: src/routes/(root)/[handle]/invite/[id].tsx:251 msgid "Check your email to complete sign-up. We've sent a verification link to your email address." msgstr "请检查你的邮箱以完成注册。我们已向你的邮箱地址发送了验证链接。" @@ -561,7 +614,7 @@ msgstr "请检查你的邮箱以完成注册。我们已向你的邮箱地址发 msgid "Checking…" msgstr "正在检查…" -#: src/routes/(root)/[handle]/settings/invite.tsx:386 +#: src/routes/(root)/[handle]/settings/invite.tsx:387 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "选择你的朋友使用的语言。这个语言只会用于邀请。" @@ -569,20 +622,25 @@ msgstr "选择你的朋友使用的语言。这个语言只会用于邀请。" msgid "Choose whether push notifications may include post excerpts. Generic notification text is used when previews are hidden." msgstr "选择推送通知是否可以包含内容摘要。隐藏预览时会使用通用通知文本。" -#: src/routes/(root)/[handle]/invite/[id].tsx:395 +#: src/routes/(root)/[handle]/invite/[id].tsx:400 msgid "Choose your preferred language for the verification email." msgstr "请选择验证邮件的语言。" #: src/components/admin/AdminAccountsTable.tsx:213 +#: src/routes/(root)/admin/news.tsx:501 msgid "Clear" msgstr "清除" +#: src/components/NewsStoryCard.tsx:264 +msgid "Clear penalty" +msgstr "清除处罚" + #: src/components/WebPushNotificationSettings.tsx:395 msgid "Clicking a notification opens your notifications page." msgstr "点击通知会打开你的通知页面。" #: src/components/ImageLightbox.tsx:74 -#: src/routes/(root)/[handle]/settings/invite.tsx:663 +#: src/routes/(root)/[handle]/settings/invite.tsx:664 msgid "Close" msgstr "关闭" @@ -594,7 +652,7 @@ msgstr "已结束" msgid "Code" msgstr "代码" -#: src/components/AppSidebar.tsx:977 +#: src/components/AppSidebar.tsx:1023 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/sign/up/[token].tsx:458 msgid "Code of conduct" @@ -604,18 +662,18 @@ msgstr "行为准则" #~ msgid "Comments ({0})" #~ msgstr "评论({0})" -#: src/components/AppSidebar.tsx:819 +#: src/components/AppSidebar.tsx:865 #: src/components/FloatingComposeButton.tsx:61 msgid "Compose" msgstr "写作" #: src/components/article-composer/ArticleComposerForm.tsx:65 -#: src/components/NoteComposer.tsx:1122 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:392 +#: src/components/NoteComposer.tsx:1133 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:393 msgid "Content" msgstr "内容" -#: src/components/NoteComposer.tsx:791 +#: src/components/NoteComposer.tsx:797 msgid "Content cannot be empty" msgstr "内容不能为空" @@ -627,15 +685,15 @@ msgstr "在浏览器中继续" msgid "Controls who can quote this article on their timeline." msgstr "设置谁可以在时间线上引用这篇文章。" -#: src/routes/(root)/[handle]/settings/invite.tsx:611 +#: src/routes/(root)/[handle]/settings/invite.tsx:612 msgid "Copied" msgstr "已复制" -#: src/routes/(root)/[handle]/settings/invite.tsx:705 +#: src/routes/(root)/[handle]/settings/invite.tsx:706 msgid "Copy" msgstr "复制" -#: src/routes/(root)/[handle]/settings/invite.tsx:618 +#: src/routes/(root)/[handle]/settings/invite.tsx:619 msgid "Could not copy the link to the clipboard." msgstr "无法将链接复制到剪贴板。" @@ -655,23 +713,23 @@ msgstr "无法撤销引用" msgid "Could not vote on this poll" msgstr "无法提交此投票" -#: src/components/AppSidebar.tsx:865 +#: src/components/AppSidebar.tsx:911 #: src/components/FloatingComposeButton.tsx:123 msgid "Create article" msgstr "创建文章" -#: src/routes/(root)/[handle]/settings/invite.tsx:834 +#: src/routes/(root)/[handle]/settings/invite.tsx:835 msgid "Create invitation link" msgstr "创建邀请链接" -#: src/components/AppSidebar.tsx:841 +#: src/components/AppSidebar.tsx:887 #: src/components/FloatingComposeButton.tsx:99 -#: src/components/NoteComposeModal.tsx:72 -#: src/components/NoteComposer.tsx:1407 +#: src/components/NoteComposeModal.tsx:73 +#: src/components/NoteComposer.tsx:1418 msgid "Create note" msgstr "创建帖子" -#: src/routes/(root)/[handle]/settings/invite.tsx:628 +#: src/routes/(root)/[handle]/settings/invite.tsx:629 msgid "Create shareable invitation links. Each link can be used multiple times until the invitation count runs out or the link expires." msgstr "创建可共享的邀请链接。每个链接可多次使用,直到邀请次数用完或链接过期。" @@ -680,7 +738,7 @@ msgid "Created" msgstr "创建时间" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:426 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:427 msgid "Created {0}" msgstr "{0}创建" @@ -688,16 +746,16 @@ msgstr "{0}创建" msgid "Creating account…" msgstr "正在创建账户…" -#: src/components/NoteComposer.tsx:1408 -#: src/routes/(root)/[handle]/settings/invite.tsx:833 +#: src/components/NoteComposer.tsx:1419 +#: src/routes/(root)/[handle]/settings/invite.tsx:834 msgid "Creating…" msgstr "正在创建…" -#: src/routes/(root)/[handle]/settings/index.tsx:405 +#: src/routes/(root)/[handle]/settings/index.tsx:406 msgid "Crop" msgstr "裁剪" -#: src/routes/(root)/[handle]/settings/index.tsx:378 +#: src/routes/(root)/[handle]/settings/index.tsx:379 msgid "Crop your new avatar" msgstr "裁剪你的新头像" @@ -711,22 +769,22 @@ msgstr "截止时间:" msgid "CW" msgstr "CW" -#: src/routes/(root)/[handle]/settings/preferences.tsx:192 +#: src/routes/(root)/[handle]/settings/preferences.tsx:193 msgid "Default note privacy" msgstr "默认帖子隐私设置" -#: src/routes/(root)/[handle]/settings/preferences.tsx:217 +#: src/routes/(root)/[handle]/settings/preferences.tsx:218 msgid "Default quote permission" msgstr "默认引用权限" -#: src/routes/(root)/[handle]/settings/preferences.tsx:204 +#: src/routes/(root)/[handle]/settings/preferences.tsx:205 msgid "Default share privacy" msgstr "默认转帖隐私设置" #: src/components/PostActionMenu.tsx:353 #: src/components/PostActionMenu.tsx:373 -#: src/routes/(root)/[handle]/drafts/index.tsx:320 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/drafts/index.tsx:322 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 msgid "Delete" msgstr "删除" @@ -744,11 +802,20 @@ msgid "Delete post?" msgstr "删除内容?" #: src/components/article-composer/ArticleComposerActions.tsx:21 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 #: src/routes/(root)/admin/media.tsx:187 msgid "Deleting…" msgstr "正在删除…" +#: src/components/NewsStoryCard.tsx:252 +msgid "Demote" +msgstr "降级" + +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:493 +msgid "Demoted" +msgstr "已降级" + #: src/components/WebPushNotificationSettings.tsx:431 msgid "Disable" msgstr "关闭" @@ -757,11 +824,11 @@ msgstr "关闭" msgid "Disabling…" msgstr "正在关闭…" -#: src/components/NoteComposeModal.tsx:137 +#: src/components/NoteComposeModal.tsx:138 msgid "Discard" msgstr "放弃" -#: src/components/NoteComposeModal.tsx:126 +#: src/components/NoteComposeModal.tsx:127 msgid "Discard draft?" msgstr "放弃草稿?" @@ -769,11 +836,15 @@ msgstr "放弃草稿?" msgid "Discard unsaved changes - are you sure?" msgstr "放弃未保存的更改 - 您确定吗?" +#: src/components/NewsStoryCard.tsx:145 +#~ msgid "Discussion" +#~ msgstr "讨论" + #: src/components/WebPushPromptBanner.tsx:269 msgid "Dismiss" msgstr "关闭" -#: src/routes/(root)/[handle]/settings/index.tsx:467 +#: src/routes/(root)/[handle]/settings/index.tsx:468 #: src/routes/(root)/sign/up/[token].tsx:377 msgid "Display name" msgstr "昵称" @@ -782,12 +853,12 @@ msgstr "昵称" msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "需要创建账户吗?Hackers' Pub 仅限邀请,请联系朋友邀请您。" -#: src/components/NoteComposer.tsx:742 +#: src/components/NoteComposer.tsx:748 msgid "Do you want to quote this link?" msgstr "要引用此链接吗?" #: src/components/article-composer/ArticleComposerContext.tsx:450 -#: src/routes/(root)/[handle]/drafts/index.tsx:175 +#: src/routes/(root)/[handle]/drafts/index.tsx:176 msgid "Draft deleted" msgstr "草稿已删除" @@ -803,7 +874,7 @@ msgstr "未找到草稿" msgid "Draft saved" msgstr "草稿已保存" -#: src/routes/(root)/[handle]/settings/index.tsx:381 +#: src/routes/(root)/[handle]/settings/index.tsx:382 msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "拖动选择要保留的区域,然后点击「裁剪」来更新你的头像。" @@ -812,25 +883,25 @@ msgid "e.g., @user@mastodon.social" msgstr "例如:@user@mastodon.social" #: src/components/PostActionMenu.tsx:328 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:695 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:696 msgid "Edit" msgstr "编辑" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:374 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:375 msgid "Edit article" msgstr "编辑文章" -#: src/routes/(root)/[handle]/drafts/[id].tsx:73 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/[id].tsx:75 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "Edit draft" msgstr "编辑草稿" -#: src/components/NoteComposeModal.tsx:69 +#: src/components/NoteComposeModal.tsx:70 msgid "Edit note" msgstr "编辑帖子" -#: src/routes/(root)/[handle]/invite/[id].tsx:366 -#: src/routes/(root)/[handle]/settings/invite.tsx:334 +#: src/routes/(root)/[handle]/invite/[id].tsx:371 +#: src/routes/(root)/[handle]/settings/invite.tsx:335 #: src/routes/(root)/sign/up/[token].tsx:311 msgid "Email address" msgstr "电子邮件地址" @@ -857,7 +928,7 @@ msgstr "已结束" msgid "Ends" msgstr "截止" -#: src/routes/(root)/[handle]/invite/[id].tsx:279 +#: src/routes/(root)/[handle]/invite/[id].tsx:284 msgid "Enter your email address below to get started." msgstr "请在下方输入你的电子邮件地址以开始。" @@ -879,47 +950,63 @@ msgstr "请在下方输入您的邮箱或用户名以登录。" #: src/components/article-composer/ArticleComposerContext.tsx:466 #: src/components/article-composer/ArticleComposerContext.tsx:474 #: src/components/article-composer/ArticleComposerForm.tsx:35 -#: src/components/NoteComposer.tsx:601 -#: src/components/NoteComposer.tsx:648 -#: src/components/NoteComposer.tsx:790 -#: src/components/NoteComposer.tsx:800 -#: src/components/NoteComposer.tsx:808 -#: src/components/NoteComposer.tsx:842 -#: src/components/NoteComposer.tsx:850 -#: src/components/NoteComposer.tsx:858 -#: src/components/NoteComposer.tsx:892 -#: src/components/NoteComposer.tsx:900 -#: src/components/NoteComposer.tsx:908 -#: src/components/NoteComposer.tsx:965 +#: src/components/NoteComposer.tsx:607 +#: src/components/NoteComposer.tsx:654 +#: src/components/NoteComposer.tsx:796 +#: src/components/NoteComposer.tsx:806 +#: src/components/NoteComposer.tsx:814 +#: src/components/NoteComposer.tsx:848 +#: src/components/NoteComposer.tsx:856 +#: src/components/NoteComposer.tsx:864 +#: src/components/NoteComposer.tsx:903 +#: src/components/NoteComposer.tsx:911 +#: src/components/NoteComposer.tsx:919 +#: src/components/NoteComposer.tsx:976 #: src/components/QuotedNoteCard.tsx:270 #: src/components/QuotedNoteCard.tsx:278 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:257 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:345 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:355 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:364 -#: src/routes/(root)/[handle]/drafts/index.tsx:182 -#: src/routes/(root)/[handle]/drafts/index.tsx:191 -#: src/routes/(root)/[handle]/drafts/index.tsx:199 -#: src/routes/(root)/[handle]/invite/[id].tsx:196 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:258 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:346 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/drafts/index.tsx:183 +#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:200 +#: src/routes/(root)/[handle]/invite/[id].tsx:201 #: src/routes/(root)/sign/up/[token].tsx:269 msgid "Error" msgstr "错误" +#: src/routes/(root)/admin/news.tsx:382 +msgid "Excluded URL patterns" +msgstr "排除的 URL 模式" + +#: src/routes/(root)/admin/news.tsx:233 +msgid "Exclusion pattern added." +msgstr "已添加排除模式。" + +#: src/routes/(root)/admin/news.tsx:268 +msgid "Exclusion pattern removed." +msgstr "已移除排除模式。" + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/invite/[id].tsx:334 -#: src/routes/(root)/[handle]/settings/invite.tsx:759 +#: src/routes/(root)/[handle]/invite/[id].tsx:339 +#: src/routes/(root)/[handle]/settings/invite.tsx:760 msgid "Expires {0}" msgstr "{0}过期" -#: src/routes/(root)/[handle]/settings/invite.tsx:806 +#: src/routes/(root)/[handle]/settings/invite.tsx:807 msgid "Expiry" msgstr "到期时间" -#: src/routes/(root)/[handle]/settings/invite.tsx:391 -#: src/routes/(root)/[handle]/settings/invite.tsx:796 +#: src/routes/(root)/[handle]/settings/invite.tsx:392 +#: src/routes/(root)/[handle]/settings/invite.tsx:797 msgid "Extra message" msgstr "额外消息" +#: src/routes/(root)/admin/news.tsx:252 +msgid "Failed to add exclusion pattern." +msgstr "添加排除模式失败。" + #: src/components/HashtagActionBar.tsx:192 #: src/components/HashtagActionBar.tsx:199 msgid "Failed to add to sidebar" @@ -935,17 +1022,21 @@ msgstr "未能屏蔽此用户" msgid "Failed to bookmark" msgstr "收藏失败" -#: src/routes/(root)/[handle]/settings/invite.tsx:617 +#: src/routes/(root)/admin/news.tsx:310 +msgid "Failed to clear penalty." +msgstr "清除处罚失败。" + +#: src/routes/(root)/[handle]/settings/invite.tsx:618 msgid "Failed to copy" msgstr "复制失败" -#: src/routes/(root)/[handle]/settings/invite.tsx:539 -#: src/routes/(root)/[handle]/settings/invite.tsx:549 +#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:550 msgid "Failed to create invitation link" msgstr "创建邀请链接失败" -#: src/routes/(root)/[handle]/settings/invite.tsx:588 -#: src/routes/(root)/[handle]/settings/invite.tsx:599 +#: src/routes/(root)/[handle]/settings/invite.tsx:589 +#: src/routes/(root)/[handle]/settings/invite.tsx:600 msgid "Failed to delete invitation link" msgstr "删除邀请链接失败" @@ -979,7 +1070,7 @@ msgstr "未能开启浏览器通知" msgid "Failed to follow" msgstr "关注失败" -#: src/components/NoteComposer.tsx:966 +#: src/components/NoteComposer.tsx:977 msgid "Failed to generate alt text" msgstr "替代文字生成失败" @@ -995,7 +1086,7 @@ msgstr "加载更多文章失败,点击重试" msgid "Failed to load more bookmarks; click to retry" msgstr "加载更多收藏失败,点击重试" -#: src/routes/(root)/[handle]/drafts/index.tsx:345 +#: src/routes/(root)/[handle]/drafts/index.tsx:347 msgid "Failed to load more drafts; click to retry" msgstr "加载更多草稿失败,点击重试" @@ -1007,7 +1098,7 @@ msgstr "加载更多粉丝失败,点击重试" msgid "Failed to load more following; click to retry" msgstr "加载更多关注失败,点击重试" -#: src/routes/(root)/[handle]/settings/invite.tsx:947 +#: src/routes/(root)/[handle]/settings/invite.tsx:948 msgid "Failed to load more invitees; click to retry" msgstr "加载更多受邀者失败;点击重试" @@ -1019,7 +1110,7 @@ msgstr "加载更多帖子失败,点击重试" msgid "Failed to load more notifications; click to retry" msgstr "加载更多通知失败;点击重试" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:495 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 msgid "Failed to load more passkeys; click to retry" msgstr "加载更多通行密钥失败,点击重试" @@ -1031,8 +1122,8 @@ msgstr "加载更多通行密钥失败,点击重试" msgid "Failed to load more posts; click to retry" msgstr "加载更多内容失败,点击重试" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:194 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:201 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:195 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:202 msgid "Failed to load more quotes; click to retry" msgstr "加载更多引用失败,点击重试" @@ -1040,28 +1131,31 @@ msgstr "加载更多引用失败,点击重试" msgid "Failed to load more reactors; click to retry" msgstr "加载更多反应者失败,点击重试" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:670 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:671 msgid "Failed to load more replies; click to retry" msgstr "加载更多回复失败,点击重试" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:204 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:213 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:205 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:214 msgid "Failed to load more shares; click to retry" msgstr "加载更多转帖失败,点击重试" -#: src/components/BlockedAccountsList.tsx:199 -#: src/components/MutedAccountsList.tsx:196 +#: src/components/AccountListBase.tsx:112 msgid "Failed to load more; click to retry" msgstr "加载更多失败;点击重试" -#: src/components/NoteComposer.tsx:1014 +#: src/components/NoteComposer.tsx:1025 msgid "Failed to load post" msgstr "无法加载内容" -#: src/components/NoteComposer.tsx:1065 +#: src/components/NoteComposer.tsx:1076 msgid "Failed to load quoted post" msgstr "无法加载引用内容" +#: src/components/NewsDiscussionThread.tsx:408 +msgid "Failed to load replies; click to retry" +msgstr "加载回复失败,点击重试" + #: src/components/RemoteFollowButton.tsx:126 msgid "Failed to look up user." msgstr "查找用户失败。" @@ -1087,11 +1181,15 @@ msgstr "无法置顶内容" msgid "Failed to react" msgstr "反应失败" +#: src/routes/(root)/admin/news.tsx:212 +msgid "Failed to recompute news scores." +msgstr "无法重新计算新闻评分。" + #: src/routes/(root)/admin/invitations.tsx:125 msgid "Failed to regenerate invitations." msgstr "重新发放邀请失败。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:277 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:278 msgid "Failed to register passkey" msgstr "注册通行密钥失败" @@ -1100,40 +1198,44 @@ msgstr "注册通行密钥失败" msgid "Failed to remove bookmark" msgstr "取消收藏失败" +#: src/routes/(root)/admin/news.tsx:281 +msgid "Failed to remove exclusion pattern." +msgstr "移除排除模式失败。" + #: src/components/HashtagActionBar.tsx:212 #: src/components/HashtagActionBar.tsx:219 msgid "Failed to remove from sidebar" msgstr "从侧边栏移除失败" #: src/components/MarkdownEditor.tsx:192 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:461 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 msgid "Failed to render preview" msgstr "预览渲染失败" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:319 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:325 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:320 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "Failed to revoke passkey" msgstr "撤销通行密钥失败" -#: src/routes/(root)/[handle]/settings/language.tsx:160 +#: src/routes/(root)/[handle]/settings/language.tsx:161 msgid "Failed to save language preferences" msgstr "保存语言偏好失败" -#: src/routes/(root)/[handle]/settings/preferences.tsx:141 +#: src/routes/(root)/[handle]/settings/preferences.tsx:142 msgid "Failed to save preferences" msgstr "保存设置失败" -#: src/routes/(root)/[handle]/settings/index.tsx:293 -#: src/routes/(root)/[handle]/settings/index.tsx:335 +#: src/routes/(root)/[handle]/settings/index.tsx:294 +#: src/routes/(root)/[handle]/settings/index.tsx:336 msgid "Failed to save settings" msgstr "保存设置失败" -#: src/routes/(root)/[handle]/invite/[id].tsx:185 +#: src/routes/(root)/[handle]/invite/[id].tsx:190 msgid "Failed to send email" msgstr "发送邮件失败" -#: src/routes/(root)/[handle]/settings/invite.tsx:248 -#: src/routes/(root)/[handle]/settings/invite.tsx:268 +#: src/routes/(root)/[handle]/settings/invite.tsx:249 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "Failed to send invitation" msgstr "发送邀请失败" @@ -1147,8 +1249,8 @@ msgstr "转帖失败" msgid "Failed to sign out: {0}" msgstr "登出失败:{0}" -#: src/components/BlockedAccountsList.tsx:98 -#: src/components/BlockedAccountsList.tsx:104 +#: src/components/BlockedAccountsList.tsx:96 +#: src/components/BlockedAccountsList.tsx:102 #: src/components/ProfileActionMenu.tsx:258 #: src/components/ProfileActionMenu.tsx:264 msgid "Failed to unblock this user" @@ -1160,8 +1262,8 @@ msgstr "未能取消屏蔽此用户" msgid "Failed to unfollow" msgstr "取消关注失败" -#: src/components/MutedAccountsList.tsx:97 -#: src/components/MutedAccountsList.tsx:101 +#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:99 #: src/components/ProfileActionMenu.tsx:300 #: src/components/ProfileActionMenu.tsx:308 msgid "Failed to unmute this user" @@ -1177,11 +1279,16 @@ msgstr "无法取消置顶内容" msgid "Failed to unshare post" msgstr "取消转帖失败" +#: src/components/NewsStoryCard.tsx:87 +#: src/components/NewsStoryCard.tsx:91 +msgid "Failed to update penalty." +msgstr "更新处罚失败。" + #: src/components/WebPushNotificationSettings.tsx:354 msgid "Failed to update push notification privacy" msgstr "未能更新推送通知隐私设置" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:366 msgid "Failed to update the article. Please try again." msgstr "文章更新失败,请重试。" @@ -1190,8 +1297,8 @@ msgstr "文章更新失败,请重试。" #~ msgstr "帖子更新失败,请重试。" #: src/components/article-composer/ArticleComposerForm.tsx:38 -#: src/components/NoteComposer.tsx:651 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:260 +#: src/components/NoteComposer.tsx:657 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:261 msgid "Failed to upload image" msgstr "图片上传失败" @@ -1199,7 +1306,7 @@ msgstr "图片上传失败" msgid "Failed to vote" msgstr "投票失败" -#: src/components/AppSidebar.tsx:354 +#: src/components/AppSidebar.tsx:377 msgid "Fediverse" msgstr "联邦宇宙" @@ -1207,10 +1314,14 @@ msgstr "联邦宇宙" msgid "Fediverse handle" msgstr "联邦宇宙用户名" -#: src/components/AppSidebar.tsx:245 +#: src/components/AppSidebar.tsx:268 msgid "Feed" msgstr "订阅流" +#: src/components/NewsStoryHeader.tsx:113 +msgid "First shared" +msgstr "首次转帖" + #: src/components/FollowButton.tsx:195 #: src/components/HashtagActionBar.tsx:237 msgid "Follow" @@ -1253,7 +1364,7 @@ msgstr "关注了你" msgid "Formatting" msgstr "格式" -#: src/components/NoteComposer.tsx:1337 +#: src/components/NoteComposer.tsx:1348 msgid "Generating…" msgstr "生成中…" @@ -1261,14 +1372,14 @@ msgstr "生成中…" msgid "Get browser notifications" msgstr "接收浏览器通知" -#: src/components/AppSidebar.tsx:1015 +#: src/components/AppSidebar.tsx:1061 msgid "GitHub repository" msgstr "GitHub 仓库" -#: src/routes/(root)/[handle]/bookmarks.tsx:108 -#: src/routes/(root)/[handle]/drafts/[id].tsx:59 -#: src/routes/(root)/[handle]/drafts/index.tsx:225 -#: src/routes/(root)/[handle]/drafts/new.tsx:69 +#: src/routes/(root)/[handle]/bookmarks.tsx:109 +#: src/routes/(root)/[handle]/drafts/[id].tsx:61 +#: src/routes/(root)/[handle]/drafts/index.tsx:227 +#: src/routes/(root)/[handle]/drafts/new.tsx:71 msgid "Go back" msgstr "返回" @@ -1280,13 +1391,13 @@ msgstr "返回首页" msgid "Go to Drafts" msgstr "前往草稿" -#: src/routes/(root)/[handle]/bookmarks.tsx:114 +#: src/routes/(root)/[handle]/bookmarks.tsx:115 msgid "Go to my bookmarks" msgstr "前往我的收藏" -#: src/routes/(root)/[handle]/drafts/[id].tsx:64 -#: src/routes/(root)/[handle]/drafts/index.tsx:230 -#: src/routes/(root)/[handle]/drafts/new.tsx:74 +#: src/routes/(root)/[handle]/drafts/[id].tsx:66 +#: src/routes/(root)/[handle]/drafts/index.tsx:232 +#: src/routes/(root)/[handle]/drafts/new.tsx:76 msgid "Go to my drafts" msgstr "前往我的草稿" @@ -1294,8 +1405,8 @@ msgstr "前往我的草稿" msgid "Grants one extra invitation to the most active accounts (the top third by post count) since the last regeneration cutoff." msgstr "向自上次重新发放截止时间以来最活跃的账号(按内容数排名前三分之一)额外发放一份邀请。" -#: src/components/AppSidebar.tsx:323 -#: src/components/AppSidebar.tsx:446 +#: src/components/AppSidebar.tsx:346 +#: src/components/AppSidebar.tsx:469 #: src/routes/(root).tsx:134 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/markdown.tsx:40 @@ -1324,6 +1435,19 @@ msgstr "Hackers' Pub:管理 · 邀请" msgid "Hackers' Pub: Admin · Media" msgstr "Hackers' Pub:管理 · 媒体" +#: src/routes/(root)/admin/news.tsx:320 +msgid "Hackers' Pub: Admin · News" +msgstr "Hackers' Pub:管理 · 新闻" + +#: src/routes/(root)/admin/news.tsx:124 +#~ msgid "Hackers' Pub: Admin · News scores" +#~ msgstr "Hackers' Pub:管理 · 新闻评分" + +#: src/routes/(root)/news/[link_id]/index.tsx:56 +#: src/routes/(root)/news/index.tsx:37 +msgid "Hackers' Pub: News" +msgstr "Hackers' Pub:新闻" + #: src/routes/(root)/notifications.tsx:47 msgid "Hackers' Pub: Notifications" msgstr "Hackers' Pub:通知" @@ -1346,6 +1470,10 @@ msgstr "标题 3" msgid "Hide" msgstr "隐藏" +#: src/routes/(root)/admin/news.tsx:384 +msgid "Hide links matching a URL pattern from the news feed list (every sort order). Their discussion pages stay reachable by direct link. Patterns use the URLPattern syntax, e.g. https://example.com/* or https://*.example.com/*." +msgstr "将匹配某个 URL 模式的链接从新闻信息流列表(所有排序)中隐藏。这些链接的讨论页仍可通过直接 URL 访问。模式使用 URLPattern 语法,例如 https://example.com/* 或 https://*.example.com/*。" + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Hide preview" #~ msgstr "隐藏预览" @@ -1358,23 +1486,23 @@ msgstr "隐藏" msgid "I have read and agree to the Code of conduct." msgstr "我同意 Hackers' Pub 的行为准则。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:186 +#: src/routes/(root)/[handle]/settings/preferences.tsx:187 msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "启用后,AI 将为您生成文章摘要。否则,将使用文章的前几行作为摘要。" #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1013 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1014 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:548 msgid "If you have a fediverse account, you can reply to this article from your own instance. Search {0} on your instance and reply to it." msgstr "如果你在联邦宇宙有个账户,你可以在你自己的实例里评论此文章。在你的实例搜索 {0} 后回复。" #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:393 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:394 msgid "If you have a fediverse account, you can reply to this note from your own instance. Search {0} on your instance and reply to it." msgstr "如果你在联邦宇宙有个账户,你可以在你自己的实例里评论此帖子。在你的实例搜索 {0} 后回复。" #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:471 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:472 msgid "If you have a fediverse account, you can reply to this post from your own instance. Search {0} on your instance and reply to it." msgstr "如果你有联邦宇宙账号,可以从自己的实例回复此内容。在你的实例中搜索 {0} 并回复。" @@ -1397,42 +1525,42 @@ msgstr "联邦宇宙用户名格式无效。" #: src/components/article-composer/ArticleComposerContext.tsx:321 #: src/components/article-composer/ArticleComposerContext.tsx:394 #: src/components/article-composer/ArticleComposerContext.tsx:459 -#: src/components/NoteComposer.tsx:843 -#: src/components/NoteComposer.tsx:893 -#: src/routes/(root)/[handle]/drafts/index.tsx:184 +#: src/components/NoteComposer.tsx:849 +#: src/components/NoteComposer.tsx:904 +#: src/routes/(root)/[handle]/drafts/index.tsx:185 msgid "Invalid input: {0}" msgstr "无效输入:{0}" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:349 msgid "Invalid input: {inputPath}" msgstr "无效输入:{inputPath}" #. placeholder {0}: link.inviter.name ?? link.inviter.username -#: src/routes/(root)/[handle]/invite/[id].tsx:237 +#: src/routes/(root)/[handle]/invite/[id].tsx:242 msgid "Invitation from {0}" msgstr "来自 {0} 的邀请" -#: src/routes/(root)/[handle]/settings/invite.tsx:379 +#: src/routes/(root)/[handle]/settings/invite.tsx:380 msgid "Invitation language" msgstr "邀请语言" -#: src/routes/(root)/[handle]/settings/invite.tsx:531 +#: src/routes/(root)/[handle]/settings/invite.tsx:532 msgid "Invitation link created" msgstr "邀请链接已创建" -#: src/routes/(root)/[handle]/settings/invite.tsx:582 +#: src/routes/(root)/[handle]/settings/invite.tsx:583 msgid "Invitation link deleted" msgstr "邀请链接已删除" -#: src/routes/(root)/[handle]/settings/invite.tsx:626 +#: src/routes/(root)/[handle]/settings/invite.tsx:627 msgid "Invitation links" msgstr "邀请链接" -#: src/routes/(root)/[handle]/settings/invite.tsx:258 +#: src/routes/(root)/[handle]/settings/invite.tsx:259 msgid "Invitation sent" msgstr "邀请已发送" -#: src/components/AppSidebar.tsx:773 +#: src/components/AppSidebar.tsx:796 #: src/routes/(root)/admin/invitations.tsx:148 msgid "Invitations" msgstr "邀请" @@ -1441,13 +1569,13 @@ msgstr "邀请" msgid "Invitations left" msgstr "剩余邀请" -#: src/components/AppSidebar.tsx:642 +#: src/components/AppSidebar.tsx:665 #: src/components/SettingsTabs.tsx:69 -#: src/routes/(root)/[handle]/settings/invite.tsx:295 +#: src/routes/(root)/[handle]/settings/invite.tsx:296 msgid "Invite" msgstr "邀请" -#: src/routes/(root)/[handle]/settings/invite.tsx:304 +#: src/routes/(root)/[handle]/settings/invite.tsx:305 msgid "Invite a friend" msgstr "邀请朋友" @@ -1463,23 +1591,27 @@ msgstr "邀请人" msgid "Italic" msgstr "斜体" -#: src/routes/(root)/[handle]/settings/index.tsx:474 +#: src/routes/(root)/[handle]/settings/index.tsx:475 msgid "John Doe" msgstr "张三" +#: src/components/NewsDiscussionComposer.tsx:41 +msgid "Join the discussion about this story." +msgstr "参与关于这个话题的讨论。" + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/settings/invite.tsx:921 +#: src/routes/(root)/[handle]/settings/invite.tsx:922 msgid "Joined on {0}" msgstr "于{0}加入" -#: src/components/NoteComposeModal.tsx:132 +#: src/components/NoteComposeModal.tsx:133 msgid "Keep editing" msgstr "继续编辑" #: src/components/article-composer/ArticleComposerPublishFields.tsx:53 #: src/components/LanguageList.tsx:33 #: src/components/LanguageSelect.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:489 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:490 msgid "Language" msgstr "语言" @@ -1487,7 +1619,7 @@ msgstr "语言" msgid "Language code" msgstr "语言代码" -#: src/routes/(root)/[handle]/settings/language.tsx:82 +#: src/routes/(root)/[handle]/settings/language.tsx:83 msgid "Language settings" msgstr "语言设置" @@ -1495,16 +1627,25 @@ msgstr "语言设置" msgid "Languages" msgstr "语言" +#: src/components/NewsStoryCard.tsx:205 +#: src/components/NewsStoryHeader.tsx:106 +msgid "Last active" +msgstr "最近活动" + #: src/components/admin/AdminAccountsTable.tsx:278 msgid "Last activity" msgstr "最近活动" +#: src/routes/(root)/admin/news.tsx:360 +msgid "Last recomputed:" +msgstr "上次重新计算:" + #: src/routes/(root)/admin/invitations.tsx:160 msgid "Last regenerated:" msgstr "上次重新发放:" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:450 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:451 msgid "Last used {0}" msgstr "{0}最后使用" @@ -1520,21 +1661,28 @@ msgstr "链接作者:" #~ msgid "Link author: " #~ msgstr "链接作者:" -#: src/routes/(root)/[handle]/invite/[id].tsx:255 +#: src/routes/(root)/[handle]/invite/[id].tsx:260 msgid "Link expired" msgstr "链接已过期" -#: src/routes/(root)/[handle]/settings/index.tsx:560 +#: src/routes/(root)/[handle]/settings/index.tsx:561 msgid "Link name" msgstr "链接名" +#: src/routes/(root)/admin/news.tsx:465 +msgid "Links a moderator has demoted in the popular feed. Clear a penalty to restore a link's normal ranking." +msgstr "版主在热门信息流中降级的链接。清除处罚可恢复链接的正常排名。" + +#: src/routes/(root)/news/index.tsx:41 +msgid "Links circulating across the fediverse, ranked by how much they are being shared and discussed." +msgstr "联邦宇宙中流传的链接,按照被转帖和讨论的热度排名。" + #: src/components/ui/markdown-editor.tsx:291 msgid "List" msgstr "列表" -#: src/components/BlockedAccountsList.tsx:202 -#: src/components/MutedAccountsList.tsx:199 -#: src/routes/(root)/[handle]/drafts/index.tsx:348 +#: src/components/AccountListBase.tsx:115 +#: src/routes/(root)/[handle]/drafts/index.tsx:350 msgid "Load more" msgstr "加载更多" @@ -1558,7 +1706,7 @@ msgstr "加载更多粉丝" msgid "Load more following" msgstr "加载更多关注" -#: src/routes/(root)/[handle]/settings/invite.tsx:950 +#: src/routes/(root)/[handle]/settings/invite.tsx:951 msgid "Load more invitees" msgstr "加载更多受邀者" @@ -1570,7 +1718,7 @@ msgstr "加载更多帖子" msgid "Load more notifications" msgstr "加载更多通知" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:497 msgid "Load more passkeys" msgstr "加载更多通行密钥" @@ -1582,8 +1730,9 @@ msgstr "加载更多通行密钥" msgid "Load more posts" msgstr "加载更多内容" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:197 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:204 +#: src/components/NewsDiscussionThread.tsx:378 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:205 msgid "Load more quotes" msgstr "加载更多引用" @@ -1591,15 +1740,20 @@ msgstr "加载更多引用" #~ msgid "Load more reactors (+{0})" #~ msgstr "加载更多反应者 (+{0})" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:673 +#: src/components/NewsDiscussionThread.tsx:399 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:674 msgid "Load more replies" msgstr "加载更多回复" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:207 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:216 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:208 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:217 msgid "Load more shares" msgstr "加载更多转帖" +#: src/components/NewsList.tsx:139 +msgid "Load more stories" +msgstr "加载更多新闻" + #: src/components/article-composer/ArticleComposer.tsx:31 msgid "Loading draft…" msgstr "加载草稿中…" @@ -1624,7 +1778,7 @@ msgstr "加载更多粉丝中…" msgid "Loading more following…" msgstr "正在加载更多关注…" -#: src/routes/(root)/[handle]/settings/invite.tsx:944 +#: src/routes/(root)/[handle]/settings/invite.tsx:945 msgid "Loading more invitees…" msgstr "正在加载更多受邀者…" @@ -1636,7 +1790,7 @@ msgstr "加载更多帖子中…" msgid "Loading more notifications" msgstr "正在加载更多通知…" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:493 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:494 msgid "Loading more passkeys…" msgstr "正在加载更多通行密钥…" @@ -1648,8 +1802,8 @@ msgstr "正在加载更多通行密钥…" msgid "Loading more posts…" msgstr "加载更多内容中…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:191 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:192 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:199 msgid "Loading more quotes…" msgstr "正在加载更多引用…" @@ -1657,21 +1811,28 @@ msgstr "正在加载更多引用…" msgid "Loading more reactors…" msgstr "正在加载更多反应者…" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:667 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:668 msgid "Loading more replies…" msgstr "正在加载更多回复…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:201 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:210 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:202 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:211 msgid "Loading more shares…" msgstr "正在加载更多转帖…" -#: src/components/BlockedAccountsList.tsx:196 -#: src/components/MutedAccountsList.tsx:193 +#: src/components/NewsDiscussion.tsx:79 +msgid "Loading more sharing posts…" +msgstr "正在加载更多转帖内容…" + +#: src/components/NewsList.tsx:141 +msgid "Loading more stories…" +msgstr "正在加载更多新闻…" + +#: src/components/AccountListBase.tsx:109 msgid "Loading more…" msgstr "正在加载更多…" -#: src/components/NoteComposer.tsx:1066 +#: src/components/NoteComposer.tsx:1077 msgid "Loading quoted post…" msgstr "正在加载引用内容…" @@ -1679,13 +1840,13 @@ msgstr "正在加载引用内容…" msgid "Loading search results…" msgstr "正在加载搜索结果…" -#: src/components/NoteComposer.tsx:1015 -#: src/routes/(root)/[handle]/drafts/index.tsx:342 +#: src/components/NoteComposer.tsx:1026 +#: src/routes/(root)/[handle]/drafts/index.tsx:344 #: src/routes/(root)/sign/up/[token].tsx:465 msgid "Loading…" msgstr "正在加载…" -#: src/routes/(root)/[handle]/settings/preferences.tsx:226 +#: src/routes/(root)/[handle]/settings/preferences.tsx:227 msgid "Locked to \"Only me\" because your default note privacy restricts visibility." msgstr "由于默认帖子隐私设置限制了可见性,已锁定为「仅我自己」。" @@ -1702,12 +1863,12 @@ msgid "Markdown guide" msgstr "Markdown 指南" #: src/components/article-composer/ArticleComposerForm.tsx:90 -#: src/components/NoteComposer.tsx:1232 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:417 +#: src/components/NoteComposer.tsx:1243 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:418 msgid "Markdown supported" msgstr "Markdown 可用" -#: src/components/AppSidebar.tsx:796 +#: src/components/AppSidebar.tsx:819 #: src/routes/(root)/admin/media.tsx:152 msgid "Media" msgstr "媒体" @@ -1718,6 +1879,11 @@ msgstr "媒体" msgid "Mentioned only" msgstr "只提及用户可见" +#: src/components/NewsStoryCard.tsx:225 +#: src/components/NewsStoryCard.tsx:243 +msgid "Moderate" +msgstr "管理" + #: src/components/PostEngagementBar.tsx:436 #: src/components/PostEngagementBar.tsx:437 msgid "More engagement views" @@ -1747,7 +1913,7 @@ msgstr "多选" msgid "Mute" msgstr "隐藏" -#: src/routes/(root)/[handle]/settings/blocks.tsx:84 +#: src/routes/(root)/[handle]/settings/blocks.tsx:85 msgid "Muted accounts" msgstr "已隐藏的账户" @@ -1755,7 +1921,7 @@ msgstr "已隐藏的账户" #~ msgid "Muted accounts are hidden from your feeds and stop notifying you, but you can still visit their profiles. Muting is private and is never federated." #~ msgstr "被隐藏的账户不会出现在你的时间线中,也不会向你发送通知,但你仍可访问其个人资料。隐藏是私密的,且不会进行联邦。" -#: src/routes/(root)/[handle]/settings/blocks.tsx:86 +#: src/routes/(root)/[handle]/settings/blocks.tsx:87 msgid "Muted accounts are hidden from your feeds and stop notifying you, except for replies and mentions from accounts you follow. You can still visit their profiles, and muting is private and never federated." msgstr "被隐藏的账户不会出现在你的时间线中,也不会通知你,但你关注的账户的回复和提及仍会通知你。你仍可访问其个人资料;隐藏是私密的,且不会进行联邦。" @@ -1763,11 +1929,11 @@ msgstr "被隐藏的账户不会出现在你的时间线中,也不会通知你 msgid "Mutes & blocks" msgstr "隐藏与屏蔽" -#: src/routes/(root)/[handle]/settings/blocks.tsx:77 +#: src/routes/(root)/[handle]/settings/blocks.tsx:78 msgid "Mutes and blocks" msgstr "隐藏与屏蔽" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:375 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:376 msgid "My passkey" msgstr "我的通行密钥" @@ -1783,21 +1949,25 @@ msgstr "用户名太长。不能长于 50 字符。" msgid "Native name" msgstr "本地名称" +#: src/routes/(root)/admin/news.tsx:365 +msgid "never" +msgstr "从未" + #: src/routes/(root)/admin/invitations.tsx:167 msgid "Never" msgstr "从未" -#: src/routes/(root)/[handle]/settings/invite.tsx:755 -#: src/routes/(root)/[handle]/settings/invite.tsx:821 +#: src/routes/(root)/[handle]/settings/invite.tsx:756 +#: src/routes/(root)/[handle]/settings/invite.tsx:822 msgid "Never expires" msgstr "永不过期" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:446 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:447 msgid "Never used" msgstr "从未使用过" -#: src/routes/(root)/[handle]/drafts/index.tsx:251 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/index.tsx:253 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "New article" msgstr "新建文章" @@ -1810,6 +1980,22 @@ msgstr "现在即使 Hackers' Pub 未打开,也可以显示新通知。" msgid "New posts available — click to load" msgstr "有新内容 — 点击加载" +#: src/components/NewsList.tsx:89 +msgid "Newest" +msgstr "最新" + +#: src/components/AppSidebar.tsx:244 +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:337 +#: src/routes/(root)/news/index.tsx:39 +msgid "News" +msgstr "新闻" + +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:139 +#~ msgid "News scores" +#~ msgstr "新闻评分" + #: src/components/ImageLightbox.tsx:126 msgid "Next image" msgstr "下一张图片" @@ -1822,10 +2008,14 @@ msgstr "暂无收藏" msgid "No draft to delete" msgstr "没有要删除的草稿" -#: src/routes/(root)/[handle]/drafts/index.tsx:262 +#: src/routes/(root)/[handle]/drafts/index.tsx:264 msgid "No drafts yet. Create your first article!" msgstr "还没有草稿。创建您的第一篇文章!" +#: src/routes/(root)/admin/news.tsx:428 +msgid "No exclusion patterns yet." +msgstr "暂无排除模式。" + #: src/components/ActorFollowerList.tsx:92 msgid "No followers found" msgstr "未找到粉丝" @@ -1834,9 +2024,9 @@ msgstr "未找到粉丝" msgid "No following found" msgstr "没有找到关注的用户" -#: src/routes/(root)/[handle]/invite/[id].tsx:265 -#: src/routes/(root)/[handle]/settings/invite.tsx:410 -#: src/routes/(root)/[handle]/settings/invite.tsx:831 +#: src/routes/(root)/[handle]/invite/[id].tsx:270 +#: src/routes/(root)/[handle]/settings/invite.tsx:411 +#: src/routes/(root)/[handle]/settings/invite.tsx:832 msgid "No invitations left" msgstr "没有剩余邀请名额" @@ -1852,16 +2042,24 @@ msgstr "未找到文章" msgid "No notes found" msgstr "未找到帖子" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:171 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:178 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:172 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:179 msgid "No one has quoted this yet." msgstr "暂无引用。" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:184 +#: src/components/NewsDiscussion.tsx:86 +msgid "No one has shared this link in a public post yet." +msgstr "还没有人在公开帖子中转帖这个链接。" + +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:185 msgid "No one has shared this yet." msgstr "暂无转帖。" +#: src/routes/(root)/admin/news.tsx:473 +msgid "No penalized links." +msgstr "没有受罚链接。" + #: src/components/ActorPostList.tsx:140 #: src/components/ActorSharedPostList.tsx:93 #: src/components/PersonalTimeline.tsx:289 @@ -1874,8 +2072,8 @@ msgstr "未找到内容" msgid "No previews" msgstr "不显示预览" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:189 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:190 msgid "No reactions yet." msgstr "暂无反应。" @@ -1883,6 +2081,10 @@ msgstr "暂无反应。" msgid "No reactors loaded." msgstr "未加载到反应者。" +#: src/components/NewsList.tsx:148 +msgid "No shared links yet. Once links start circulating across the fediverse, they will appear here." +msgstr "还没有被转帖的链接。当链接开始在联邦宇宙中流传时,就会显示在这里。" + #: src/routes/(root)/sign/index.tsx:223 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pub 中无此账户,请重试。" @@ -1891,16 +2093,29 @@ msgstr "Hackers' Pub 中无此账户,请重试。" msgid "No user URI provided." msgstr "未提供用户 URI。" +#: src/routes/(root)/admin/news.tsx:303 +msgid "Not authorized to clear penalties." +msgstr "无权清除处罚。" + #: src/routes/(root)/admin/media.tsx:117 msgid "Not authorized to delete orphan media." msgstr "无权删除孤立媒体。" +#: src/routes/(root)/admin/news.tsx:244 +#: src/routes/(root)/admin/news.tsx:274 +msgid "Not authorized to manage exclusions." +msgstr "无权管理排除模式。" + +#: src/routes/(root)/admin/news.tsx:203 +msgid "Not authorized to recompute news scores." +msgstr "无权重新计算新闻评分。" + #: src/routes/(root)/admin/invitations.tsx:116 msgid "Not authorized to regenerate invitations." msgstr "无权重新发放邀请。" -#: src/routes/(root)/[handle]/invite/[id].tsx:222 -#: src/routes/(root)/[handle]/invite/[id].tsx:225 +#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:230 msgid "Not found" msgstr "未找到" @@ -1912,26 +2127,30 @@ msgstr "未找到" #~ msgid "Note" #~ msgstr "帖子" -#: src/components/NoteComposer.tsx:885 +#: src/routes/(root)/admin/news.tsx:407 +msgid "Note (optional)" +msgstr "备注(可选)" + +#: src/components/NoteComposer.tsx:896 msgid "Note created successfully" msgstr "帖子创建成功" #. placeholder {0}: "REL_ME_ATTR" -#: src/routes/(root)/[handle]/settings/index.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:524 msgid "Note that you can verify your links belong to you by making the linked pages also link to your Hackers' Pub profile with {0} attribute." msgstr "你可以验证这些链接确实属于你。在链接的页面上使用 {0} 属性链接回你的 Hackers' Pub 个人资料页。" -#: src/components/NoteComposer.tsx:833 +#: src/components/NoteComposer.tsx:839 msgid "Note updated" msgstr "帖子已更新。" #: src/components/ProfileTabs.tsx:44 -#: src/routes/(root)/[handle]/bookmarks.tsx:137 +#: src/routes/(root)/[handle]/bookmarks.tsx:138 msgid "Notes" msgstr "帖子" #: src/components/MarkdownEditor.tsx:193 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:463 msgid "Nothing to preview" msgstr "没有可预览的内容" @@ -1944,7 +2163,7 @@ msgstr "未授予通知权限。" msgid "Notification preview privacy" msgstr "通知预览隐私" -#: src/components/AppSidebar.tsx:574 +#: src/components/AppSidebar.tsx:597 msgid "Notifications" msgstr "通知" @@ -1957,7 +2176,7 @@ msgstr "此网站的通知已被屏蔽。" msgid "Notifications are blocked in your browser settings." msgstr "浏览器设置已屏蔽通知。" -#: src/routes/(root)/[handle]/settings/invite.tsx:782 +#: src/routes/(root)/[handle]/settings/invite.tsx:783 msgid "Number of invitations" msgstr "邀请次数" @@ -1982,7 +2201,7 @@ msgstr "或" msgid "Or enter the code from the email" msgstr "或输入邮件中的验证码" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:877 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:878 msgid "Other languages" msgstr "其他语言" @@ -1994,31 +2213,39 @@ msgstr "页面未找到" msgid "Passkey authentication failed" msgstr "通行密钥验证失败" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:370 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:371 msgid "Passkey name" msgstr "通行密钥名称" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:265 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:266 msgid "Passkey registered successfully" msgstr "通行密钥注册成功" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:312 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 msgid "Passkey revoked" msgstr "通行密钥已撤销" #: src/components/SettingsTabs.tsx:77 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:355 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:356 msgid "Passkeys" msgstr "通行密钥" -#: src/routes/(root)/[handle]/bookmarks.tsx:98 -#: src/routes/(root)/[handle]/bookmarks.tsx:101 -#: src/routes/(root)/[handle]/drafts/[id].tsx:50 -#: src/routes/(root)/[handle]/drafts/[id].tsx:51 -#: src/routes/(root)/[handle]/drafts/index.tsx:213 -#: src/routes/(root)/[handle]/drafts/index.tsx:216 -#: src/routes/(root)/[handle]/drafts/new.tsx:60 -#: src/routes/(root)/[handle]/drafts/new.tsx:61 +#: src/routes/(root)/admin/news.tsx:463 +msgid "Penalized links" +msgstr "受罚链接" + +#: src/routes/(root)/admin/news.tsx:297 +msgid "Penalty cleared." +msgstr "已清除处罚。" + +#: src/routes/(root)/[handle]/bookmarks.tsx:99 +#: src/routes/(root)/[handle]/bookmarks.tsx:102 +#: src/routes/(root)/[handle]/drafts/[id].tsx:52 +#: src/routes/(root)/[handle]/drafts/[id].tsx:53 +#: src/routes/(root)/[handle]/drafts/index.tsx:215 +#: src/routes/(root)/[handle]/drafts/index.tsx:218 +#: src/routes/(root)/[handle]/drafts/new.tsx:62 +#: src/routes/(root)/[handle]/drafts/new.tsx:63 msgid "Permission denied" msgstr "权限被拒绝" @@ -2026,21 +2253,21 @@ msgstr "权限被拒绝" msgid "Pin to profile" msgstr "置顶到个人资料" -#: src/routes/(root)/[handle]/(profile)/index.tsx:302 +#: src/routes/(root)/[handle]/(profile)/index.tsx:305 msgid "Pinned posts" msgstr "已置顶内容" -#: src/routes/(root)/[handle]/settings/index.tsx:187 +#: src/routes/(root)/[handle]/settings/index.tsx:188 msgid "Please choose an image file smaller than 5 MiB." msgstr "请选择小于 5 MiB 的图片文件。" -#: src/routes/(root)/[handle]/settings/invite.tsx:249 -#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:250 +#: src/routes/(root)/[handle]/settings/invite.tsx:541 msgid "Please correct the errors and try again." msgstr "请修正错误并重试。" #: src/components/article-composer/ArticleComposerForm.tsx:53 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:383 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:384 msgid "Please enter a title for your article." msgstr "请输入文章标题。" @@ -2048,9 +2275,9 @@ msgstr "请输入文章标题。" msgid "Please enter your Fediverse handle." msgstr "请输入你的联邦宇宙用户名。" -#: src/routes/(root)/[handle]/drafts/[id].tsx:55 -#: src/routes/(root)/[handle]/drafts/index.tsx:220 -#: src/routes/(root)/[handle]/drafts/new.tsx:65 +#: src/routes/(root)/[handle]/drafts/[id].tsx:57 +#: src/routes/(root)/[handle]/drafts/index.tsx:222 +#: src/routes/(root)/[handle]/drafts/new.tsx:67 msgid "Please sign in to access this page" msgstr "请登录以访问此页面" @@ -2062,6 +2289,10 @@ msgstr "请登录后投票" msgid "Poll closed" msgstr "投票已结束" +#: src/components/NewsList.tsx:88 +msgid "Popular" +msgstr "热门" + #: src/components/PostActionMenu.tsx:291 msgid "Post deleted" msgstr "内容已删除" @@ -2079,27 +2310,27 @@ msgstr "已取消置顶内容" msgid "Posts" msgstr "内容" -#: src/routes/(root)/[handle]/settings/preferences.tsx:183 +#: src/routes/(root)/[handle]/settings/preferences.tsx:184 msgid "Prefer AI-generated summary" msgstr "优先使用 AI 生成的摘要" #: src/components/SettingsTabs.tsx:53 -#: src/routes/(root)/[handle]/settings/preferences.tsx:169 #: src/routes/(root)/[handle]/settings/preferences.tsx:170 +#: src/routes/(root)/[handle]/settings/preferences.tsx:171 msgid "Preferences" msgstr "偏好设置" -#: src/routes/(root)/[handle]/invite/[id].tsx:387 +#: src/routes/(root)/[handle]/invite/[id].tsx:392 msgid "Preferred language" msgstr "首选语言" -#: src/routes/(root)/[handle]/settings/language.tsx:83 +#: src/routes/(root)/[handle]/settings/language.tsx:84 msgid "Preferred languages" msgstr "偏好语言" #: src/components/article-composer/ArticleComposerForm.tsx:111 #: src/components/MarkdownEditor.tsx:164 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:426 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:427 msgid "Preview" msgstr "预览" @@ -2111,7 +2342,7 @@ msgstr "上一张图片" msgid "Priority" msgstr "优先级" -#: src/components/AppSidebar.tsx:982 +#: src/components/AppSidebar.tsx:1028 #: src/routes/(root)/privacy.tsx:40 msgid "Privacy policy" msgstr "隐私政策" @@ -2124,8 +2355,8 @@ msgstr "个人资料" msgid "Profile actions" msgstr "个人资料操作" -#: src/routes/(root)/[handle]/settings/index.tsx:124 #: src/routes/(root)/[handle]/settings/index.tsx:125 +#: src/routes/(root)/[handle]/settings/index.tsx:126 msgid "Profile settings" msgstr "个人资料设置" @@ -2155,8 +2386,8 @@ msgstr "发布中…" msgid "Push notification privacy updated" msgstr "已更新推送通知隐私设置" -#: src/routes/(root)/[handle]/settings/invite.tsx:647 -#: src/routes/(root)/[handle]/settings/invite.tsx:713 +#: src/routes/(root)/[handle]/settings/invite.tsx:648 +#: src/routes/(root)/[handle]/settings/invite.tsx:714 msgid "QR code" msgstr "二维码" @@ -2170,7 +2401,7 @@ msgstr "搜索关键词不能为空" msgid "Quiet public" msgstr "悄悄公开" -#: src/components/NoteComposeModal.tsx:71 +#: src/components/NoteComposeModal.tsx:72 #: src/components/PostEngagementBar.tsx:270 #: src/components/ui/markdown-editor.tsx:289 msgid "Quote" @@ -2194,8 +2425,8 @@ msgid "Quoted post hidden" msgstr "引用内容已隐藏" #: src/components/EngagementTabs.tsx:46 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:100 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:111 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:101 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:112 msgid "Quotes" msgstr "引用" @@ -2218,8 +2449,8 @@ msgid "reaction" msgstr "反应" #: src/components/EngagementTabs.tsx:55 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:159 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:173 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:160 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:174 msgid "Reactions" msgstr "反应" @@ -2232,6 +2463,10 @@ msgstr "阅读完整文章" #~ msgid "Read the full Code of conduct" #~ msgstr "阅读完整的行为准则" +#: src/routes/(root)/admin/news.tsx:344 +msgid "Rebuilds the popularity score of every shared link from scratch. The operation is idempotent and safe to run at any time; scores normally stay fresh on their own, so this is mainly a manual backstop and a development tool." +msgstr "从头重新计算每个被转帖链接的热度评分。该操作是幂等的,随时运行都安全。评分通常会自动保持最新,因此这主要是一个手动备用手段和开发工具。" + #: src/components/WebPushPromptBanner.tsx:252 msgid "Receive new notifications immediately, even when this tab is closed." msgstr "即使此标签页已关闭,也能立即收到新通知。" @@ -2240,10 +2475,19 @@ msgstr "即使此标签页已关闭,也能立即收到新通知。" msgid "Receive notifications immediately through this browser, even when this tab is closed." msgstr "通过此浏览器立即接收通知,即使此标签页已关闭。" -#: src/components/AppSidebar.tsx:901 +#: src/components/AppSidebar.tsx:947 msgid "Recent drafts" msgstr "最近的草稿" +#: src/routes/(root)/admin/news.tsx:342 +#: src/routes/(root)/admin/news.tsx:375 +msgid "Recompute news scores" +msgstr "重新计算新闻评分" + +#: src/routes/(root)/admin/news.tsx:374 +msgid "Recomputing…" +msgstr "正在重新计算…" + #: src/routes/(root)/admin/invitations.tsx:203 msgid "Regenerate" msgstr "重新发放" @@ -2260,23 +2504,23 @@ msgstr "重新发放邀请" msgid "Regenerating…" msgstr "正在重新发放…" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Register" msgstr "注册" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:362 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:363 msgid "Register a passkey" msgstr "注册通行密钥" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:364 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:365 msgid "Register a passkey to sign in to your account. You can use a passkey instead of receiving a sign-in link by email." msgstr "为你的账户注册通行密钥。你可以使用通行密钥登录,而不是通过电子邮件接收登录链接。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:393 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:394 msgid "Registered passkeys" msgstr "已注册的通行密钥" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Registering…" msgstr "正在注册…" @@ -2287,6 +2531,7 @@ msgid "Remote follow" msgstr "远程关注" #: src/components/LanguageList.tsx:225 +#: src/routes/(root)/admin/news.tsx:451 msgid "Remove" msgstr "移除" @@ -2304,13 +2549,13 @@ msgstr "取消收藏" msgid "Remove from sidebar" msgstr "从侧边栏移除" -#: src/components/NoteComposer.tsx:1358 -#: src/components/NoteComposer.tsx:1359 +#: src/components/NoteComposer.tsx:1369 +#: src/components/NoteComposer.tsx:1370 msgid "Remove image" msgstr "移除图片" -#: src/components/NoteComposer.tsx:1113 -#: src/components/NoteComposer.tsx:1114 +#: src/components/NoteComposer.tsx:1124 +#: src/components/NoteComposer.tsx:1125 msgid "Remove quote" msgstr "移除引用" @@ -2320,11 +2565,11 @@ msgstr "移除已过期且不再附加到头像、帖子、文章草稿或文章 #: src/components/article-composer/ArticleComposerForm.tsx:134 #: src/components/MarkdownEditor.tsx:181 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:452 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:453 msgid "Rendering…" msgstr "渲染中…" -#: src/components/NoteComposeModal.tsx:70 +#: src/components/NoteComposeModal.tsx:71 #: src/components/PostEngagementBar.tsx:258 msgid "Reply" msgstr "回复" @@ -2333,20 +2578,20 @@ msgstr "回复" msgid "Replying is not available for this post" msgstr "无法回复此内容" -#: src/components/NoteComposer.tsx:1007 +#: src/components/NoteComposer.tsx:1018 msgid "Replying to" msgstr "回复给" -#: src/components/AppSidebar.tsx:498 +#: src/components/AppSidebar.tsx:521 msgid "Return to old UI" msgstr "返回旧界面" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:475 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:526 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:476 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:527 msgid "Revoke" msgstr "撤销" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:515 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:516 msgid "Revoke passkey" msgstr "撤销通行密钥" @@ -2359,14 +2604,14 @@ msgstr "撤销引用" msgid "Revoke this quote?" msgstr "撤销此引用?" -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Save" msgstr "保存" -#: src/components/NoteComposer.tsx:1412 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 +#: src/components/NoteComposer.tsx:1423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 msgid "Save changes" msgstr "保存更改" @@ -2379,16 +2624,16 @@ msgid "Save draft to see preview" msgstr "保存草稿以查看预览" #: src/components/article-composer/ArticleComposerActions.tsx:36 -#: src/components/NoteComposer.tsx:1413 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/components/NoteComposer.tsx:1424 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Saving…" msgstr "保存中…" #: src/components/admin/AdminAccountsTable.tsx:202 -#: src/components/AppSidebar.tsx:377 +#: src/components/AppSidebar.tsx:400 #: src/components/SearchForm.tsx:65 #: src/components/SearchForm.tsx:80 msgid "Search" @@ -2423,16 +2668,16 @@ msgstr "请选择一个选项" msgid "Select options" msgstr "请选择选项" -#: src/routes/(root)/[handle]/settings/language.tsx:84 +#: src/routes/(root)/[handle]/settings/language.tsx:85 msgid "Select your preferred languages in order of preference. This will help tailor content to your preferences." msgstr "按偏好顺序选择您的偏好语言。这将有助于根据您的偏好定制内容。" -#: src/routes/(root)/[handle]/settings/invite.tsx:413 +#: src/routes/(root)/[handle]/settings/invite.tsx:414 msgid "Send" msgstr "发送" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 -#: src/routes/(root)/[handle]/settings/invite.tsx:412 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 +#: src/routes/(root)/[handle]/settings/invite.tsx:413 msgid "Sending…" msgstr "发送中…" @@ -2444,11 +2689,11 @@ msgstr "敏感内容" msgid "Separate tags with spaces. Tags help readers discover your article." msgstr "标签之间用空格分隔。标签有助于读者发现您的文章。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:171 +#: src/routes/(root)/[handle]/settings/preferences.tsx:172 msgid "Set your personal preferences." msgstr "设置您的个人偏好设置。" -#: src/components/AppSidebar.tsx:674 +#: src/components/AppSidebar.tsx:697 msgid "Settings" msgstr "设置" @@ -2456,10 +2701,19 @@ msgstr "设置" msgid "Share" msgstr "分享" +#: src/components/NewsStoryCard.tsx:198 +#: src/components/NewsStoryHeader.tsx:132 +msgid "Share this link" +msgstr "转帖此链接" + +#: src/components/NewsDiscussionComposer.tsx:30 +msgid "Share your opinion on this story…" +msgstr "分享你对这个话题的看法…" + #: src/components/EngagementTabs.tsx:37 #: src/components/ProfileTabs.tsx:54 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:101 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:114 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:102 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:115 msgid "Shares" msgstr "转帖" @@ -2474,6 +2728,15 @@ msgstr "无法转帖此内容。" msgid "Show" msgstr "显示" +#. placeholder {0}: childCount() +#: src/components/NewsDiscussionThread.tsx:356 +msgid "Show {0} more in this thread" +msgstr "显示该讨论中的另外 {0} 条" + +#: src/components/NewsDiscussion.tsx:77 +msgid "Show more sharing posts" +msgstr "显示更多转帖内容" + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Show preview" #~ msgstr "显示预览" @@ -2482,7 +2745,7 @@ msgstr "显示" msgid "Show sensitive content" msgstr "显示敏感内容" -#: src/components/AppSidebar.tsx:535 +#: src/components/AppSidebar.tsx:558 #: src/routes/(root)/sign/index.tsx:382 msgid "Sign in" msgstr "登录" @@ -2491,6 +2754,10 @@ msgstr "登录" msgid "Sign in to Hackers' Pub" msgstr "登录 Hackers' Pub" +#: src/components/NewsDiscussionComposer.tsx:44 +msgid "Sign in to post" +msgstr "登录后发表" + #: src/components/QuestionCard.tsx:394 msgid "Sign in to vote" msgstr "登录后投票" @@ -2499,11 +2766,11 @@ msgstr "登录后投票" msgid "Sign in with passkey" msgstr "使用通行密钥登录" -#: src/components/AppSidebar.tsx:964 +#: src/components/AppSidebar.tsx:1010 msgid "Sign out" msgstr "登出" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 #: src/routes/(root)/sign/up/[token].tsx:494 msgid "Sign up" msgstr "注册" @@ -2536,7 +2803,7 @@ msgstr "URL 别名" msgid "Slug cannot be empty" msgstr "URL 别名不能为空" -#: src/components/NoteComposer.tsx:613 +#: src/components/NoteComposer.tsx:619 msgid "Some images were skipped because the limit of {MAX_MEDIA} was reached" msgstr "已达到 {MAX_MEDIA} 张图片上限,部分图片已被跳过" @@ -2551,22 +2818,22 @@ msgstr "出现错误,请重试。" #: src/components/article-composer/ArticleComposerContext.tsx:309 #: src/components/article-composer/ArticleComposerContext.tsx:384 #: src/components/article-composer/ArticleComposerContext.tsx:449 -#: src/components/NoteComposer.tsx:832 -#: src/components/NoteComposer.tsx:884 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:331 -#: src/routes/(root)/[handle]/drafts/index.tsx:174 +#: src/components/NoteComposer.tsx:838 +#: src/components/NoteComposer.tsx:895 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/drafts/index.tsx:175 msgid "Success" msgstr "成功" -#: src/routes/(root)/[handle]/settings/language.tsx:147 +#: src/routes/(root)/[handle]/settings/language.tsx:148 msgid "Successfully saved language preferences" msgstr "成功保存语言偏好" -#: src/routes/(root)/[handle]/settings/preferences.tsx:133 +#: src/routes/(root)/[handle]/settings/preferences.tsx:134 msgid "Successfully saved preferences" msgstr "设置已成功保存" -#: src/routes/(root)/[handle]/settings/index.tsx:328 +#: src/routes/(root)/[handle]/settings/index.tsx:329 msgid "Successfully saved settings" msgstr "设置保存成功" @@ -2575,9 +2842,9 @@ msgid "Summarized by LLM" msgstr "由 AI 生成的摘要" #: src/components/DocumentView.tsx:38 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:721 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:729 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1056 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:722 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:730 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1057 msgid "Table of contents" msgstr "目录" @@ -2586,8 +2853,8 @@ msgstr "目录" #~ msgstr "标签" #: src/components/article-composer/ArticleComposerForm.tsx:158 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:478 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1065 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:479 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1066 msgid "Tags" msgstr "标签" @@ -2595,65 +2862,73 @@ msgstr "标签" msgid "Tell us about yourself…" msgstr "请告诉我们关于您自己的信息…" +#: src/routes/(root)/admin/news.tsx:237 +msgid "That is not a valid URL pattern." +msgstr "这不是有效的 URL 模式。" + #: src/components/WebPushNotificationSettings.tsx:158 #: src/components/WebPushPromptBanner.tsx:93 msgid "The browser did not provide a complete push subscription." msgstr "浏览器未提供完整的推送通知订阅。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:200 +#: src/routes/(root)/[handle]/settings/preferences.tsx:201 msgid "The default privacy setting for your notes." msgstr "您帖子的默认隐私设置。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:212 +#: src/routes/(root)/[handle]/settings/preferences.tsx:213 msgid "The default privacy setting for your shares." msgstr "您转帖的默认隐私设置。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:227 +#: src/routes/(root)/[handle]/settings/preferences.tsx:228 msgid "The default quote permission for your notes." msgstr "帖子的默认引用权限设置。" -#: src/routes/(root)/[handle]/invite/[id].tsx:169 -#: src/routes/(root)/[handle]/settings/invite.tsx:352 +#: src/routes/(root)/[handle]/invite/[id].tsx:174 +#: src/routes/(root)/[handle]/settings/invite.tsx:353 msgid "The email address is invalid." msgstr "电子邮件地址无效。" -#: src/routes/(root)/[handle]/settings/invite.tsx:347 +#: src/routes/(root)/[handle]/settings/invite.tsx:348 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "电子邮件地址不仅用于接收邀请,还用于登录账户。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:395 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:396 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "以下通行密钥已注册到你的账户。你可以使用它们登录你的账户。" -#: src/routes/(root)/[handle]/invite/[id].tsx:187 +#: src/routes/(root)/[handle]/invite/[id].tsx:192 msgid "The invitation email could not be sent. Please try again later." msgstr "邀请邮件发送失败。请稍后再试。" -#: src/routes/(root)/[handle]/settings/invite.tsx:259 +#: src/routes/(root)/[handle]/settings/invite.tsx:260 msgid "The invitation has been sent successfully." msgstr "邀请已成功发送。" -#: src/routes/(root)/[handle]/settings/invite.tsx:590 +#: src/routes/(root)/[handle]/settings/invite.tsx:591 msgid "The invitation link could not be found or you are not authorized to delete it." msgstr "找不到该邀请链接,或您无权删除该链接。" -#: src/routes/(root)/[handle]/settings/invite.tsx:612 +#: src/routes/(root)/[handle]/settings/invite.tsx:613 msgid "The invitation link has been copied to the clipboard." msgstr "邀请链接已复制到剪贴板。" -#: src/routes/(root)/[handle]/settings/invite.tsx:532 +#: src/routes/(root)/[handle]/settings/invite.tsx:533 msgid "The invitation link has been created successfully." msgstr "邀请链接已成功创建。" -#: src/routes/(root)/[handle]/settings/invite.tsx:583 +#: src/routes/(root)/[handle]/settings/invite.tsx:584 msgid "The invitation link has been deleted successfully." msgstr "邀请链接已成功删除。" +#: src/components/NewsDiscussionComposer.tsx:34 +msgid "The link to this story is added to your post automatically." +msgstr "该话题的链接会自动添加到你的内容中。" + #: src/components/NotFoundPage.tsx:45 msgid "The page you're looking for doesn't exist or has been moved." msgstr "您访问的页面不存在或已被移动。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:314 msgid "The passkey has been successfully revoked." msgstr "通行密钥已成功撤销。" @@ -2671,7 +2946,7 @@ msgstr "注册链接无效。请确保你使用的是你收到的正确邮件链 #. placeholder {0}: "GITHUB_REPOSITORY" #. placeholder {1}: "AGPL-3.0" -#: src/components/AppSidebar.tsx:1006 +#: src/components/AppSidebar.tsx:1052 msgid "The source code of this website is available on {0} under the {1} license." msgstr "可在 {0} 上以 {1} 许可获取该网站的源代码。" @@ -2679,7 +2954,7 @@ msgstr "可在 {0} 上以 {1} 许可获取该网站的源代码。" msgid "The title will appear at the top of your article and in link previews." msgstr "标题将显示在文章顶部和链接预览中。" -#: src/routes/(root)/[handle]/settings/index.tsx:603 +#: src/routes/(root)/[handle]/settings/index.tsx:604 msgid "The URL of the link, e.g., https://github.com/yourhandle." msgstr "该链接的 URL,例如 https://github.com/nideyonghuming 。" @@ -2697,26 +2972,26 @@ msgstr "此操作无法撤销。这将永久删除该内容。" msgid "This browser does not support Web Push." msgstr "此浏览器不支持 Web Push。" -#: src/routes/(root)/[handle]/invite/[id].tsx:173 -#: src/routes/(root)/[handle]/invite/[id].tsx:177 +#: src/routes/(root)/[handle]/invite/[id].tsx:178 +#: src/routes/(root)/[handle]/invite/[id].tsx:182 msgid "This email is already associated with an existing account." msgstr "此邮箱地址已关联到现有账户。" -#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:232 msgid "This invitation link does not exist or has been deleted." msgstr "此邀请链接不存在或已被删除。" -#: src/routes/(root)/[handle]/invite/[id].tsx:161 -#: src/routes/(root)/[handle]/invite/[id].tsx:257 +#: src/routes/(root)/[handle]/invite/[id].tsx:166 +#: src/routes/(root)/[handle]/invite/[id].tsx:262 msgid "This invitation link has expired." msgstr "此邀请链接已过期。" -#: src/routes/(root)/[handle]/invite/[id].tsx:164 -#: src/routes/(root)/[handle]/invite/[id].tsx:267 +#: src/routes/(root)/[handle]/invite/[id].tsx:169 +#: src/routes/(root)/[handle]/invite/[id].tsx:272 msgid "This invitation link has no remaining invitations." msgstr "此邀请链接已无剩余邀请次数。" -#: src/routes/(root)/[handle]/invite/[id].tsx:159 +#: src/routes/(root)/[handle]/invite/[id].tsx:164 msgid "This invitation link was not found." msgstr "未找到此邀请链接。" @@ -2748,7 +3023,7 @@ msgstr "此服务器尚未配置 Web Push。" msgid "This service does not support remote follow." msgstr "该服务不支持远程关注。" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:552 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:553 msgid "This usually takes about a minute. The page will update automatically when the translation is ready." msgstr "通常需要一分钟左右。翻译完成后页面将自动更新。" @@ -2765,7 +3040,7 @@ msgid "Timeline" msgstr "时间线" #: src/components/article-composer/ArticleComposerForm.tsx:49 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:380 msgid "Title" msgstr "标题" @@ -2789,7 +3064,7 @@ msgstr "共 {0} 个" #. placeholder {0}: "LANGUAGE" #: src/components/ArticleCard.tsx:350 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:861 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:862 msgid "Translated from {0}" msgstr "翻译自{0}" @@ -2797,18 +3072,18 @@ msgstr "翻译自{0}" #~ msgid "Translating to {0}…" #~ msgstr "正在翻译为{0}…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:548 msgid "Translating to {name}…" msgstr "正在翻译为{name}…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:543 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:544 msgid "Translating…" msgstr "正在翻译…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:378 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:401 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:410 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:588 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:402 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:411 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:589 msgid "Translation request failed" msgstr "翻译请求失败" @@ -2816,17 +3091,17 @@ msgstr "翻译请求失败" #~ msgid "Translation request failed for {0}" #~ msgstr "向{0}的翻译请求失败" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:593 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:594 msgid "Translation request failed for {name}" msgstr "向{name}的翻译请求失败" #: src/app.tsx:125 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:601 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:602 msgid "Try again" msgstr "重试" #: src/components/article-composer/ArticleComposerForm.tsx:162 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:482 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:483 msgid "Type tags separated by spaces" msgstr "输入标签,用空格分隔" @@ -2844,7 +3119,7 @@ msgstr "无法添加反应。请重试。" msgid "Unable to remove reaction. Please try again." msgstr "无法移除反应。请重试。" -#: src/components/BlockedAccountsList.tsx:181 +#: src/components/BlockedAccountsList.tsx:117 #: src/components/ProfileActionMenu.tsx:377 #: src/components/ProfileActionMenu.tsx:405 #: src/components/ProfileActionMenu.tsx:413 @@ -2860,7 +3135,7 @@ msgstr "取消屏蔽用户?" msgid "Unfollow" msgstr "取消关注" -#: src/components/MutedAccountsList.tsx:178 +#: src/components/MutedAccountsList.tsx:114 #: src/components/ProfileActionMenu.tsx:361 msgid "Unmute" msgstr "取消隐藏" @@ -2873,23 +3148,27 @@ msgstr "从个人资料取消置顶" msgid "Unshare" msgstr "取消转帖" -#: src/routes/(root)/[handle]/settings/index.tsx:126 +#: src/routes/(root)/[handle]/settings/index.tsx:127 msgid "Update your profile information, including your avatar, username, display name, bio, and links." msgstr "更新您的个人资料信息,包括头像、用户名、昵称、个人简介和链接。" #. placeholder {0}: new Date(edge.node.updated).toLocaleDateString() -#: src/routes/(root)/[handle]/drafts/index.tsx:304 +#: src/routes/(root)/[handle]/drafts/index.tsx:306 msgid "Updated {0}" msgstr "更新于 {0}" -#: src/components/NoteComposer.tsx:1274 +#: src/components/NoteComposer.tsx:1285 msgid "Upload progress" msgstr "上传进度" -#: src/routes/(root)/[handle]/settings/index.tsx:585 +#: src/routes/(root)/[handle]/settings/index.tsx:586 msgid "URL" msgstr "URL" +#: src/routes/(root)/admin/news.tsx:394 +msgid "URL pattern" +msgstr "URL 模式" + #: src/components/ProfileActionMenu.tsx:277 msgid "User blocked" msgstr "已屏蔽用户" @@ -2902,17 +3181,17 @@ msgstr "已隐藏用户" msgid "User not found." msgstr "未找到用户信息。" -#: src/components/BlockedAccountsList.tsx:95 +#: src/components/BlockedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:259 msgid "User unblocked" msgstr "已取消屏蔽用户" -#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:304 msgid "User unmuted" msgstr "已取消隐藏用户" -#: src/routes/(root)/[handle]/settings/index.tsx:413 +#: src/routes/(root)/[handle]/settings/index.tsx:414 #: src/routes/(root)/sign/up/[token].tsx:331 msgid "Username" msgstr "用户名" @@ -2933,7 +3212,7 @@ msgstr "需要用户名" msgid "Username is too long. Maximum length is 15 characters." msgstr "用户名太长。不能长于 15 字符。" -#: src/routes/(root)/[handle]/settings/invite.tsx:435 +#: src/routes/(root)/[handle]/settings/invite.tsx:436 msgid "Users you have invited" msgstr "您邀请的用户" @@ -2947,7 +3226,7 @@ msgstr "已于{1}验证此链接归{0}所有" msgid "Verifying your invitation…" msgstr "正在验证您的邀请…" -#: src/components/AppSidebar.tsx:927 +#: src/components/AppSidebar.tsx:973 msgid "View all drafts →" msgstr "查看所有草稿 →" @@ -3015,7 +3294,7 @@ msgstr "已投票" msgid "Voting…" msgstr "正在投票…" -#: src/components/NoteComposer.tsx:611 +#: src/components/NoteComposer.tsx:617 msgid "Warning" msgstr "警告" @@ -3023,11 +3302,11 @@ msgstr "警告" msgid "We couldn't reach the server" msgstr "无法连接到服务器" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:598 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:599 msgid "We couldn't reach the translation service. Try again, or come back in a few minutes." msgstr "无法连接到翻译服务。请重试或几分钟后再来。" -#: src/routes/(root)/[handle]/settings/index.tsx:566 +#: src/routes/(root)/[handle]/settings/index.tsx:567 msgid "Website" msgstr "网站" @@ -3039,7 +3318,7 @@ msgstr "欢迎来到 Hackers' Pub!请填写以下表单完成注册。" msgid "What is Hackers' Pub?" msgstr "什么是 Hackers' Pub?" -#: src/components/NoteComposer.tsx:1153 +#: src/components/NoteComposer.tsx:1164 msgid "What's on your mind?" msgstr "你在想啥?" @@ -3051,25 +3330,25 @@ msgstr "启用后,AI 可能会自动将这篇文章翻译成其他语言。" #~ msgid "Who can quote this note" #~ msgstr "谁可以引用此帖子" -#: src/components/AppSidebar.tsx:285 +#: src/components/AppSidebar.tsx:308 msgid "Without shares" msgstr "不含转帖" #: src/components/article-composer/ArticleComposerForm.tsx:108 #: src/components/MarkdownEditor.tsx:161 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:424 msgid "Write" msgstr "撰写" -#: src/components/NoteComposeModal.tsx:109 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1004 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:384 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:462 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:538 +#: src/components/NoteComposeModal.tsx:110 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1005 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:385 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:463 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:539 msgid "Write a reply…" msgstr "写个回复…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:437 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:438 msgid "Write your article here." msgstr "在此撰写您的文章。" @@ -3094,53 +3373,53 @@ msgstr "你已被此用户屏蔽。你无法关注该用户或查看该用户的 msgid "You are blocking this user. They can't follow you or see your posts." msgstr "你正在屏蔽此用户。该用户无法关注你或查看你的内容。" -#: src/components/NoteComposer.tsx:602 +#: src/components/NoteComposer.tsx:608 msgid "You can attach up to {MAX_MEDIA} images" msgstr "最多可附加 {MAX_MEDIA} 张图片" -#: src/routes/(root)/[handle]/settings/index.tsx:448 +#: src/routes/(root)/[handle]/settings/index.tsx:449 msgid "You can change it only once, and the old username will become available to others." msgstr "你只能更改一次用户名,而旧的用户名会公开为别人使用。" -#: src/routes/(root)/[handle]/settings/index.tsx:614 +#: src/routes/(root)/[handle]/settings/index.tsx:615 msgid "You can leave this empty to remove the link." msgstr "您可以将此处留空以删除链接。" -#: src/routes/(root)/[handle]/settings/invite.tsx:397 -#: src/routes/(root)/[handle]/settings/invite.tsx:802 +#: src/routes/(root)/[handle]/settings/invite.tsx:398 +#: src/routes/(root)/[handle]/settings/invite.tsx:803 msgid "You can leave this field empty." msgstr "你可以留空此字段。" -#: src/routes/(root)/[handle]/drafts/new.tsx:64 +#: src/routes/(root)/[handle]/drafts/new.tsx:66 msgid "You can only create drafts for your own account" msgstr "您只能为自己的账户创建草稿" -#: src/routes/(root)/[handle]/drafts/[id].tsx:54 +#: src/routes/(root)/[handle]/drafts/[id].tsx:56 msgid "You can only edit your own drafts" msgstr "您只能编辑自己的草稿" -#: src/routes/(root)/[handle]/bookmarks.tsx:103 +#: src/routes/(root)/[handle]/bookmarks.tsx:104 msgid "You can only view your own bookmarks" msgstr "您只能查看自己的收藏" -#: src/routes/(root)/[handle]/drafts/index.tsx:219 +#: src/routes/(root)/[handle]/drafts/index.tsx:221 msgid "You can only view your own drafts" msgstr "您只能查看自己的草稿" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:404 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:405 msgid "You don't have any passkeys registered yet." msgstr "您尚未注册任何通行密钥。" -#: src/routes/(root)/[handle]/settings/invite.tsx:309 -#: src/routes/(root)/[handle]/settings/invite.tsx:420 +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:421 msgid "You have no invitations left. Please wait until you receive more." msgstr "你没有剩余邀请名额。请稍后再试。" -#: src/components/BlockedAccountsList.tsx:116 +#: src/components/BlockedAccountsList.tsx:119 msgid "You haven't blocked anyone." msgstr "你还没有屏蔽任何账户。" -#: src/components/MutedAccountsList.tsx:113 +#: src/components/MutedAccountsList.tsx:116 msgid "You haven't muted anyone." msgstr "你还没有隐藏任何账户。" @@ -3152,20 +3431,20 @@ msgstr "你还没有隐藏任何账户。" msgid "You must be signed in" msgstr "请先登录" -#: src/components/NoteComposer.tsx:901 +#: src/components/NoteComposer.tsx:912 msgid "You must be signed in to create a note" msgstr "你必须登录才能创建帖子" #: src/components/article-composer/ArticleComposerContext.tsx:467 -#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:193 msgid "You must be signed in to delete a draft" msgstr "您必须登录才能删除草稿" -#: src/components/NoteComposer.tsx:851 +#: src/components/NoteComposer.tsx:857 msgid "You must be signed in to edit a note" msgstr "编辑帖子需要登录。" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:357 msgid "You must be signed in to edit an article" msgstr "您必须登录才能编辑文章" @@ -3185,15 +3464,15 @@ msgstr "您被以下用户邀请" msgid "You'll automatically follow each other when you sign up." msgstr "您在注册时将自动互相关注。" -#: src/routes/(root)/[handle]/invite/[id].tsx:276 +#: src/routes/(root)/[handle]/invite/[id].tsx:281 msgid "You've been invited to Hackers' Pub" msgstr "你被邀请加入 Hackers' Pub" -#: src/routes/(root)/[handle]/settings/index.tsx:353 +#: src/routes/(root)/[handle]/settings/index.tsx:354 msgid "Your avatar will be displayed on your profile and in your posts. You can upload a PNG, JPEG, GIF, or WebP image up to 5 MiB in size." msgstr "你的头像将在你的个人资料页面和你的帖文中显示。可以上传大小不超过 5 MiB 的 PNG、JPEG、GIF 或 WebP 图片。" -#: src/routes/(root)/[handle]/settings/index.tsx:492 +#: src/routes/(root)/[handle]/settings/index.tsx:493 msgid "Your bio will be displayed on your profile. You can use Markdown to format it." msgstr "你的个人简介将在你的个人资料页面显示。你可以用 Markdown 文档格式化。" @@ -3209,36 +3488,36 @@ msgstr "您的网络似乎不稳定。请检查网络后重试。" msgid "Your email address will be used to sign in to your account." msgstr "你的电子邮件地址将用于登录。" -#: src/routes/(root)/[handle]/settings/invite.tsx:400 +#: src/routes/(root)/[handle]/settings/invite.tsx:401 msgid "Your friend will see this message in the invitation email." msgstr "你的朋友将在邀请邮件中看到此信息。" -#: src/routes/(root)/[handle]/settings/index.tsx:478 +#: src/routes/(root)/[handle]/settings/index.tsx:479 #: src/routes/(root)/sign/up/[token].tsx:393 msgid "Your name will be displayed on your profile and in your posts." msgstr "你的昵称将在你的个人资料页面和你的帖文中显示。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:267 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:268 msgid "Your passkey has been registered and can now be used for authentication." msgstr "您的通行密钥已注册,现在可以用于身份验证。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:134 +#: src/routes/(root)/[handle]/settings/preferences.tsx:135 msgid "Your preferences have been updated successfully." msgstr "您的设置已成功更新。" -#: src/routes/(root)/[handle]/settings/language.tsx:148 +#: src/routes/(root)/[handle]/settings/language.tsx:149 msgid "Your preferred languages have been updated." msgstr "您的偏好语言已更新。" -#: src/routes/(root)/[handle]/settings/index.tsx:329 +#: src/routes/(root)/[handle]/settings/index.tsx:330 msgid "Your profile settings have been updated successfully." msgstr "个人资料设置已成功更新。" -#: src/components/NoteComposeModal.tsx:128 +#: src/components/NoteComposeModal.tsx:129 msgid "Your unsaved draft will be lost." msgstr "未保存的草稿将会丢失。" -#: src/routes/(root)/[handle]/settings/index.tsx:445 +#: src/routes/(root)/[handle]/settings/index.tsx:446 #: src/routes/(root)/sign/up/[token].tsx:367 msgid "Your username will be used to create your profile URL and your fediverse handle." msgstr "你的用户名将被用来创建你的个人资料 URL 和你的 Fediverse 用户名(handle)。" diff --git a/web-next/src/locales/zh-TW/messages.po b/web-next/src/locales/zh-TW/messages.po index 7533f7b48..13124602d 100644 --- a/web-next/src/locales/zh-TW/messages.po +++ b/web-next/src/locales/zh-TW/messages.po @@ -14,7 +14,7 @@ msgstr "" "Plural-Forms: \n" #. placeholder {0}: article.replies?.edges.length ?? 0 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:991 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:992 msgid "{0, plural, one {# comment} other {# comments}}" msgstr "{0, plural, other {# 則評論}}" @@ -44,11 +44,16 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {關注 # 人}}" #. placeholder {0}: link.invitationsLeft -#: src/routes/(root)/[handle]/invite/[id].tsx:321 -#: src/routes/(root)/[handle]/settings/invite.tsx:742 +#: src/routes/(root)/[handle]/invite/[id].tsx:326 +#: src/routes/(root)/[handle]/settings/invite.tsx:743 msgid "{0, plural, one {# invitation left} other {# invitations left}}" msgstr "{0, plural, other {剩餘 # 次邀請}}" +#. placeholder {0}: status()?.scoredLinkCount ?? 0 +#: src/routes/(root)/admin/news.tsx:350 +msgid "{0, plural, one {# link is currently in the news feed.} other {# links are currently in the news feed.}}" +msgstr "{0, plural, one {目前新聞訂閱流中有 # 個連結。} other {目前新聞訂閱流中有 # 個連結。}}" + #. placeholder {0}: count() #: src/routes/(root)/admin/media.tsx:172 msgid "{0, plural, one {# orphan medium can be deleted.} other {# orphan media can be deleted.}}" @@ -77,7 +82,7 @@ msgid "{0, plural, one {# voter} other {# voters}}" msgstr "{0, plural, other {# 位投票者}}" #. placeholder {0}: edge.node.tags.length - 3 -#: src/routes/(root)/[handle]/drafts/index.tsx:293 +#: src/routes/(root)/[handle]/drafts/index.tsx:295 msgid "{0, plural, one {+1 more} other {+# more}}" msgstr "{0, plural, other {還有#個}}" @@ -87,7 +92,7 @@ msgid "{0, plural, one {Deleted # orphan medium.} other {Deleted # orphan media. msgstr "{0, plural, other {已刪除 # 個孤立媒體。}}" #. placeholder {0}: account.invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:311 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, other {邀請你的朋友加入 Hackers' Pub。你可以邀請最多 # 個人。}}" @@ -96,13 +101,18 @@ msgstr "{0, plural, other {邀請你的朋友加入 Hackers' Pub。你可以邀 msgid "{0, plural, one {Load # more reactor} other {Load # more reactors}}" msgstr "{0, plural, one {載入更多反應者 (#)} other {載入更多反應者 (#)}}" +#. placeholder {0}: result.linksUpdated! +#: src/routes/(root)/admin/news.tsx:190 +msgid "{0, plural, one {Recomputed # link.} other {Recomputed # links.}}" +msgstr "{0, plural, one {已重新計算 # 個連結的評分。} other {已重新計算 # 個連結的評分。}}" + #. placeholder {0}: result.accountsAffected! #: src/routes/(root)/admin/invitations.tsx:97 msgid "{0, plural, one {Regenerated invitations for # account.} other {Regenerated invitations for # accounts.}}" msgstr "{0, plural, other {已為 # 個帳號重新發放邀請。}}" #. placeholder {0}: account.inviteesCount.totalCount -#: src/routes/(root)/[handle]/settings/invite.tsx:438 +#: src/routes/(root)/[handle]/settings/invite.tsx:439 msgid "{0, plural, one {You have invited total # person so far.} other {You have invited total # people so far.}}" msgstr "{0, plural, other {您已經邀請了總共 # 人。}}" @@ -172,8 +182,23 @@ msgstr "{0} 和其他 {1} 人更新了你轉貼過的內容" msgid "{0} followed you" msgstr "{0} 關注了你" +#. placeholder {0}: s.sourceBreakdown.bluesky +#: src/components/NewsStoryHeader.tsx:124 +msgid "{0} from Bluesky" +msgstr "來自 Bluesky 的 {0} 次" + +#. placeholder {0}: s.sourceBreakdown.local +#: src/components/NewsStoryHeader.tsx:118 +msgid "{0} from Hackers' Pub" +msgstr "來自 Hackers' Pub 的 {0} 次" + +#. placeholder {0}: s.sourceBreakdown.remote +#: src/components/NewsStoryHeader.tsx:121 +msgid "{0} from the fediverse" +msgstr "來自聯邦宇宙的 {0} 次" + #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:361 +#: src/routes/(root)/[handle]/settings/invite.tsx:362 msgid "{0} is already a member of Hackers' Pub." msgstr "{0} 已經是 Hackers' Pub 的成員。" @@ -229,47 +254,57 @@ msgstr "{0} 更新了你轉貼過的內容" #. placeholder {0}: post.actor.rawName ?? post.actor.username #. placeholder {1}: post.excerpt #. placeholder {1}: title() -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:237 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:283 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:238 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:284 msgid "{0}: {1}" msgstr "{0}:{1}" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/articles.tsx:86 -#: src/routes/(root)/[handle]/(profile)/articles.tsx:90 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:87 +#: src/routes/(root)/[handle]/(profile)/articles.tsx:91 msgid "{0}'s articles" msgstr "{0}的文章" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/followers.tsx:77 -#: src/routes/(root)/[handle]/(profile)/followers.tsx:80 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:78 +#: src/routes/(root)/[handle]/(profile)/followers.tsx:81 msgid "{0}'s followers" msgstr "{0}的粉絲" #. placeholder {0}: account.name -#: src/routes/(root)/[handle]/(profile)/following.tsx:77 -#: src/routes/(root)/[handle]/(profile)/following.tsx:80 +#: src/routes/(root)/[handle]/(profile)/following.tsx:78 +#: src/routes/(root)/[handle]/(profile)/following.tsx:81 msgid "{0}'s following" msgstr "{0}的關注" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/notes.tsx:86 -#: src/routes/(root)/[handle]/(profile)/notes.tsx:90 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:87 +#: src/routes/(root)/[handle]/(profile)/notes.tsx:91 msgid "{0}'s notes" msgstr "{0}的貼文" #. placeholder {0}: actor.rawName ?? actor.username -#: src/routes/(root)/[handle]/(profile)/shares.tsx:86 -#: src/routes/(root)/[handle]/(profile)/shares.tsx:90 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:87 +#: src/routes/(root)/[handle]/(profile)/shares.tsx:91 msgid "{0}'s shares" msgstr "{0}的轉貼" +#: src/components/NewsStoryCard.tsx:107 +#: src/components/NewsStoryHeader.tsx:54 +msgid "{count, plural, one {# opinion} other {# opinions}}" +msgstr "{count, plural, other {# 則意見}}" + +#: src/components/NewsStoryCard.tsx:51 +#: src/components/NewsStoryHeader.tsx:53 +#~ msgid "{count, plural, one {# share} other {# shares}}" +#~ msgstr "{count, plural, one {# 次轉貼} other {# 次轉貼}}" + #: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:231 #: src/routes/(root)/[handle]/[noteId]/reactions.tsx:253 #~ msgid "+{0} more reactor(s) not shown" #~ msgstr "+{0} 個未顯示" -#: src/routes/(root)/[handle]/settings/index.tsx:579 +#: src/routes/(root)/[handle]/settings/index.tsx:580 msgid "A name for the link that will be displayed on your profile, e.g., GitHub." msgstr "為顯示在你個人資料頁面的連結命名,例如 GitHub。" @@ -278,11 +313,11 @@ msgid "A sign-in link has been sent to your email. Please check your inbox (or s msgstr "登入連結已傳送至您的郵箱。請檢查收件匣。(或垃圾郵件資料夾)" #: src/components/admin/AdminAccountsTable.tsx:224 -#: src/components/AppSidebar.tsx:479 +#: src/components/AppSidebar.tsx:502 msgid "Account" msgstr "帳戶" -#: src/components/AppSidebar.tsx:750 +#: src/components/AppSidebar.tsx:773 #: src/routes/(root)/admin/index.tsx:95 msgid "Accounts" msgstr "帳號" @@ -292,7 +327,8 @@ msgstr "帳號" msgid "Actions" msgstr "操作" -#: src/routes/(root)/[handle]/settings/language.tsx:195 +#: src/routes/(root)/[handle]/settings/language.tsx:196 +#: src/routes/(root)/admin/news.tsx:421 msgid "Add" msgstr "新增" @@ -305,19 +341,23 @@ msgstr "新增{0}" msgid "Add to sidebar" msgstr "新增至側邊欄" -#: src/components/AppSidebar.tsx:728 +#: src/routes/(root)/admin/news.tsx:421 +msgid "Adding…" +msgstr "新增中…" + +#: src/components/AppSidebar.tsx:751 msgid "Admin" msgstr "管理" -#: src/routes/(root)/[handle]/bookmarks.tsx:135 +#: src/routes/(root)/[handle]/bookmarks.tsx:136 msgid "All" msgstr "全部" -#: src/components/NoteComposer.tsx:801 +#: src/components/NoteComposer.tsx:807 msgid "All images must finish uploading before posting" msgstr "發佈前,所有圖片必須完成上傳。" -#: src/components/NoteComposer.tsx:809 +#: src/components/NoteComposer.tsx:815 msgid "All images require alt text" msgstr "所有圖片均需提供替代文字。" @@ -329,17 +369,21 @@ msgstr "所有語言" msgid "All notifications" msgstr "所有通知" +#: src/components/NewsList.tsx:90 +msgid "All-time" +msgstr "全部時間" + #: src/components/article-composer/ArticleComposerPublishFields.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:506 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:507 msgid "Allow automatic translation by AI" msgstr "允許 AI 自動翻譯" #. placeholder {0}: index() + 1 -#: src/components/NoteComposer.tsx:1287 +#: src/components/NoteComposer.tsx:1298 msgid "Alt text for image {0}" msgstr "圖片 {0} 的替代文字" -#: src/components/NoteComposer.tsx:1299 +#: src/components/NoteComposer.tsx:1310 msgid "Alt text for visually impaired people (required)" msgstr "供視力障礙人士使用的替代文字(必填)" @@ -347,31 +391,31 @@ msgstr "供視力障礙人士使用的替代文字(必填)" msgid "An error occurred during signup. Please try again." msgstr "註冊過程中發生錯誤。請重試。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:280 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:281 msgid "An error occurred while registering your passkey." msgstr "註冊通行金鑰時發生錯誤。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:328 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:329 msgid "An error occurred while revoking your passkey." msgstr "撤銷通行金鑰時發生錯誤。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:143 +#: src/routes/(root)/[handle]/settings/preferences.tsx:144 msgid "An error occurred while saving your preferences. Please try again, or contact support if the problem persists." msgstr "儲存設定時發生錯誤。請重試,如果問題持續存在,請聯繫支援。" -#: src/routes/(root)/[handle]/settings/language.tsx:162 +#: src/routes/(root)/[handle]/settings/language.tsx:163 msgid "An error occurred while saving your preferred languages. Please try again, or contact support if the problem persists." msgstr "儲存語言偏好時出現錯誤。請重試,如果問題仍然存在,請聯繫支援。" -#: src/routes/(root)/[handle]/settings/index.tsx:296 -#: src/routes/(root)/[handle]/settings/index.tsx:337 +#: src/routes/(root)/[handle]/settings/index.tsx:297 +#: src/routes/(root)/[handle]/settings/index.tsx:338 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "儲存設定時發生錯誤。請重試,如果問題仍然存在,請聯繫支援。" -#: src/routes/(root)/[handle]/invite/[id].tsx:198 -#: src/routes/(root)/[handle]/settings/invite.tsx:270 -#: src/routes/(root)/[handle]/settings/invite.tsx:551 -#: src/routes/(root)/[handle]/settings/invite.tsx:601 +#: src/routes/(root)/[handle]/invite/[id].tsx:203 +#: src/routes/(root)/[handle]/settings/invite.tsx:271 +#: src/routes/(root)/[handle]/settings/invite.tsx:552 +#: src/routes/(root)/[handle]/settings/invite.tsx:602 msgid "An unexpected error occurred. Please try again later." msgstr "發生了意外錯誤。請稍後再試。" @@ -386,7 +430,7 @@ msgstr "任何人都可以引用" msgid "Are you sure you want to block {0} ({1})? They won't be able to follow you or see your posts." msgstr "確定要封鎖 {0}({1})嗎?該使用者將無法關注你或查看你的內容。" -#: src/routes/(root)/[handle]/drafts/index.tsx:156 +#: src/routes/(root)/[handle]/drafts/index.tsx:157 msgid "Are you sure you want to delete \"{draftTitle}\"? This action cannot be undone." msgstr "您確定要刪除「{draftTitle}」嗎?此操作無法復原。" @@ -395,7 +439,7 @@ msgid "Are you sure you want to delete this draft? This action cannot be undone. msgstr "您確定要刪除此草稿嗎?此操作無法復原。" #. placeholder {0}: passkeyToRevoke()?.name -#: src/routes/(root)/[handle]/settings/passkeys.tsx:517 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:518 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." msgstr "您確定要取消密碼{0}嗎?您將無法再使用它登入您的帳戶。" @@ -405,8 +449,8 @@ msgstr "您確定要取消密碼{0}嗎?您將無法再使用它登入您的帳 msgid "Are you sure you want to unblock {0} ({1})? They will be able to follow you and see your posts." msgstr "確定要解除封鎖 {0}({1})嗎?該使用者將能夠關注你並查看你的內容。" -#: src/routes/(root)/[handle]/drafts/index.tsx:243 #: src/routes/(root)/[handle]/drafts/index.tsx:245 +#: src/routes/(root)/[handle]/drafts/index.tsx:247 msgid "Article drafts" msgstr "文章草稿" @@ -414,7 +458,7 @@ msgstr "文章草稿" msgid "Article published" msgstr "文章已發布" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:333 msgid "Article updated" msgstr "文章已更新" @@ -423,21 +467,21 @@ msgid "article-url-slug" msgstr "文章網址別名" #: src/components/ProfileTabs.tsx:51 -#: src/routes/(root)/[handle]/bookmarks.tsx:136 +#: src/routes/(root)/[handle]/bookmarks.tsx:137 msgid "Articles" msgstr "文章" -#: src/components/AppSidebar.tsx:308 +#: src/components/AppSidebar.tsx:331 msgid "Articles only" msgstr "僅文章" #. placeholder {0}: "CHANGED" -#: src/routes/(root)/[handle]/settings/index.tsx:454 +#: src/routes/(root)/[handle]/settings/index.tsx:455 msgid "As you have already changed it {0}, you can't change it again." msgstr "自從你已經把使用者名稱換成 {0} 了,你再也改不了了。" -#: src/components/NoteComposer.tsx:1173 -#: src/components/NoteComposer.tsx:1174 +#: src/components/NoteComposer.tsx:1184 +#: src/components/NoteComposer.tsx:1185 msgid "Attach image" msgstr "附加圖片" @@ -445,20 +489,20 @@ msgstr "附加圖片" msgid "Authenticating…" msgstr "驗證中…" -#: src/components/NoteComposer.tsx:1318 +#: src/components/NoteComposer.tsx:1329 msgid "Auto-fill" msgstr "自動填寫" -#: src/components/NoteComposer.tsx:1311 -#: src/components/NoteComposer.tsx:1312 +#: src/components/NoteComposer.tsx:1322 +#: src/components/NoteComposer.tsx:1323 msgid "Auto-fill alt text" msgstr "自動填寫替代文字" -#: src/routes/(root)/[handle]/settings/index.tsx:351 +#: src/routes/(root)/[handle]/settings/index.tsx:352 msgid "Avatar" msgstr "頭像" -#: src/routes/(root)/[handle]/settings/index.tsx:483 +#: src/routes/(root)/[handle]/settings/index.tsx:484 #: src/routes/(root)/sign/up/[token].tsx:403 msgid "Bio" msgstr "個人簡介" @@ -477,11 +521,11 @@ msgstr "封鎖" msgid "Block user?" msgstr "封鎖使用者?" -#: src/routes/(root)/[handle]/settings/blocks.tsx:98 +#: src/routes/(root)/[handle]/settings/blocks.tsx:99 msgid "Blocked accounts" msgstr "已封鎖的帳號" -#: src/routes/(root)/[handle]/settings/blocks.tsx:100 +#: src/routes/(root)/[handle]/settings/blocks.tsx:101 msgid "Blocked accounts cannot follow you or see your posts. Unlike muting, blocking is federated to the blocked account's instance." msgstr "被封鎖的帳號無法關注你或查看你的內容。與靜音不同,封鎖會聯邦到被封鎖帳號所在的站台。" @@ -494,8 +538,8 @@ msgstr "粗體" msgid "Bookmark" msgstr "收藏" -#: src/components/AppSidebar.tsx:618 -#: src/routes/(root)/[handle]/bookmarks.tsx:125 +#: src/components/AppSidebar.tsx:641 +#: src/routes/(root)/[handle]/bookmarks.tsx:126 msgid "Bookmarks" msgstr "收藏" @@ -524,24 +568,33 @@ msgstr "已停用瀏覽器通知" msgid "Browser notifications enabled" msgstr "已啟用瀏覽器通知" +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:492 +msgid "Buried" +msgstr "已沉底" + +#: src/components/NewsStoryCard.tsx:258 +msgid "Bury" +msgstr "沉底" + #: src/components/article-composer/ArticleComposerActions.tsx:48 -#: src/components/NoteComposer.tsx:1346 -#: src/components/NoteComposer.tsx:1347 -#: src/components/NoteComposer.tsx:1390 +#: src/components/NoteComposer.tsx:1357 +#: src/components/NoteComposer.tsx:1358 +#: src/components/NoteComposer.tsx:1401 #: src/components/PostActionMenu.tsx:367 #: src/components/ProfileActionMenu.tsx:401 #: src/components/ProfileActionMenu.tsx:402 #: src/components/QuotedNoteCard.tsx:248 #: src/components/RemoteFollowButton.tsx:218 #: src/components/RemoteFollowButton.tsx:280 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:522 -#: src/routes/(root)/[handle]/settings/index.tsx:398 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:521 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:399 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:522 #: src/routes/(root)/authorize_interaction.tsx:273 msgid "Cancel" msgstr "取消" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:347 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 msgid "Cannot change the language because translations already exist" msgstr "由於已存在翻譯,無法變更語言" @@ -549,11 +602,11 @@ msgstr "由於已存在翻譯,無法變更語言" msgid "Check again" msgstr "重新檢查" -#: src/routes/(root)/[handle]/invite/[id].tsx:244 +#: src/routes/(root)/[handle]/invite/[id].tsx:249 msgid "Check your email" msgstr "請檢查你的信箱" -#: src/routes/(root)/[handle]/invite/[id].tsx:246 +#: src/routes/(root)/[handle]/invite/[id].tsx:251 msgid "Check your email to complete sign-up. We've sent a verification link to your email address." msgstr "請檢查你的信箱以完成註冊。我們已向你的信箱地址發送了驗證連結。" @@ -561,7 +614,7 @@ msgstr "請檢查你的信箱以完成註冊。我們已向你的信箱地址發 msgid "Checking…" msgstr "檢查中…" -#: src/routes/(root)/[handle]/settings/invite.tsx:386 +#: src/routes/(root)/[handle]/settings/invite.tsx:387 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "選擇你的朋友使用的語言。這個語言只會用於邀請。" @@ -569,20 +622,25 @@ msgstr "選擇你的朋友使用的語言。這個語言只會用於邀請。" msgid "Choose whether push notifications may include post excerpts. Generic notification text is used when previews are hidden." msgstr "選擇推播通知是否可以包含內容摘要。隱藏預覽時會使用一般通知文字。" -#: src/routes/(root)/[handle]/invite/[id].tsx:395 +#: src/routes/(root)/[handle]/invite/[id].tsx:400 msgid "Choose your preferred language for the verification email." msgstr "請選擇驗證信件的語言。" #: src/components/admin/AdminAccountsTable.tsx:213 +#: src/routes/(root)/admin/news.tsx:501 msgid "Clear" msgstr "清除" +#: src/components/NewsStoryCard.tsx:264 +msgid "Clear penalty" +msgstr "清除處罰" + #: src/components/WebPushNotificationSettings.tsx:395 msgid "Clicking a notification opens your notifications page." msgstr "點擊通知會開啟你的通知頁面。" #: src/components/ImageLightbox.tsx:74 -#: src/routes/(root)/[handle]/settings/invite.tsx:663 +#: src/routes/(root)/[handle]/settings/invite.tsx:664 msgid "Close" msgstr "關閉" @@ -594,7 +652,7 @@ msgstr "已結束" msgid "Code" msgstr "程式碼" -#: src/components/AppSidebar.tsx:977 +#: src/components/AppSidebar.tsx:1023 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/sign/up/[token].tsx:458 msgid "Code of conduct" @@ -604,18 +662,18 @@ msgstr "行為準則" #~ msgid "Comments ({0})" #~ msgstr "評論({0})" -#: src/components/AppSidebar.tsx:819 +#: src/components/AppSidebar.tsx:865 #: src/components/FloatingComposeButton.tsx:61 msgid "Compose" msgstr "寫作" #: src/components/article-composer/ArticleComposerForm.tsx:65 -#: src/components/NoteComposer.tsx:1122 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:392 +#: src/components/NoteComposer.tsx:1133 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:393 msgid "Content" msgstr "內容" -#: src/components/NoteComposer.tsx:791 +#: src/components/NoteComposer.tsx:797 msgid "Content cannot be empty" msgstr "內容不能為空" @@ -627,15 +685,15 @@ msgstr "在瀏覽器中繼續" msgid "Controls who can quote this article on their timeline." msgstr "設定誰可以在時間軸上引用這篇文章。" -#: src/routes/(root)/[handle]/settings/invite.tsx:611 +#: src/routes/(root)/[handle]/settings/invite.tsx:612 msgid "Copied" msgstr "已複製" -#: src/routes/(root)/[handle]/settings/invite.tsx:705 +#: src/routes/(root)/[handle]/settings/invite.tsx:706 msgid "Copy" msgstr "複製" -#: src/routes/(root)/[handle]/settings/invite.tsx:618 +#: src/routes/(root)/[handle]/settings/invite.tsx:619 msgid "Could not copy the link to the clipboard." msgstr "無法將連結複製到剪貼簿。" @@ -655,23 +713,23 @@ msgstr "無法撤銷引用" msgid "Could not vote on this poll" msgstr "無法提交此投票" -#: src/components/AppSidebar.tsx:865 +#: src/components/AppSidebar.tsx:911 #: src/components/FloatingComposeButton.tsx:123 msgid "Create article" msgstr "建立文章" -#: src/routes/(root)/[handle]/settings/invite.tsx:834 +#: src/routes/(root)/[handle]/settings/invite.tsx:835 msgid "Create invitation link" msgstr "建立邀請連結" -#: src/components/AppSidebar.tsx:841 +#: src/components/AppSidebar.tsx:887 #: src/components/FloatingComposeButton.tsx:99 -#: src/components/NoteComposeModal.tsx:72 -#: src/components/NoteComposer.tsx:1407 +#: src/components/NoteComposeModal.tsx:73 +#: src/components/NoteComposer.tsx:1418 msgid "Create note" msgstr "建立貼文" -#: src/routes/(root)/[handle]/settings/invite.tsx:628 +#: src/routes/(root)/[handle]/settings/invite.tsx:629 msgid "Create shareable invitation links. Each link can be used multiple times until the invitation count runs out or the link expires." msgstr "建立可共享的邀請連結。每個連結可多次使用,直到邀請次數用完或連結過期。" @@ -680,7 +738,7 @@ msgid "Created" msgstr "建立時間" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:426 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:427 msgid "Created {0}" msgstr "{0}建立" @@ -688,16 +746,16 @@ msgstr "{0}建立" msgid "Creating account…" msgstr "創建帳戶…" -#: src/components/NoteComposer.tsx:1408 -#: src/routes/(root)/[handle]/settings/invite.tsx:833 +#: src/components/NoteComposer.tsx:1419 +#: src/routes/(root)/[handle]/settings/invite.tsx:834 msgid "Creating…" msgstr "正在建立…" -#: src/routes/(root)/[handle]/settings/index.tsx:405 +#: src/routes/(root)/[handle]/settings/index.tsx:406 msgid "Crop" msgstr "裁剪" -#: src/routes/(root)/[handle]/settings/index.tsx:378 +#: src/routes/(root)/[handle]/settings/index.tsx:379 msgid "Crop your new avatar" msgstr "裁剪你的新頭像" @@ -711,22 +769,22 @@ msgstr "截止時間:" msgid "CW" msgstr "CW" -#: src/routes/(root)/[handle]/settings/preferences.tsx:192 +#: src/routes/(root)/[handle]/settings/preferences.tsx:193 msgid "Default note privacy" msgstr "預設貼文隱私設定" -#: src/routes/(root)/[handle]/settings/preferences.tsx:217 +#: src/routes/(root)/[handle]/settings/preferences.tsx:218 msgid "Default quote permission" msgstr "預設引用權限" -#: src/routes/(root)/[handle]/settings/preferences.tsx:204 +#: src/routes/(root)/[handle]/settings/preferences.tsx:205 msgid "Default share privacy" msgstr "預設轉貼隱私設定" #: src/components/PostActionMenu.tsx:353 #: src/components/PostActionMenu.tsx:373 -#: src/routes/(root)/[handle]/drafts/index.tsx:320 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/drafts/index.tsx:322 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 msgid "Delete" msgstr "刪除" @@ -744,11 +802,20 @@ msgid "Delete post?" msgstr "刪除內容?" #: src/components/article-composer/ArticleComposerActions.tsx:21 -#: src/routes/(root)/[handle]/settings/invite.tsx:723 +#: src/routes/(root)/[handle]/settings/invite.tsx:724 #: src/routes/(root)/admin/media.tsx:187 msgid "Deleting…" msgstr "正在刪除…" +#: src/components/NewsStoryCard.tsx:252 +msgid "Demote" +msgstr "降級" + +#: src/components/NewsStoryCard.tsx:215 +#: src/routes/(root)/admin/news.tsx:493 +msgid "Demoted" +msgstr "已降級" + #: src/components/WebPushNotificationSettings.tsx:431 msgid "Disable" msgstr "停用" @@ -757,11 +824,11 @@ msgstr "停用" msgid "Disabling…" msgstr "停用中…" -#: src/components/NoteComposeModal.tsx:137 +#: src/components/NoteComposeModal.tsx:138 msgid "Discard" msgstr "放棄" -#: src/components/NoteComposeModal.tsx:126 +#: src/components/NoteComposeModal.tsx:127 msgid "Discard draft?" msgstr "放棄草稿?" @@ -769,11 +836,15 @@ msgstr "放棄草稿?" msgid "Discard unsaved changes - are you sure?" msgstr "放棄未儲存的變更 - 您確定嗎?" +#: src/components/NewsStoryCard.tsx:145 +#~ msgid "Discussion" +#~ msgstr "討論" + #: src/components/WebPushPromptBanner.tsx:269 msgid "Dismiss" msgstr "關閉" -#: src/routes/(root)/[handle]/settings/index.tsx:467 +#: src/routes/(root)/[handle]/settings/index.tsx:468 #: src/routes/(root)/sign/up/[token].tsx:377 msgid "Display name" msgstr "暱稱" @@ -782,12 +853,12 @@ msgstr "暱稱" msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "需要建立帳戶嗎?Hackers' Pub 僅限邀請,請聯繫朋友邀請您。" -#: src/components/NoteComposer.tsx:742 +#: src/components/NoteComposer.tsx:748 msgid "Do you want to quote this link?" msgstr "要引用此連結嗎?" #: src/components/article-composer/ArticleComposerContext.tsx:450 -#: src/routes/(root)/[handle]/drafts/index.tsx:175 +#: src/routes/(root)/[handle]/drafts/index.tsx:176 msgid "Draft deleted" msgstr "草稿已刪除" @@ -803,7 +874,7 @@ msgstr "未找到草稿" msgid "Draft saved" msgstr "草稿已儲存" -#: src/routes/(root)/[handle]/settings/index.tsx:381 +#: src/routes/(root)/[handle]/settings/index.tsx:382 msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "拖動選擇要保留的區域,然後點擊「裁剪」來更新你的頭像。" @@ -812,25 +883,25 @@ msgid "e.g., @user@mastodon.social" msgstr "例如:@user@mastodon.social" #: src/components/PostActionMenu.tsx:328 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:695 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:696 msgid "Edit" msgstr "編輯" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:374 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:375 msgid "Edit article" msgstr "編輯文章" -#: src/routes/(root)/[handle]/drafts/[id].tsx:73 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/[id].tsx:75 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "Edit draft" msgstr "編輯草稿" -#: src/components/NoteComposeModal.tsx:69 +#: src/components/NoteComposeModal.tsx:70 msgid "Edit note" msgstr "編輯貼文" -#: src/routes/(root)/[handle]/invite/[id].tsx:366 -#: src/routes/(root)/[handle]/settings/invite.tsx:334 +#: src/routes/(root)/[handle]/invite/[id].tsx:371 +#: src/routes/(root)/[handle]/settings/invite.tsx:335 #: src/routes/(root)/sign/up/[token].tsx:311 msgid "Email address" msgstr "電子郵件地址" @@ -857,7 +928,7 @@ msgstr "已結束" msgid "Ends" msgstr "截止" -#: src/routes/(root)/[handle]/invite/[id].tsx:279 +#: src/routes/(root)/[handle]/invite/[id].tsx:284 msgid "Enter your email address below to get started." msgstr "請在下方輸入你的電子郵件地址以開始。" @@ -879,47 +950,63 @@ msgstr "請在下方輸入您的電子郵件或使用者名稱以登入。" #: src/components/article-composer/ArticleComposerContext.tsx:466 #: src/components/article-composer/ArticleComposerContext.tsx:474 #: src/components/article-composer/ArticleComposerForm.tsx:35 -#: src/components/NoteComposer.tsx:601 -#: src/components/NoteComposer.tsx:648 -#: src/components/NoteComposer.tsx:790 -#: src/components/NoteComposer.tsx:800 -#: src/components/NoteComposer.tsx:808 -#: src/components/NoteComposer.tsx:842 -#: src/components/NoteComposer.tsx:850 -#: src/components/NoteComposer.tsx:858 -#: src/components/NoteComposer.tsx:892 -#: src/components/NoteComposer.tsx:900 -#: src/components/NoteComposer.tsx:908 -#: src/components/NoteComposer.tsx:965 +#: src/components/NoteComposer.tsx:607 +#: src/components/NoteComposer.tsx:654 +#: src/components/NoteComposer.tsx:796 +#: src/components/NoteComposer.tsx:806 +#: src/components/NoteComposer.tsx:814 +#: src/components/NoteComposer.tsx:848 +#: src/components/NoteComposer.tsx:856 +#: src/components/NoteComposer.tsx:864 +#: src/components/NoteComposer.tsx:903 +#: src/components/NoteComposer.tsx:911 +#: src/components/NoteComposer.tsx:919 +#: src/components/NoteComposer.tsx:976 #: src/components/QuotedNoteCard.tsx:270 #: src/components/QuotedNoteCard.tsx:278 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:257 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:345 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:355 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:364 -#: src/routes/(root)/[handle]/drafts/index.tsx:182 -#: src/routes/(root)/[handle]/drafts/index.tsx:191 -#: src/routes/(root)/[handle]/drafts/index.tsx:199 -#: src/routes/(root)/[handle]/invite/[id].tsx:196 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:258 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:346 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/drafts/index.tsx:183 +#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:200 +#: src/routes/(root)/[handle]/invite/[id].tsx:201 #: src/routes/(root)/sign/up/[token].tsx:269 msgid "Error" msgstr "錯誤" +#: src/routes/(root)/admin/news.tsx:382 +msgid "Excluded URL patterns" +msgstr "排除的 URL 模式" + +#: src/routes/(root)/admin/news.tsx:233 +msgid "Exclusion pattern added." +msgstr "已新增排除模式。" + +#: src/routes/(root)/admin/news.tsx:268 +msgid "Exclusion pattern removed." +msgstr "已移除排除模式。" + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/invite/[id].tsx:334 -#: src/routes/(root)/[handle]/settings/invite.tsx:759 +#: src/routes/(root)/[handle]/invite/[id].tsx:339 +#: src/routes/(root)/[handle]/settings/invite.tsx:760 msgid "Expires {0}" msgstr "{0}到期" -#: src/routes/(root)/[handle]/settings/invite.tsx:806 +#: src/routes/(root)/[handle]/settings/invite.tsx:807 msgid "Expiry" msgstr "到期時間" -#: src/routes/(root)/[handle]/settings/invite.tsx:391 -#: src/routes/(root)/[handle]/settings/invite.tsx:796 +#: src/routes/(root)/[handle]/settings/invite.tsx:392 +#: src/routes/(root)/[handle]/settings/invite.tsx:797 msgid "Extra message" msgstr "額外訊息" +#: src/routes/(root)/admin/news.tsx:252 +msgid "Failed to add exclusion pattern." +msgstr "新增排除模式失敗。" + #: src/components/HashtagActionBar.tsx:192 #: src/components/HashtagActionBar.tsx:199 msgid "Failed to add to sidebar" @@ -935,17 +1022,21 @@ msgstr "無法封鎖此使用者" msgid "Failed to bookmark" msgstr "收藏失敗" -#: src/routes/(root)/[handle]/settings/invite.tsx:617 +#: src/routes/(root)/admin/news.tsx:310 +msgid "Failed to clear penalty." +msgstr "清除處罰失敗。" + +#: src/routes/(root)/[handle]/settings/invite.tsx:618 msgid "Failed to copy" msgstr "複製失敗" -#: src/routes/(root)/[handle]/settings/invite.tsx:539 -#: src/routes/(root)/[handle]/settings/invite.tsx:549 +#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:550 msgid "Failed to create invitation link" msgstr "建立邀請連結失敗" -#: src/routes/(root)/[handle]/settings/invite.tsx:588 -#: src/routes/(root)/[handle]/settings/invite.tsx:599 +#: src/routes/(root)/[handle]/settings/invite.tsx:589 +#: src/routes/(root)/[handle]/settings/invite.tsx:600 msgid "Failed to delete invitation link" msgstr "刪除邀請連結失敗" @@ -979,7 +1070,7 @@ msgstr "無法啟用瀏覽器通知" msgid "Failed to follow" msgstr "關注失敗" -#: src/components/NoteComposer.tsx:966 +#: src/components/NoteComposer.tsx:977 msgid "Failed to generate alt text" msgstr "替代文字生成失敗" @@ -995,7 +1086,7 @@ msgstr "載入更多文章失敗,點擊重試" msgid "Failed to load more bookmarks; click to retry" msgstr "載入更多收藏失敗,點擊重試" -#: src/routes/(root)/[handle]/drafts/index.tsx:345 +#: src/routes/(root)/[handle]/drafts/index.tsx:347 msgid "Failed to load more drafts; click to retry" msgstr "載入更多草稿失敗,點擊重試" @@ -1007,7 +1098,7 @@ msgstr "載入更多粉絲失敗,點擊重試" msgid "Failed to load more following; click to retry" msgstr "載入更多關注失敗,點擊重試" -#: src/routes/(root)/[handle]/settings/invite.tsx:947 +#: src/routes/(root)/[handle]/settings/invite.tsx:948 msgid "Failed to load more invitees; click to retry" msgstr "載入更多受邀者失敗;點擊重試" @@ -1019,7 +1110,7 @@ msgstr "載入更多貼文失敗,點擊重試" msgid "Failed to load more notifications; click to retry" msgstr "無法載入更多通知;點擊重試" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:495 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 msgid "Failed to load more passkeys; click to retry" msgstr "載入更多通行金鑰失敗;點擊重試" @@ -1031,8 +1122,8 @@ msgstr "載入更多通行金鑰失敗;點擊重試" msgid "Failed to load more posts; click to retry" msgstr "載入更多內容失敗,點擊重試" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:194 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:201 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:195 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:202 msgid "Failed to load more quotes; click to retry" msgstr "載入更多引用失敗,點擊重試" @@ -1040,28 +1131,31 @@ msgstr "載入更多引用失敗,點擊重試" msgid "Failed to load more reactors; click to retry" msgstr "載入更多反應者失敗,點擊重試" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:670 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:671 msgid "Failed to load more replies; click to retry" msgstr "載入更多回覆失敗,點擊重試" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:204 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:213 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:205 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:214 msgid "Failed to load more shares; click to retry" msgstr "載入更多轉貼失敗,點擊重試" -#: src/components/BlockedAccountsList.tsx:199 -#: src/components/MutedAccountsList.tsx:196 +#: src/components/AccountListBase.tsx:112 msgid "Failed to load more; click to retry" msgstr "載入更多失敗;點擊重試" -#: src/components/NoteComposer.tsx:1014 +#: src/components/NoteComposer.tsx:1025 msgid "Failed to load post" msgstr "無法載入內容" -#: src/components/NoteComposer.tsx:1065 +#: src/components/NoteComposer.tsx:1076 msgid "Failed to load quoted post" msgstr "無法載入引用內容" +#: src/components/NewsDiscussionThread.tsx:408 +msgid "Failed to load replies; click to retry" +msgstr "載入回覆失敗,點擊重試" + #: src/components/RemoteFollowButton.tsx:126 msgid "Failed to look up user." msgstr "查找使用者失敗。" @@ -1087,11 +1181,15 @@ msgstr "無法釘選內容" msgid "Failed to react" msgstr "反應失敗" +#: src/routes/(root)/admin/news.tsx:212 +msgid "Failed to recompute news scores." +msgstr "無法重新計算新聞評分。" + #: src/routes/(root)/admin/invitations.tsx:125 msgid "Failed to regenerate invitations." msgstr "重新發放邀請失敗。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:277 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:278 msgid "Failed to register passkey" msgstr "註冊通行金鑰失敗" @@ -1100,40 +1198,44 @@ msgstr "註冊通行金鑰失敗" msgid "Failed to remove bookmark" msgstr "取消收藏失敗" +#: src/routes/(root)/admin/news.tsx:281 +msgid "Failed to remove exclusion pattern." +msgstr "移除排除模式失敗。" + #: src/components/HashtagActionBar.tsx:212 #: src/components/HashtagActionBar.tsx:219 msgid "Failed to remove from sidebar" msgstr "從側邊欄移除失敗" #: src/components/MarkdownEditor.tsx:192 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:461 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 msgid "Failed to render preview" msgstr "預覽渲染失敗" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:319 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:325 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:320 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "Failed to revoke passkey" msgstr "撤銷通行金鑰失敗" -#: src/routes/(root)/[handle]/settings/language.tsx:160 +#: src/routes/(root)/[handle]/settings/language.tsx:161 msgid "Failed to save language preferences" msgstr "儲存語言偏好失敗" -#: src/routes/(root)/[handle]/settings/preferences.tsx:141 +#: src/routes/(root)/[handle]/settings/preferences.tsx:142 msgid "Failed to save preferences" msgstr "儲存設定失敗" -#: src/routes/(root)/[handle]/settings/index.tsx:293 -#: src/routes/(root)/[handle]/settings/index.tsx:335 +#: src/routes/(root)/[handle]/settings/index.tsx:294 +#: src/routes/(root)/[handle]/settings/index.tsx:336 msgid "Failed to save settings" msgstr "儲存設定失敗" -#: src/routes/(root)/[handle]/invite/[id].tsx:185 +#: src/routes/(root)/[handle]/invite/[id].tsx:190 msgid "Failed to send email" msgstr "傳送郵件失敗" -#: src/routes/(root)/[handle]/settings/invite.tsx:248 -#: src/routes/(root)/[handle]/settings/invite.tsx:268 +#: src/routes/(root)/[handle]/settings/invite.tsx:249 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "Failed to send invitation" msgstr "發送邀請失敗" @@ -1147,8 +1249,8 @@ msgstr "轉貼失敗" msgid "Failed to sign out: {0}" msgstr "登出失敗:{0}" -#: src/components/BlockedAccountsList.tsx:98 -#: src/components/BlockedAccountsList.tsx:104 +#: src/components/BlockedAccountsList.tsx:96 +#: src/components/BlockedAccountsList.tsx:102 #: src/components/ProfileActionMenu.tsx:258 #: src/components/ProfileActionMenu.tsx:264 msgid "Failed to unblock this user" @@ -1160,8 +1262,8 @@ msgstr "無法解除封鎖此使用者" msgid "Failed to unfollow" msgstr "取消關注失敗" -#: src/components/MutedAccountsList.tsx:97 -#: src/components/MutedAccountsList.tsx:101 +#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:99 #: src/components/ProfileActionMenu.tsx:300 #: src/components/ProfileActionMenu.tsx:308 msgid "Failed to unmute this user" @@ -1177,11 +1279,16 @@ msgstr "無法取消釘選內容" msgid "Failed to unshare post" msgstr "取消轉貼失敗" +#: src/components/NewsStoryCard.tsx:87 +#: src/components/NewsStoryCard.tsx:91 +msgid "Failed to update penalty." +msgstr "更新處罰失敗。" + #: src/components/WebPushNotificationSettings.tsx:354 msgid "Failed to update push notification privacy" msgstr "無法更新推播通知隱私設定" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:365 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:366 msgid "Failed to update the article. Please try again." msgstr "文章更新失敗,請重試。" @@ -1190,8 +1297,8 @@ msgstr "文章更新失敗,請重試。" #~ msgstr "貼文更新失敗,請重試。" #: src/components/article-composer/ArticleComposerForm.tsx:38 -#: src/components/NoteComposer.tsx:651 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:260 +#: src/components/NoteComposer.tsx:657 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:261 msgid "Failed to upload image" msgstr "圖片上傳失敗" @@ -1199,7 +1306,7 @@ msgstr "圖片上傳失敗" msgid "Failed to vote" msgstr "投票失敗" -#: src/components/AppSidebar.tsx:354 +#: src/components/AppSidebar.tsx:377 msgid "Fediverse" msgstr "聯邦宇宙" @@ -1207,10 +1314,14 @@ msgstr "聯邦宇宙" msgid "Fediverse handle" msgstr "聯邦宇宙使用者名稱" -#: src/components/AppSidebar.tsx:245 +#: src/components/AppSidebar.tsx:268 msgid "Feed" msgstr "訂閱流" +#: src/components/NewsStoryHeader.tsx:113 +msgid "First shared" +msgstr "首次轉貼" + #: src/components/FollowButton.tsx:195 #: src/components/HashtagActionBar.tsx:237 msgid "Follow" @@ -1253,7 +1364,7 @@ msgstr "關注了你" msgid "Formatting" msgstr "格式" -#: src/components/NoteComposer.tsx:1337 +#: src/components/NoteComposer.tsx:1348 msgid "Generating…" msgstr "生成中…" @@ -1261,14 +1372,14 @@ msgstr "生成中…" msgid "Get browser notifications" msgstr "接收瀏覽器通知" -#: src/components/AppSidebar.tsx:1015 +#: src/components/AppSidebar.tsx:1061 msgid "GitHub repository" msgstr "GitHub 儲存庫" -#: src/routes/(root)/[handle]/bookmarks.tsx:108 -#: src/routes/(root)/[handle]/drafts/[id].tsx:59 -#: src/routes/(root)/[handle]/drafts/index.tsx:225 -#: src/routes/(root)/[handle]/drafts/new.tsx:69 +#: src/routes/(root)/[handle]/bookmarks.tsx:109 +#: src/routes/(root)/[handle]/drafts/[id].tsx:61 +#: src/routes/(root)/[handle]/drafts/index.tsx:227 +#: src/routes/(root)/[handle]/drafts/new.tsx:71 msgid "Go back" msgstr "返回" @@ -1280,13 +1391,13 @@ msgstr "返回首頁" msgid "Go to Drafts" msgstr "前往草稿" -#: src/routes/(root)/[handle]/bookmarks.tsx:114 +#: src/routes/(root)/[handle]/bookmarks.tsx:115 msgid "Go to my bookmarks" msgstr "前往我的收藏" -#: src/routes/(root)/[handle]/drafts/[id].tsx:64 -#: src/routes/(root)/[handle]/drafts/index.tsx:230 -#: src/routes/(root)/[handle]/drafts/new.tsx:74 +#: src/routes/(root)/[handle]/drafts/[id].tsx:66 +#: src/routes/(root)/[handle]/drafts/index.tsx:232 +#: src/routes/(root)/[handle]/drafts/new.tsx:76 msgid "Go to my drafts" msgstr "前往我的草稿" @@ -1294,8 +1405,8 @@ msgstr "前往我的草稿" msgid "Grants one extra invitation to the most active accounts (the top third by post count) since the last regeneration cutoff." msgstr "向自上次重新發放截止時間以來最活躍的帳號(按內容數排名前三分之一)額外發放一份邀請。" -#: src/components/AppSidebar.tsx:323 -#: src/components/AppSidebar.tsx:446 +#: src/components/AppSidebar.tsx:346 +#: src/components/AppSidebar.tsx:469 #: src/routes/(root).tsx:134 #: src/routes/(root)/coc.tsx:40 #: src/routes/(root)/markdown.tsx:40 @@ -1324,6 +1435,19 @@ msgstr "Hackers' Pub:管理 · 邀請" msgid "Hackers' Pub: Admin · Media" msgstr "Hackers' Pub:管理 · 媒體" +#: src/routes/(root)/admin/news.tsx:320 +msgid "Hackers' Pub: Admin · News" +msgstr "Hackers' Pub:管理 · 新聞" + +#: src/routes/(root)/admin/news.tsx:124 +#~ msgid "Hackers' Pub: Admin · News scores" +#~ msgstr "Hackers' Pub:管理 · 新聞評分" + +#: src/routes/(root)/news/[link_id]/index.tsx:56 +#: src/routes/(root)/news/index.tsx:37 +msgid "Hackers' Pub: News" +msgstr "Hackers' Pub:新聞" + #: src/routes/(root)/notifications.tsx:47 msgid "Hackers' Pub: Notifications" msgstr "Hackers' Pub:通知" @@ -1346,6 +1470,10 @@ msgstr "標題 3" msgid "Hide" msgstr "隱藏" +#: src/routes/(root)/admin/news.tsx:384 +msgid "Hide links matching a URL pattern from the news feed list (every sort order). Their discussion pages stay reachable by direct link. Patterns use the URLPattern syntax, e.g. https://example.com/* or https://*.example.com/*." +msgstr "將符合某個 URL 模式的連結從新聞資訊流列表(所有排序)中隱藏。這些連結的討論頁仍可透過直接 URL 存取。模式使用 URLPattern 語法,例如 https://example.com/* 或 https://*.example.com/*。" + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Hide preview" #~ msgstr "隱藏預覽" @@ -1358,23 +1486,23 @@ msgstr "隱藏" msgid "I have read and agree to the Code of conduct." msgstr "我同意 Hackers' Pub 的行為準則。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:186 +#: src/routes/(root)/[handle]/settings/preferences.tsx:187 msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "啟用後,AI 將為您生成文章摘要。否則,將使用文章的前幾行作為摘要。" #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1013 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1014 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:548 msgid "If you have a fediverse account, you can reply to this article from your own instance. Search {0} on your instance and reply to it." msgstr "如果你在聯邦宇宙有個帳戶,你可以在你自己的站台裡評論此文章。在你的站台搜尋 {0} 後回覆。" #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:393 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:394 msgid "If you have a fediverse account, you can reply to this note from your own instance. Search {0} on your instance and reply to it." msgstr "如果你在聯邦宇宙有個帳戶,你可以在你自己的實例裡評論此貼文。在你的實例搜尋 {0} 後回覆。" #. placeholder {0}: "ACTIVITYPUB_URI" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:471 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:472 msgid "If you have a fediverse account, you can reply to this post from your own instance. Search {0} on your instance and reply to it." msgstr "如果你有聯邦宇宙帳號,可以從自己的站台回覆此內容。在你的站台中搜尋 {0} 並回覆。" @@ -1397,42 +1525,42 @@ msgstr "聯邦宇宙使用者名稱格式無效。" #: src/components/article-composer/ArticleComposerContext.tsx:321 #: src/components/article-composer/ArticleComposerContext.tsx:394 #: src/components/article-composer/ArticleComposerContext.tsx:459 -#: src/components/NoteComposer.tsx:843 -#: src/components/NoteComposer.tsx:893 -#: src/routes/(root)/[handle]/drafts/index.tsx:184 +#: src/components/NoteComposer.tsx:849 +#: src/components/NoteComposer.tsx:904 +#: src/routes/(root)/[handle]/drafts/index.tsx:185 msgid "Invalid input: {0}" msgstr "無效輸入:{0}" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:348 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:349 msgid "Invalid input: {inputPath}" msgstr "無效輸入:{inputPath}" #. placeholder {0}: link.inviter.name ?? link.inviter.username -#: src/routes/(root)/[handle]/invite/[id].tsx:237 +#: src/routes/(root)/[handle]/invite/[id].tsx:242 msgid "Invitation from {0}" msgstr "來自 {0} 的邀請" -#: src/routes/(root)/[handle]/settings/invite.tsx:379 +#: src/routes/(root)/[handle]/settings/invite.tsx:380 msgid "Invitation language" msgstr "邀請語言" -#: src/routes/(root)/[handle]/settings/invite.tsx:531 +#: src/routes/(root)/[handle]/settings/invite.tsx:532 msgid "Invitation link created" msgstr "邀請連結已建立" -#: src/routes/(root)/[handle]/settings/invite.tsx:582 +#: src/routes/(root)/[handle]/settings/invite.tsx:583 msgid "Invitation link deleted" msgstr "邀請連結已刪除" -#: src/routes/(root)/[handle]/settings/invite.tsx:626 +#: src/routes/(root)/[handle]/settings/invite.tsx:627 msgid "Invitation links" msgstr "邀請連結" -#: src/routes/(root)/[handle]/settings/invite.tsx:258 +#: src/routes/(root)/[handle]/settings/invite.tsx:259 msgid "Invitation sent" msgstr "邀請已發送" -#: src/components/AppSidebar.tsx:773 +#: src/components/AppSidebar.tsx:796 #: src/routes/(root)/admin/invitations.tsx:148 msgid "Invitations" msgstr "邀請" @@ -1441,13 +1569,13 @@ msgstr "邀請" msgid "Invitations left" msgstr "剩餘邀請" -#: src/components/AppSidebar.tsx:642 +#: src/components/AppSidebar.tsx:665 #: src/components/SettingsTabs.tsx:69 -#: src/routes/(root)/[handle]/settings/invite.tsx:295 +#: src/routes/(root)/[handle]/settings/invite.tsx:296 msgid "Invite" msgstr "邀請" -#: src/routes/(root)/[handle]/settings/invite.tsx:304 +#: src/routes/(root)/[handle]/settings/invite.tsx:305 msgid "Invite a friend" msgstr "邀請朋友" @@ -1463,23 +1591,27 @@ msgstr "邀請人" msgid "Italic" msgstr "斜體" -#: src/routes/(root)/[handle]/settings/index.tsx:474 +#: src/routes/(root)/[handle]/settings/index.tsx:475 msgid "John Doe" msgstr "張三" +#: src/components/NewsDiscussionComposer.tsx:41 +msgid "Join the discussion about this story." +msgstr "參與關於這個話題的討論。" + #. placeholder {0}: "DATE" -#: src/routes/(root)/[handle]/settings/invite.tsx:921 +#: src/routes/(root)/[handle]/settings/invite.tsx:922 msgid "Joined on {0}" msgstr "於{0}加入" -#: src/components/NoteComposeModal.tsx:132 +#: src/components/NoteComposeModal.tsx:133 msgid "Keep editing" msgstr "繼續編輯" #: src/components/article-composer/ArticleComposerPublishFields.tsx:53 #: src/components/LanguageList.tsx:33 #: src/components/LanguageSelect.tsx:93 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:489 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:490 msgid "Language" msgstr "語言" @@ -1487,7 +1619,7 @@ msgstr "語言" msgid "Language code" msgstr "語言代碼" -#: src/routes/(root)/[handle]/settings/language.tsx:82 +#: src/routes/(root)/[handle]/settings/language.tsx:83 msgid "Language settings" msgstr "語言設定" @@ -1495,16 +1627,25 @@ msgstr "語言設定" msgid "Languages" msgstr "語言" +#: src/components/NewsStoryCard.tsx:205 +#: src/components/NewsStoryHeader.tsx:106 +msgid "Last active" +msgstr "最近活動" + #: src/components/admin/AdminAccountsTable.tsx:278 msgid "Last activity" msgstr "最近活動" +#: src/routes/(root)/admin/news.tsx:360 +msgid "Last recomputed:" +msgstr "上次重新計算:" + #: src/routes/(root)/admin/invitations.tsx:160 msgid "Last regenerated:" msgstr "上次重新發放:" #. placeholder {0}: "RELATIVE_DATE" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:450 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:451 msgid "Last used {0}" msgstr "{0}最後使用" @@ -1520,21 +1661,28 @@ msgstr "連結作者:" #~ msgid "Link author: " #~ msgstr "連結作者:" -#: src/routes/(root)/[handle]/invite/[id].tsx:255 +#: src/routes/(root)/[handle]/invite/[id].tsx:260 msgid "Link expired" msgstr "連結已過期" -#: src/routes/(root)/[handle]/settings/index.tsx:560 +#: src/routes/(root)/[handle]/settings/index.tsx:561 msgid "Link name" msgstr "連結名" +#: src/routes/(root)/admin/news.tsx:465 +msgid "Links a moderator has demoted in the popular feed. Clear a penalty to restore a link's normal ranking." +msgstr "版主在熱門資訊流中降級的連結。清除處罰可恢復連結的正常排名。" + +#: src/routes/(root)/news/index.tsx:41 +msgid "Links circulating across the fediverse, ranked by how much they are being shared and discussed." +msgstr "聯邦宇宙中流傳的連結,依照被轉貼與討論的熱度排名。" + #: src/components/ui/markdown-editor.tsx:291 msgid "List" msgstr "列表" -#: src/components/BlockedAccountsList.tsx:202 -#: src/components/MutedAccountsList.tsx:199 -#: src/routes/(root)/[handle]/drafts/index.tsx:348 +#: src/components/AccountListBase.tsx:115 +#: src/routes/(root)/[handle]/drafts/index.tsx:350 msgid "Load more" msgstr "載入更多" @@ -1558,7 +1706,7 @@ msgstr "載入更多粉絲" msgid "Load more following" msgstr "載入更多關注" -#: src/routes/(root)/[handle]/settings/invite.tsx:950 +#: src/routes/(root)/[handle]/settings/invite.tsx:951 msgid "Load more invitees" msgstr "載入更多受邀者" @@ -1570,7 +1718,7 @@ msgstr "載入更多貼文" msgid "Load more notifications" msgstr "載入更多通知" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:496 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:497 msgid "Load more passkeys" msgstr "載入更多通行金鑰" @@ -1582,8 +1730,9 @@ msgstr "載入更多通行金鑰" msgid "Load more posts" msgstr "載入更多內容" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:197 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:204 +#: src/components/NewsDiscussionThread.tsx:378 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:205 msgid "Load more quotes" msgstr "載入更多引用" @@ -1591,15 +1740,20 @@ msgstr "載入更多引用" #~ msgid "Load more reactors (+{0})" #~ msgstr "載入更多反應者 (+{0})" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:673 +#: src/components/NewsDiscussionThread.tsx:399 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:674 msgid "Load more replies" msgstr "載入更多回覆" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:207 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:216 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:208 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:217 msgid "Load more shares" msgstr "載入更多轉貼" +#: src/components/NewsList.tsx:139 +msgid "Load more stories" +msgstr "載入更多新聞" + #: src/components/article-composer/ArticleComposer.tsx:31 msgid "Loading draft…" msgstr "載入草稿中…" @@ -1624,7 +1778,7 @@ msgstr "載入更多粉絲中…" msgid "Loading more following…" msgstr "正在載入更多關注…" -#: src/routes/(root)/[handle]/settings/invite.tsx:944 +#: src/routes/(root)/[handle]/settings/invite.tsx:945 msgid "Loading more invitees…" msgstr "正在載入更多受邀者…" @@ -1636,7 +1790,7 @@ msgstr "載入更多貼文中…" msgid "Loading more notifications" msgstr "正在載入更多通知…" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:493 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:494 msgid "Loading more passkeys…" msgstr "正在載入更多通行金鑰…" @@ -1648,8 +1802,8 @@ msgstr "正在載入更多通行金鑰…" msgid "Loading more posts…" msgstr "載入更多內容中…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:191 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:198 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:192 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:199 msgid "Loading more quotes…" msgstr "正在載入更多引用…" @@ -1657,21 +1811,28 @@ msgstr "正在載入更多引用…" msgid "Loading more reactors…" msgstr "正在載入更多反應者…" -#: src/routes/(root)/[handle]/[noteId]/index.tsx:667 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:668 msgid "Loading more replies…" msgstr "正在載入更多回覆…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:201 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:210 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:202 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:211 msgid "Loading more shares…" msgstr "正在載入更多轉貼…" -#: src/components/BlockedAccountsList.tsx:196 -#: src/components/MutedAccountsList.tsx:193 +#: src/components/NewsDiscussion.tsx:79 +msgid "Loading more sharing posts…" +msgstr "正在載入更多轉貼內容…" + +#: src/components/NewsList.tsx:141 +msgid "Loading more stories…" +msgstr "正在載入更多新聞…" + +#: src/components/AccountListBase.tsx:109 msgid "Loading more…" msgstr "正在載入更多…" -#: src/components/NoteComposer.tsx:1066 +#: src/components/NoteComposer.tsx:1077 msgid "Loading quoted post…" msgstr "正在載入引用內容…" @@ -1679,13 +1840,13 @@ msgstr "正在載入引用內容…" msgid "Loading search results…" msgstr "正在載入搜尋結果…" -#: src/components/NoteComposer.tsx:1015 -#: src/routes/(root)/[handle]/drafts/index.tsx:342 +#: src/components/NoteComposer.tsx:1026 +#: src/routes/(root)/[handle]/drafts/index.tsx:344 #: src/routes/(root)/sign/up/[token].tsx:465 msgid "Loading…" msgstr "載入中…" -#: src/routes/(root)/[handle]/settings/preferences.tsx:226 +#: src/routes/(root)/[handle]/settings/preferences.tsx:227 msgid "Locked to \"Only me\" because your default note privacy restricts visibility." msgstr "由於預設貼文隱私設定限制了可見性,已鎖定為「僅自己」。" @@ -1702,12 +1863,12 @@ msgid "Markdown guide" msgstr "Markdown 指南" #: src/components/article-composer/ArticleComposerForm.tsx:90 -#: src/components/NoteComposer.tsx:1232 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:417 +#: src/components/NoteComposer.tsx:1243 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:418 msgid "Markdown supported" msgstr "Markdown 可用" -#: src/components/AppSidebar.tsx:796 +#: src/components/AppSidebar.tsx:819 #: src/routes/(root)/admin/media.tsx:152 msgid "Media" msgstr "媒體" @@ -1718,6 +1879,11 @@ msgstr "媒體" msgid "Mentioned only" msgstr "只提及使用者可見" +#: src/components/NewsStoryCard.tsx:225 +#: src/components/NewsStoryCard.tsx:243 +msgid "Moderate" +msgstr "管理" + #: src/components/PostEngagementBar.tsx:436 #: src/components/PostEngagementBar.tsx:437 msgid "More engagement views" @@ -1747,7 +1913,7 @@ msgstr "多選" msgid "Mute" msgstr "靜音" -#: src/routes/(root)/[handle]/settings/blocks.tsx:84 +#: src/routes/(root)/[handle]/settings/blocks.tsx:85 msgid "Muted accounts" msgstr "已靜音的帳號" @@ -1755,7 +1921,7 @@ msgstr "已靜音的帳號" #~ msgid "Muted accounts are hidden from your feeds and stop notifying you, but you can still visit their profiles. Muting is private and is never federated." #~ msgstr "被靜音的帳號不會出現在你的時間軸中,也不會向你發送通知,但你仍可造訪其個人檔案。靜音是私密的,且不會進行聯邦。" -#: src/routes/(root)/[handle]/settings/blocks.tsx:86 +#: src/routes/(root)/[handle]/settings/blocks.tsx:87 msgid "Muted accounts are hidden from your feeds and stop notifying you, except for replies and mentions from accounts you follow. You can still visit their profiles, and muting is private and never federated." msgstr "被靜音的帳號不會出現在你的時間軸中,也不會通知你,但你關注的帳號的回覆與提及仍會通知你。你仍可造訪其個人檔案;靜音是私密的,且不會進行聯邦。" @@ -1763,11 +1929,11 @@ msgstr "被靜音的帳號不會出現在你的時間軸中,也不會通知你 msgid "Mutes & blocks" msgstr "靜音與封鎖" -#: src/routes/(root)/[handle]/settings/blocks.tsx:77 +#: src/routes/(root)/[handle]/settings/blocks.tsx:78 msgid "Mutes and blocks" msgstr "靜音與封鎖" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:375 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:376 msgid "My passkey" msgstr "我的通行金鑰" @@ -1783,21 +1949,25 @@ msgstr "使用者名稱太長。不能長於 50 字元。" msgid "Native name" msgstr "本地名稱" +#: src/routes/(root)/admin/news.tsx:365 +msgid "never" +msgstr "從未" + #: src/routes/(root)/admin/invitations.tsx:167 msgid "Never" msgstr "從未" -#: src/routes/(root)/[handle]/settings/invite.tsx:755 -#: src/routes/(root)/[handle]/settings/invite.tsx:821 +#: src/routes/(root)/[handle]/settings/invite.tsx:756 +#: src/routes/(root)/[handle]/settings/invite.tsx:822 msgid "Never expires" msgstr "永不過期" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:446 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:447 msgid "Never used" msgstr "從未使用過" -#: src/routes/(root)/[handle]/drafts/index.tsx:251 -#: src/routes/(root)/[handle]/drafts/new.tsx:83 +#: src/routes/(root)/[handle]/drafts/index.tsx:253 +#: src/routes/(root)/[handle]/drafts/new.tsx:85 msgid "New article" msgstr "新建文章" @@ -1810,6 +1980,22 @@ msgstr "現在即使 Hackers' Pub 未開啟,也可以顯示新通知。" msgid "New posts available — click to load" msgstr "有新內容 — 點擊載入" +#: src/components/NewsList.tsx:89 +msgid "Newest" +msgstr "最新" + +#: src/components/AppSidebar.tsx:244 +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:337 +#: src/routes/(root)/news/index.tsx:39 +msgid "News" +msgstr "新聞" + +#: src/components/AppSidebar.tsx:842 +#: src/routes/(root)/admin/news.tsx:139 +#~ msgid "News scores" +#~ msgstr "新聞評分" + #: src/components/ImageLightbox.tsx:126 msgid "Next image" msgstr "下一張圖片" @@ -1822,10 +2008,14 @@ msgstr "暫無收藏" msgid "No draft to delete" msgstr "沒有要刪除的草稿" -#: src/routes/(root)/[handle]/drafts/index.tsx:262 +#: src/routes/(root)/[handle]/drafts/index.tsx:264 msgid "No drafts yet. Create your first article!" msgstr "還沒有草稿。建立您的第一篇文章!" +#: src/routes/(root)/admin/news.tsx:428 +msgid "No exclusion patterns yet." +msgstr "尚無排除模式。" + #: src/components/ActorFollowerList.tsx:92 msgid "No followers found" msgstr "未找到粉絲" @@ -1834,9 +2024,9 @@ msgstr "未找到粉絲" msgid "No following found" msgstr "沒有找到關注的使用者" -#: src/routes/(root)/[handle]/invite/[id].tsx:265 -#: src/routes/(root)/[handle]/settings/invite.tsx:410 -#: src/routes/(root)/[handle]/settings/invite.tsx:831 +#: src/routes/(root)/[handle]/invite/[id].tsx:270 +#: src/routes/(root)/[handle]/settings/invite.tsx:411 +#: src/routes/(root)/[handle]/settings/invite.tsx:832 msgid "No invitations left" msgstr "沒有剩餘邀請名額" @@ -1852,16 +2042,24 @@ msgstr "未找到文章" msgid "No notes found" msgstr "未找到貼文" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:171 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:178 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:172 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:179 msgid "No one has quoted this yet." msgstr "尚無引用。" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:184 +#: src/components/NewsDiscussion.tsx:86 +msgid "No one has shared this link in a public post yet." +msgstr "還沒有人在公開貼文中轉貼這個連結。" + +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:185 msgid "No one has shared this yet." msgstr "尚無轉貼。" +#: src/routes/(root)/admin/news.tsx:473 +msgid "No penalized links." +msgstr "沒有受罰連結。" + #: src/components/ActorPostList.tsx:140 #: src/components/ActorSharedPostList.tsx:93 #: src/components/PersonalTimeline.tsx:289 @@ -1874,8 +2072,8 @@ msgstr "未找到內容" msgid "No previews" msgstr "不顯示預覽" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:175 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:189 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:176 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:190 msgid "No reactions yet." msgstr "尚無反應。" @@ -1883,6 +2081,10 @@ msgstr "尚無反應。" msgid "No reactors loaded." msgstr "未載入反應者。" +#: src/components/NewsList.tsx:148 +msgid "No shared links yet. Once links start circulating across the fediverse, they will appear here." +msgstr "還沒有被轉貼的連結。當連結開始在聯邦宇宙中流傳時,就會顯示在這裡。" + #: src/routes/(root)/sign/index.tsx:223 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pub 中無此帳戶,請重試。" @@ -1891,16 +2093,29 @@ msgstr "Hackers' Pub 中無此帳戶,請重試。" msgid "No user URI provided." msgstr "未提供使用者 URI。" +#: src/routes/(root)/admin/news.tsx:303 +msgid "Not authorized to clear penalties." +msgstr "無權清除處罰。" + #: src/routes/(root)/admin/media.tsx:117 msgid "Not authorized to delete orphan media." msgstr "無權刪除孤立媒體。" +#: src/routes/(root)/admin/news.tsx:244 +#: src/routes/(root)/admin/news.tsx:274 +msgid "Not authorized to manage exclusions." +msgstr "無權管理排除模式。" + +#: src/routes/(root)/admin/news.tsx:203 +msgid "Not authorized to recompute news scores." +msgstr "無權重新計算新聞評分。" + #: src/routes/(root)/admin/invitations.tsx:116 msgid "Not authorized to regenerate invitations." msgstr "無權重新發放邀請。" -#: src/routes/(root)/[handle]/invite/[id].tsx:222 -#: src/routes/(root)/[handle]/invite/[id].tsx:225 +#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:230 msgid "Not found" msgstr "未找到" @@ -1912,26 +2127,30 @@ msgstr "未找到" #~ msgid "Note" #~ msgstr "貼文" -#: src/components/NoteComposer.tsx:885 +#: src/routes/(root)/admin/news.tsx:407 +msgid "Note (optional)" +msgstr "備註(可選)" + +#: src/components/NoteComposer.tsx:896 msgid "Note created successfully" msgstr "貼文建立成功" #. placeholder {0}: "REL_ME_ATTR" -#: src/routes/(root)/[handle]/settings/index.tsx:523 +#: src/routes/(root)/[handle]/settings/index.tsx:524 msgid "Note that you can verify your links belong to you by making the linked pages also link to your Hackers' Pub profile with {0} attribute." msgstr "你可以驗證這些連結確實屬於你。在連結的頁面上使用 {0} 屬性連結回你的 Hackers' Pub 個人資料頁。" -#: src/components/NoteComposer.tsx:833 +#: src/components/NoteComposer.tsx:839 msgid "Note updated" msgstr "貼文已更新。" #: src/components/ProfileTabs.tsx:44 -#: src/routes/(root)/[handle]/bookmarks.tsx:137 +#: src/routes/(root)/[handle]/bookmarks.tsx:138 msgid "Notes" msgstr "貼文" #: src/components/MarkdownEditor.tsx:193 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:462 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:463 msgid "Nothing to preview" msgstr "沒有可預覽的內容" @@ -1944,7 +2163,7 @@ msgstr "未授予通知權限。" msgid "Notification preview privacy" msgstr "通知預覽隱私" -#: src/components/AppSidebar.tsx:574 +#: src/components/AppSidebar.tsx:597 msgid "Notifications" msgstr "通知" @@ -1957,7 +2176,7 @@ msgstr "此網站的通知已遭封鎖。" msgid "Notifications are blocked in your browser settings." msgstr "瀏覽器設定已封鎖通知。" -#: src/routes/(root)/[handle]/settings/invite.tsx:782 +#: src/routes/(root)/[handle]/settings/invite.tsx:783 msgid "Number of invitations" msgstr "邀請次數" @@ -1982,7 +2201,7 @@ msgstr "或" msgid "Or enter the code from the email" msgstr "或輸入郵件中的驗證碼" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:877 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:878 msgid "Other languages" msgstr "其他語言" @@ -1994,31 +2213,39 @@ msgstr "頁面未找到" msgid "Passkey authentication failed" msgstr "通行金鑰驗證失敗" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:370 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:371 msgid "Passkey name" msgstr "通行金鑰名稱" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:265 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:266 msgid "Passkey registered successfully" msgstr "通行金鑰註冊成功" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:312 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 msgid "Passkey revoked" msgstr "通行金鑰已撤銷" #: src/components/SettingsTabs.tsx:77 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:355 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:356 msgid "Passkeys" msgstr "通行金鑰" -#: src/routes/(root)/[handle]/bookmarks.tsx:98 -#: src/routes/(root)/[handle]/bookmarks.tsx:101 -#: src/routes/(root)/[handle]/drafts/[id].tsx:50 -#: src/routes/(root)/[handle]/drafts/[id].tsx:51 -#: src/routes/(root)/[handle]/drafts/index.tsx:213 -#: src/routes/(root)/[handle]/drafts/index.tsx:216 -#: src/routes/(root)/[handle]/drafts/new.tsx:60 -#: src/routes/(root)/[handle]/drafts/new.tsx:61 +#: src/routes/(root)/admin/news.tsx:463 +msgid "Penalized links" +msgstr "受罰連結" + +#: src/routes/(root)/admin/news.tsx:297 +msgid "Penalty cleared." +msgstr "已清除處罰。" + +#: src/routes/(root)/[handle]/bookmarks.tsx:99 +#: src/routes/(root)/[handle]/bookmarks.tsx:102 +#: src/routes/(root)/[handle]/drafts/[id].tsx:52 +#: src/routes/(root)/[handle]/drafts/[id].tsx:53 +#: src/routes/(root)/[handle]/drafts/index.tsx:215 +#: src/routes/(root)/[handle]/drafts/index.tsx:218 +#: src/routes/(root)/[handle]/drafts/new.tsx:62 +#: src/routes/(root)/[handle]/drafts/new.tsx:63 msgid "Permission denied" msgstr "權限被拒絕" @@ -2026,21 +2253,21 @@ msgstr "權限被拒絕" msgid "Pin to profile" msgstr "釘選到個人資料" -#: src/routes/(root)/[handle]/(profile)/index.tsx:302 +#: src/routes/(root)/[handle]/(profile)/index.tsx:305 msgid "Pinned posts" msgstr "已釘選內容" -#: src/routes/(root)/[handle]/settings/index.tsx:187 +#: src/routes/(root)/[handle]/settings/index.tsx:188 msgid "Please choose an image file smaller than 5 MiB." msgstr "請選擇小於 5 MiB 的圖片檔案。" -#: src/routes/(root)/[handle]/settings/invite.tsx:249 -#: src/routes/(root)/[handle]/settings/invite.tsx:540 +#: src/routes/(root)/[handle]/settings/invite.tsx:250 +#: src/routes/(root)/[handle]/settings/invite.tsx:541 msgid "Please correct the errors and try again." msgstr "請修正錯誤並重試。" #: src/components/article-composer/ArticleComposerForm.tsx:53 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:383 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:384 msgid "Please enter a title for your article." msgstr "請輸入文章標題。" @@ -2048,9 +2275,9 @@ msgstr "請輸入文章標題。" msgid "Please enter your Fediverse handle." msgstr "請輸入你的聯邦宇宙使用者名稱。" -#: src/routes/(root)/[handle]/drafts/[id].tsx:55 -#: src/routes/(root)/[handle]/drafts/index.tsx:220 -#: src/routes/(root)/[handle]/drafts/new.tsx:65 +#: src/routes/(root)/[handle]/drafts/[id].tsx:57 +#: src/routes/(root)/[handle]/drafts/index.tsx:222 +#: src/routes/(root)/[handle]/drafts/new.tsx:67 msgid "Please sign in to access this page" msgstr "請登入以存取此頁面" @@ -2062,6 +2289,10 @@ msgstr "請登入後投票" msgid "Poll closed" msgstr "投票已結束" +#: src/components/NewsList.tsx:88 +msgid "Popular" +msgstr "熱門" + #: src/components/PostActionMenu.tsx:291 msgid "Post deleted" msgstr "內容已刪除" @@ -2079,27 +2310,27 @@ msgstr "已取消釘選內容" msgid "Posts" msgstr "內容" -#: src/routes/(root)/[handle]/settings/preferences.tsx:183 +#: src/routes/(root)/[handle]/settings/preferences.tsx:184 msgid "Prefer AI-generated summary" msgstr "優先使用 AI 生成的摘要" #: src/components/SettingsTabs.tsx:53 -#: src/routes/(root)/[handle]/settings/preferences.tsx:169 #: src/routes/(root)/[handle]/settings/preferences.tsx:170 +#: src/routes/(root)/[handle]/settings/preferences.tsx:171 msgid "Preferences" msgstr "偏好設定" -#: src/routes/(root)/[handle]/invite/[id].tsx:387 +#: src/routes/(root)/[handle]/invite/[id].tsx:392 msgid "Preferred language" msgstr "偏好語言" -#: src/routes/(root)/[handle]/settings/language.tsx:83 +#: src/routes/(root)/[handle]/settings/language.tsx:84 msgid "Preferred languages" msgstr "偏好語言" #: src/components/article-composer/ArticleComposerForm.tsx:111 #: src/components/MarkdownEditor.tsx:164 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:426 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:427 msgid "Preview" msgstr "預覽" @@ -2111,7 +2342,7 @@ msgstr "上一張圖片" msgid "Priority" msgstr "優先級" -#: src/components/AppSidebar.tsx:982 +#: src/components/AppSidebar.tsx:1028 #: src/routes/(root)/privacy.tsx:40 msgid "Privacy policy" msgstr "隱私權政策" @@ -2124,8 +2355,8 @@ msgstr "個人資料" msgid "Profile actions" msgstr "個人檔案操作" -#: src/routes/(root)/[handle]/settings/index.tsx:124 #: src/routes/(root)/[handle]/settings/index.tsx:125 +#: src/routes/(root)/[handle]/settings/index.tsx:126 msgid "Profile settings" msgstr "個人資料設定" @@ -2155,8 +2386,8 @@ msgstr "發布中…" msgid "Push notification privacy updated" msgstr "已更新推播通知隱私設定" -#: src/routes/(root)/[handle]/settings/invite.tsx:647 -#: src/routes/(root)/[handle]/settings/invite.tsx:713 +#: src/routes/(root)/[handle]/settings/invite.tsx:648 +#: src/routes/(root)/[handle]/settings/invite.tsx:714 msgid "QR code" msgstr "QR 碼" @@ -2170,7 +2401,7 @@ msgstr "搜尋關鍵詞不能為空" msgid "Quiet public" msgstr "悄悄公開" -#: src/components/NoteComposeModal.tsx:71 +#: src/components/NoteComposeModal.tsx:72 #: src/components/PostEngagementBar.tsx:270 #: src/components/ui/markdown-editor.tsx:289 msgid "Quote" @@ -2194,8 +2425,8 @@ msgid "Quoted post hidden" msgstr "引用內容已隱藏" #: src/components/EngagementTabs.tsx:46 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:100 -#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:111 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/quotes.tsx:101 +#: src/routes/(root)/[handle]/[noteId]/quotes.tsx:112 msgid "Quotes" msgstr "引用" @@ -2218,8 +2449,8 @@ msgid "reaction" msgstr "反應" #: src/components/EngagementTabs.tsx:55 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:159 -#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:173 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/reactions.tsx:160 +#: src/routes/(root)/[handle]/[noteId]/reactions.tsx:174 msgid "Reactions" msgstr "反應" @@ -2232,6 +2463,10 @@ msgstr "閱讀完整文章" #~ msgid "Read the full Code of conduct" #~ msgstr "閱讀完整的行為守則" +#: src/routes/(root)/admin/news.tsx:344 +msgid "Rebuilds the popularity score of every shared link from scratch. The operation is idempotent and safe to run at any time; scores normally stay fresh on their own, so this is mainly a manual backstop and a development tool." +msgstr "從頭重新計算每個被轉貼連結的熱度評分。此操作是冪等的,隨時執行都安全。評分通常會自動保持最新,因此這主要是手動備用手段與開發工具。" + #: src/components/WebPushPromptBanner.tsx:252 msgid "Receive new notifications immediately, even when this tab is closed." msgstr "即使此分頁已關閉,也能立即收到新通知。" @@ -2240,10 +2475,19 @@ msgstr "即使此分頁已關閉,也能立即收到新通知。" msgid "Receive notifications immediately through this browser, even when this tab is closed." msgstr "透過此瀏覽器立即接收通知,即使此分頁已關閉。" -#: src/components/AppSidebar.tsx:901 +#: src/components/AppSidebar.tsx:947 msgid "Recent drafts" msgstr "最近的草稿" +#: src/routes/(root)/admin/news.tsx:342 +#: src/routes/(root)/admin/news.tsx:375 +msgid "Recompute news scores" +msgstr "重新計算新聞評分" + +#: src/routes/(root)/admin/news.tsx:374 +msgid "Recomputing…" +msgstr "正在重新計算…" + #: src/routes/(root)/admin/invitations.tsx:203 msgid "Regenerate" msgstr "重新發放" @@ -2260,23 +2504,23 @@ msgstr "重新發放邀請" msgid "Regenerating…" msgstr "正在重新發放…" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Register" msgstr "註冊" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:362 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:363 msgid "Register a passkey" msgstr "註冊通行金鑰" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:364 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:365 msgid "Register a passkey to sign in to your account. You can use a passkey instead of receiving a sign-in link by email." msgstr "為你的帳戶註冊通行金鑰。你可以使用通行金鑰登入,而不是通過電子郵件接收登入連結。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:393 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:394 msgid "Registered passkeys" msgstr "已註冊的通行金鑰" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:386 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:387 msgid "Registering…" msgstr "註冊…" @@ -2287,6 +2531,7 @@ msgid "Remote follow" msgstr "遠端關注" #: src/components/LanguageList.tsx:225 +#: src/routes/(root)/admin/news.tsx:451 msgid "Remove" msgstr "移除" @@ -2304,13 +2549,13 @@ msgstr "取消收藏" msgid "Remove from sidebar" msgstr "從側邊欄移除" -#: src/components/NoteComposer.tsx:1358 -#: src/components/NoteComposer.tsx:1359 +#: src/components/NoteComposer.tsx:1369 +#: src/components/NoteComposer.tsx:1370 msgid "Remove image" msgstr "移除圖片" -#: src/components/NoteComposer.tsx:1113 -#: src/components/NoteComposer.tsx:1114 +#: src/components/NoteComposer.tsx:1124 +#: src/components/NoteComposer.tsx:1125 msgid "Remove quote" msgstr "移除引用" @@ -2320,11 +2565,11 @@ msgstr "移除已足夠舊且不再附加到頭像、貼文、文章草稿或文 #: src/components/article-composer/ArticleComposerForm.tsx:134 #: src/components/MarkdownEditor.tsx:181 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:452 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:453 msgid "Rendering…" msgstr "渲染中…" -#: src/components/NoteComposeModal.tsx:70 +#: src/components/NoteComposeModal.tsx:71 #: src/components/PostEngagementBar.tsx:258 msgid "Reply" msgstr "回覆" @@ -2333,20 +2578,20 @@ msgstr "回覆" msgid "Replying is not available for this post" msgstr "無法回覆此內容" -#: src/components/NoteComposer.tsx:1007 +#: src/components/NoteComposer.tsx:1018 msgid "Replying to" msgstr "回覆給" -#: src/components/AppSidebar.tsx:498 +#: src/components/AppSidebar.tsx:521 msgid "Return to old UI" msgstr "返回舊介面" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:475 -#: src/routes/(root)/[handle]/settings/passkeys.tsx:526 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:476 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:527 msgid "Revoke" msgstr "撤銷" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:515 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:516 msgid "Revoke passkey" msgstr "撤銷通行金鑰" @@ -2359,14 +2604,14 @@ msgstr "撤銷引用" msgid "Revoke this quote?" msgstr "撤銷此引用?" -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Save" msgstr "儲存" -#: src/components/NoteComposer.tsx:1412 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 +#: src/components/NoteComposer.tsx:1423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 msgid "Save changes" msgstr "儲存變更" @@ -2379,16 +2624,16 @@ msgid "Save draft to see preview" msgstr "儲存草稿以查看預覽" #: src/components/article-composer/ArticleComposerActions.tsx:36 -#: src/components/NoteComposer.tsx:1413 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:528 -#: src/routes/(root)/[handle]/settings/index.tsx:535 -#: src/routes/(root)/[handle]/settings/language.tsx:200 -#: src/routes/(root)/[handle]/settings/preferences.tsx:235 +#: src/components/NoteComposer.tsx:1424 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:529 +#: src/routes/(root)/[handle]/settings/index.tsx:536 +#: src/routes/(root)/[handle]/settings/language.tsx:201 +#: src/routes/(root)/[handle]/settings/preferences.tsx:236 msgid "Saving…" msgstr "儲存中…" #: src/components/admin/AdminAccountsTable.tsx:202 -#: src/components/AppSidebar.tsx:377 +#: src/components/AppSidebar.tsx:400 #: src/components/SearchForm.tsx:65 #: src/components/SearchForm.tsx:80 msgid "Search" @@ -2423,16 +2668,16 @@ msgstr "請選擇一個選項" msgid "Select options" msgstr "請選擇選項" -#: src/routes/(root)/[handle]/settings/language.tsx:84 +#: src/routes/(root)/[handle]/settings/language.tsx:85 msgid "Select your preferred languages in order of preference. This will help tailor content to your preferences." msgstr "按偏好順序選擇您的偏好語言。這將有助於根據您的偏好定製內容。" -#: src/routes/(root)/[handle]/settings/invite.tsx:413 +#: src/routes/(root)/[handle]/settings/invite.tsx:414 msgid "Send" msgstr "發送" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 -#: src/routes/(root)/[handle]/settings/invite.tsx:412 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 +#: src/routes/(root)/[handle]/settings/invite.tsx:413 msgid "Sending…" msgstr "發送中…" @@ -2444,11 +2689,11 @@ msgstr "敏感內容" msgid "Separate tags with spaces. Tags help readers discover your article." msgstr "標籤之間用空格分隔。標籤有助於讀者發現您的文章。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:171 +#: src/routes/(root)/[handle]/settings/preferences.tsx:172 msgid "Set your personal preferences." msgstr "設定您的個人偏好設定。" -#: src/components/AppSidebar.tsx:674 +#: src/components/AppSidebar.tsx:697 msgid "Settings" msgstr "設定" @@ -2456,10 +2701,19 @@ msgstr "設定" msgid "Share" msgstr "分享" +#: src/components/NewsStoryCard.tsx:198 +#: src/components/NewsStoryHeader.tsx:132 +msgid "Share this link" +msgstr "轉貼此連結" + +#: src/components/NewsDiscussionComposer.tsx:30 +msgid "Share your opinion on this story…" +msgstr "分享你對這個話題的看法…" + #: src/components/EngagementTabs.tsx:37 #: src/components/ProfileTabs.tsx:54 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:101 -#: src/routes/(root)/[handle]/[noteId]/shares.tsx:114 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/shares.tsx:102 +#: src/routes/(root)/[handle]/[noteId]/shares.tsx:115 msgid "Shares" msgstr "轉貼" @@ -2474,6 +2728,15 @@ msgstr "無法轉貼此內容。" msgid "Show" msgstr "顯示" +#. placeholder {0}: childCount() +#: src/components/NewsDiscussionThread.tsx:356 +msgid "Show {0} more in this thread" +msgstr "顯示此討論串中的另外 {0} 則" + +#: src/components/NewsDiscussion.tsx:77 +msgid "Show more sharing posts" +msgstr "顯示更多轉貼內容" + #: src/components/article-composer/ArticleComposerForm.tsx:73 #~ msgid "Show preview" #~ msgstr "顯示預覽" @@ -2482,7 +2745,7 @@ msgstr "顯示" msgid "Show sensitive content" msgstr "顯示敏感內容" -#: src/components/AppSidebar.tsx:535 +#: src/components/AppSidebar.tsx:558 #: src/routes/(root)/sign/index.tsx:382 msgid "Sign in" msgstr "登入" @@ -2491,6 +2754,10 @@ msgstr "登入" msgid "Sign in to Hackers' Pub" msgstr "登入 Hackers' Pub" +#: src/components/NewsDiscussionComposer.tsx:44 +msgid "Sign in to post" +msgstr "登入後發表" + #: src/components/QuestionCard.tsx:394 msgid "Sign in to vote" msgstr "登入後投票" @@ -2499,11 +2766,11 @@ msgstr "登入後投票" msgid "Sign in with passkey" msgstr "使用通行金鑰登入" -#: src/components/AppSidebar.tsx:964 +#: src/components/AppSidebar.tsx:1010 msgid "Sign out" msgstr "登出" -#: src/routes/(root)/[handle]/invite/[id].tsx:404 +#: src/routes/(root)/[handle]/invite/[id].tsx:409 #: src/routes/(root)/sign/up/[token].tsx:494 msgid "Sign up" msgstr "註冊" @@ -2536,7 +2803,7 @@ msgstr "網址別名" msgid "Slug cannot be empty" msgstr "網址別名不能為空" -#: src/components/NoteComposer.tsx:613 +#: src/components/NoteComposer.tsx:619 msgid "Some images were skipped because the limit of {MAX_MEDIA} was reached" msgstr "已達到 {MAX_MEDIA} 張圖片上限,部分圖片已被跳過" @@ -2551,22 +2818,22 @@ msgstr "發生錯誤,請重試。" #: src/components/article-composer/ArticleComposerContext.tsx:309 #: src/components/article-composer/ArticleComposerContext.tsx:384 #: src/components/article-composer/ArticleComposerContext.tsx:449 -#: src/components/NoteComposer.tsx:832 -#: src/components/NoteComposer.tsx:884 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:331 -#: src/routes/(root)/[handle]/drafts/index.tsx:174 +#: src/components/NoteComposer.tsx:838 +#: src/components/NoteComposer.tsx:895 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:332 +#: src/routes/(root)/[handle]/drafts/index.tsx:175 msgid "Success" msgstr "成功" -#: src/routes/(root)/[handle]/settings/language.tsx:147 +#: src/routes/(root)/[handle]/settings/language.tsx:148 msgid "Successfully saved language preferences" msgstr "成功儲存語言偏好" -#: src/routes/(root)/[handle]/settings/preferences.tsx:133 +#: src/routes/(root)/[handle]/settings/preferences.tsx:134 msgid "Successfully saved preferences" msgstr "設定已成功儲存" -#: src/routes/(root)/[handle]/settings/index.tsx:328 +#: src/routes/(root)/[handle]/settings/index.tsx:329 msgid "Successfully saved settings" msgstr "設定儲存成功" @@ -2575,9 +2842,9 @@ msgid "Summarized by LLM" msgstr "由 AI 生成的摘要" #: src/components/DocumentView.tsx:38 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:721 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:729 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1056 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:722 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:730 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1057 msgid "Table of contents" msgstr "目錄" @@ -2586,8 +2853,8 @@ msgstr "目錄" #~ msgstr "標籤" #: src/components/article-composer/ArticleComposerForm.tsx:158 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:478 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1065 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:479 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1066 msgid "Tags" msgstr "標籤" @@ -2595,65 +2862,73 @@ msgstr "標籤" msgid "Tell us about yourself…" msgstr "介紹您自己…" +#: src/routes/(root)/admin/news.tsx:237 +msgid "That is not a valid URL pattern." +msgstr "這不是有效的 URL 模式。" + #: src/components/WebPushNotificationSettings.tsx:158 #: src/components/WebPushPromptBanner.tsx:93 msgid "The browser did not provide a complete push subscription." msgstr "瀏覽器未提供完整的推播通知訂閱。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:200 +#: src/routes/(root)/[handle]/settings/preferences.tsx:201 msgid "The default privacy setting for your notes." msgstr "您貼文的預設隱私設定。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:212 +#: src/routes/(root)/[handle]/settings/preferences.tsx:213 msgid "The default privacy setting for your shares." msgstr "您轉貼的預設隱私設定。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:227 +#: src/routes/(root)/[handle]/settings/preferences.tsx:228 msgid "The default quote permission for your notes." msgstr "貼文的預設引用權限設定。" -#: src/routes/(root)/[handle]/invite/[id].tsx:169 -#: src/routes/(root)/[handle]/settings/invite.tsx:352 +#: src/routes/(root)/[handle]/invite/[id].tsx:174 +#: src/routes/(root)/[handle]/settings/invite.tsx:353 msgid "The email address is invalid." msgstr "電子郵件地址無效。" -#: src/routes/(root)/[handle]/settings/invite.tsx:347 +#: src/routes/(root)/[handle]/settings/invite.tsx:348 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "電子郵件地址不僅用於接收邀請,還用於登入帳戶。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:395 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:396 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "以下通行金鑰已註冊到你的帳戶。你可以使用它們登入你的帳戶。" -#: src/routes/(root)/[handle]/invite/[id].tsx:187 +#: src/routes/(root)/[handle]/invite/[id].tsx:192 msgid "The invitation email could not be sent. Please try again later." msgstr "邀請信件傳送失敗。請稍後再試。" -#: src/routes/(root)/[handle]/settings/invite.tsx:259 +#: src/routes/(root)/[handle]/settings/invite.tsx:260 msgid "The invitation has been sent successfully." msgstr "邀請已成功發送。" -#: src/routes/(root)/[handle]/settings/invite.tsx:590 +#: src/routes/(root)/[handle]/settings/invite.tsx:591 msgid "The invitation link could not be found or you are not authorized to delete it." msgstr "找不到該邀請連結,或您無權刪除該連結。" -#: src/routes/(root)/[handle]/settings/invite.tsx:612 +#: src/routes/(root)/[handle]/settings/invite.tsx:613 msgid "The invitation link has been copied to the clipboard." msgstr "邀請連結已複製到剪貼簿。" -#: src/routes/(root)/[handle]/settings/invite.tsx:532 +#: src/routes/(root)/[handle]/settings/invite.tsx:533 msgid "The invitation link has been created successfully." msgstr "邀請連結已成功建立。" -#: src/routes/(root)/[handle]/settings/invite.tsx:583 +#: src/routes/(root)/[handle]/settings/invite.tsx:584 msgid "The invitation link has been deleted successfully." msgstr "邀請連結已成功刪除。" +#: src/components/NewsDiscussionComposer.tsx:34 +msgid "The link to this story is added to your post automatically." +msgstr "這個話題的連結會自動加入你的內容中。" + #: src/components/NotFoundPage.tsx:45 msgid "The page you're looking for doesn't exist or has been moved." msgstr "您要找的頁面不存在或已移動。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:313 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:314 msgid "The passkey has been successfully revoked." msgstr "通行金鑰已成功撤銷。" @@ -2671,7 +2946,7 @@ msgstr "註冊連結無效。請確保你使用的是你收到的正確郵件連 #. placeholder {0}: "GITHUB_REPOSITORY" #. placeholder {1}: "AGPL-3.0" -#: src/components/AppSidebar.tsx:1006 +#: src/components/AppSidebar.tsx:1052 msgid "The source code of this website is available on {0} under the {1} license." msgstr "可在 {0} 上以 {1} 授權取得該網站的原始碼。" @@ -2679,7 +2954,7 @@ msgstr "可在 {0} 上以 {1} 授權取得該網站的原始碼。" msgid "The title will appear at the top of your article and in link previews." msgstr "標題將顯示在文章頂部和連結預覽中。" -#: src/routes/(root)/[handle]/settings/index.tsx:603 +#: src/routes/(root)/[handle]/settings/index.tsx:604 msgid "The URL of the link, e.g., https://github.com/yourhandle." msgstr "該連結的 URL,例如 https://github.com/你的使用者名稱。" @@ -2697,26 +2972,26 @@ msgstr "此操作無法復原。這將永久刪除該內容。" msgid "This browser does not support Web Push." msgstr "此瀏覽器不支援 Web Push。" -#: src/routes/(root)/[handle]/invite/[id].tsx:173 -#: src/routes/(root)/[handle]/invite/[id].tsx:177 +#: src/routes/(root)/[handle]/invite/[id].tsx:178 +#: src/routes/(root)/[handle]/invite/[id].tsx:182 msgid "This email is already associated with an existing account." msgstr "此信箱地址已關聯到現有帳戶。" -#: src/routes/(root)/[handle]/invite/[id].tsx:227 +#: src/routes/(root)/[handle]/invite/[id].tsx:232 msgid "This invitation link does not exist or has been deleted." msgstr "此邀請連結不存在或已被刪除。" -#: src/routes/(root)/[handle]/invite/[id].tsx:161 -#: src/routes/(root)/[handle]/invite/[id].tsx:257 +#: src/routes/(root)/[handle]/invite/[id].tsx:166 +#: src/routes/(root)/[handle]/invite/[id].tsx:262 msgid "This invitation link has expired." msgstr "此邀請連結已過期。" -#: src/routes/(root)/[handle]/invite/[id].tsx:164 -#: src/routes/(root)/[handle]/invite/[id].tsx:267 +#: src/routes/(root)/[handle]/invite/[id].tsx:169 +#: src/routes/(root)/[handle]/invite/[id].tsx:272 msgid "This invitation link has no remaining invitations." msgstr "此邀請連結已無剩餘邀請次數。" -#: src/routes/(root)/[handle]/invite/[id].tsx:159 +#: src/routes/(root)/[handle]/invite/[id].tsx:164 msgid "This invitation link was not found." msgstr "未找到此邀請連結。" @@ -2748,7 +3023,7 @@ msgstr "此伺服器尚未設定 Web Push。" msgid "This service does not support remote follow." msgstr "該服務不支援遠端關注。" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:552 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:553 msgid "This usually takes about a minute. The page will update automatically when the translation is ready." msgstr "通常需要一分鐘左右。翻譯完成後頁面將自動更新。" @@ -2765,7 +3040,7 @@ msgid "Timeline" msgstr "時間軸" #: src/components/article-composer/ArticleComposerForm.tsx:49 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:380 msgid "Title" msgstr "標題" @@ -2789,7 +3064,7 @@ msgstr "共 {0} 個" #. placeholder {0}: "LANGUAGE" #: src/components/ArticleCard.tsx:350 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:861 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:862 msgid "Translated from {0}" msgstr "翻譯自{0}" @@ -2797,18 +3072,18 @@ msgstr "翻譯自{0}" #~ msgid "Translating to {0}…" #~ msgstr "正在翻譯為{0}…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:547 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:548 msgid "Translating to {name}…" msgstr "正在翻譯為{name}…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:543 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:544 msgid "Translating…" msgstr "正在翻譯…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:378 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:401 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:410 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:588 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:379 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:402 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/[lang].tsx:411 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:589 msgid "Translation request failed" msgstr "翻譯請求失敗" @@ -2816,17 +3091,17 @@ msgstr "翻譯請求失敗" #~ msgid "Translation request failed for {0}" #~ msgstr "向{0}的翻譯請求失敗" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:593 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:594 msgid "Translation request failed for {name}" msgstr "向{name}的翻譯請求失敗" #: src/app.tsx:125 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:601 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:602 msgid "Try again" msgstr "重試" #: src/components/article-composer/ArticleComposerForm.tsx:162 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:482 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:483 msgid "Type tags separated by spaces" msgstr "輸入標籤,用空格分隔" @@ -2844,7 +3119,7 @@ msgstr "無法新增反應。請重試。" msgid "Unable to remove reaction. Please try again." msgstr "無法移除反應。請重試。" -#: src/components/BlockedAccountsList.tsx:181 +#: src/components/BlockedAccountsList.tsx:117 #: src/components/ProfileActionMenu.tsx:377 #: src/components/ProfileActionMenu.tsx:405 #: src/components/ProfileActionMenu.tsx:413 @@ -2860,7 +3135,7 @@ msgstr "解除封鎖使用者?" msgid "Unfollow" msgstr "取消關注" -#: src/components/MutedAccountsList.tsx:178 +#: src/components/MutedAccountsList.tsx:114 #: src/components/ProfileActionMenu.tsx:361 msgid "Unmute" msgstr "解除靜音" @@ -2873,23 +3148,27 @@ msgstr "從個人資料取消釘選" msgid "Unshare" msgstr "取消轉貼" -#: src/routes/(root)/[handle]/settings/index.tsx:126 +#: src/routes/(root)/[handle]/settings/index.tsx:127 msgid "Update your profile information, including your avatar, username, display name, bio, and links." msgstr "更新您的個人資料資訊,包括頭像、使用者名稱、暱稱、個人簡介和連結。" #. placeholder {0}: new Date(edge.node.updated).toLocaleDateString() -#: src/routes/(root)/[handle]/drafts/index.tsx:304 +#: src/routes/(root)/[handle]/drafts/index.tsx:306 msgid "Updated {0}" msgstr "更新於 {0}" -#: src/components/NoteComposer.tsx:1274 +#: src/components/NoteComposer.tsx:1285 msgid "Upload progress" msgstr "上傳進度" -#: src/routes/(root)/[handle]/settings/index.tsx:585 +#: src/routes/(root)/[handle]/settings/index.tsx:586 msgid "URL" msgstr "URL" +#: src/routes/(root)/admin/news.tsx:394 +msgid "URL pattern" +msgstr "URL 模式" + #: src/components/ProfileActionMenu.tsx:277 msgid "User blocked" msgstr "已封鎖使用者" @@ -2902,17 +3181,17 @@ msgstr "已靜音使用者" msgid "User not found." msgstr "未找到使用者資訊。" -#: src/components/BlockedAccountsList.tsx:95 +#: src/components/BlockedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:259 msgid "User unblocked" msgstr "已解除封鎖使用者" -#: src/components/MutedAccountsList.tsx:95 +#: src/components/MutedAccountsList.tsx:93 #: src/components/ProfileActionMenu.tsx:304 msgid "User unmuted" msgstr "已解除靜音使用者" -#: src/routes/(root)/[handle]/settings/index.tsx:413 +#: src/routes/(root)/[handle]/settings/index.tsx:414 #: src/routes/(root)/sign/up/[token].tsx:331 msgid "Username" msgstr "使用者名稱" @@ -2933,7 +3212,7 @@ msgstr "需要使用者名稱" msgid "Username is too long. Maximum length is 15 characters." msgstr "使用者名稱太長。不能長於 15 字元。" -#: src/routes/(root)/[handle]/settings/invite.tsx:435 +#: src/routes/(root)/[handle]/settings/invite.tsx:436 msgid "Users you have invited" msgstr "您邀請的使用者" @@ -2947,7 +3226,7 @@ msgstr "已於{1}驗證此連結歸{0}所有" msgid "Verifying your invitation…" msgstr "驗證您的邀請…" -#: src/components/AppSidebar.tsx:927 +#: src/components/AppSidebar.tsx:973 msgid "View all drafts →" msgstr "查看所有草稿 →" @@ -3015,7 +3294,7 @@ msgstr "已投票" msgid "Voting…" msgstr "正在投票…" -#: src/components/NoteComposer.tsx:611 +#: src/components/NoteComposer.tsx:617 msgid "Warning" msgstr "警告" @@ -3023,11 +3302,11 @@ msgstr "警告" msgid "We couldn't reach the server" msgstr "無法連接到伺服器" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:598 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:599 msgid "We couldn't reach the translation service. Try again, or come back in a few minutes." msgstr "無法連接到翻譯服務。請重試或幾分鐘後再來。" -#: src/routes/(root)/[handle]/settings/index.tsx:566 +#: src/routes/(root)/[handle]/settings/index.tsx:567 msgid "Website" msgstr "網站" @@ -3039,7 +3318,7 @@ msgstr "歡迎來到 Hackers' Pub!請填寫以下表單完成註冊。" msgid "What is Hackers' Pub?" msgstr "什麼是 Hackers' Pub?" -#: src/components/NoteComposer.tsx:1153 +#: src/components/NoteComposer.tsx:1164 msgid "What's on your mind?" msgstr "你在想什麼?" @@ -3051,25 +3330,25 @@ msgstr "啟用後,AI 可能會自動將這篇文章翻譯成其他語言。" #~ msgid "Who can quote this note" #~ msgstr "誰可以引用此貼文" -#: src/components/AppSidebar.tsx:285 +#: src/components/AppSidebar.tsx:308 msgid "Without shares" msgstr "不含轉貼" #: src/components/article-composer/ArticleComposerForm.tsx:108 #: src/components/MarkdownEditor.tsx:161 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:423 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:424 msgid "Write" msgstr "撰寫" -#: src/components/NoteComposeModal.tsx:109 -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1004 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:384 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:462 -#: src/routes/(root)/[handle]/[noteId]/index.tsx:538 +#: src/components/NoteComposeModal.tsx:110 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/index.tsx:1005 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:385 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:463 +#: src/routes/(root)/[handle]/[noteId]/index.tsx:539 msgid "Write a reply…" msgstr "寫個回覆…" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:437 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:438 msgid "Write your article here." msgstr "在此撰寫您的文章。" @@ -3094,53 +3373,53 @@ msgstr "你已被此使用者封鎖。你無法關注該使用者或查看該使 msgid "You are blocking this user. They can't follow you or see your posts." msgstr "你正在封鎖此使用者。該使用者無法關注你或查看你的內容。" -#: src/components/NoteComposer.tsx:602 +#: src/components/NoteComposer.tsx:608 msgid "You can attach up to {MAX_MEDIA} images" msgstr "最多可附加 {MAX_MEDIA} 張圖片" -#: src/routes/(root)/[handle]/settings/index.tsx:448 +#: src/routes/(root)/[handle]/settings/index.tsx:449 msgid "You can change it only once, and the old username will become available to others." msgstr "你只能更改一次使用者名稱,而舊的使用者名稱會公開為別人使用。" -#: src/routes/(root)/[handle]/settings/index.tsx:614 +#: src/routes/(root)/[handle]/settings/index.tsx:615 msgid "You can leave this empty to remove the link." msgstr "您可以將此處留空以刪除連結。" -#: src/routes/(root)/[handle]/settings/invite.tsx:397 -#: src/routes/(root)/[handle]/settings/invite.tsx:802 +#: src/routes/(root)/[handle]/settings/invite.tsx:398 +#: src/routes/(root)/[handle]/settings/invite.tsx:803 msgid "You can leave this field empty." msgstr "你可以留空此欄位。" -#: src/routes/(root)/[handle]/drafts/new.tsx:64 +#: src/routes/(root)/[handle]/drafts/new.tsx:66 msgid "You can only create drafts for your own account" msgstr "您只能為自己的帳戶建立草稿" -#: src/routes/(root)/[handle]/drafts/[id].tsx:54 +#: src/routes/(root)/[handle]/drafts/[id].tsx:56 msgid "You can only edit your own drafts" msgstr "您只能編輯自己的草稿" -#: src/routes/(root)/[handle]/bookmarks.tsx:103 +#: src/routes/(root)/[handle]/bookmarks.tsx:104 msgid "You can only view your own bookmarks" msgstr "您只能檢視自己的收藏" -#: src/routes/(root)/[handle]/drafts/index.tsx:219 +#: src/routes/(root)/[handle]/drafts/index.tsx:221 msgid "You can only view your own drafts" msgstr "您只能檢視自己的草稿" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:404 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:405 msgid "You don't have any passkeys registered yet." msgstr "您尚未註冊任何通行金鑰。" -#: src/routes/(root)/[handle]/settings/invite.tsx:309 -#: src/routes/(root)/[handle]/settings/invite.tsx:420 +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:421 msgid "You have no invitations left. Please wait until you receive more." msgstr "你沒有剩餘邀請名額。請稍後再試。" -#: src/components/BlockedAccountsList.tsx:116 +#: src/components/BlockedAccountsList.tsx:119 msgid "You haven't blocked anyone." msgstr "你還沒有封鎖任何帳號。" -#: src/components/MutedAccountsList.tsx:113 +#: src/components/MutedAccountsList.tsx:116 msgid "You haven't muted anyone." msgstr "你還沒有靜音任何帳號。" @@ -3152,20 +3431,20 @@ msgstr "你還沒有靜音任何帳號。" msgid "You must be signed in" msgstr "請先登入" -#: src/components/NoteComposer.tsx:901 +#: src/components/NoteComposer.tsx:912 msgid "You must be signed in to create a note" msgstr "你必須登入才能建立貼文" #: src/components/article-composer/ArticleComposerContext.tsx:467 -#: src/routes/(root)/[handle]/drafts/index.tsx:192 +#: src/routes/(root)/[handle]/drafts/index.tsx:193 msgid "You must be signed in to delete a draft" msgstr "您必須登入才能刪除草稿" -#: src/components/NoteComposer.tsx:851 +#: src/components/NoteComposer.tsx:857 msgid "You must be signed in to edit a note" msgstr "編輯貼文需要登入。" -#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:356 +#: src/routes/(root)/[handle]/[idOrYear]/[slug]/edit.tsx:357 msgid "You must be signed in to edit an article" msgstr "您必須登入才能編輯文章" @@ -3185,15 +3464,15 @@ msgstr "您的邀請人" msgid "You'll automatically follow each other when you sign up." msgstr "註冊後您將自動追蹤對方。" -#: src/routes/(root)/[handle]/invite/[id].tsx:276 +#: src/routes/(root)/[handle]/invite/[id].tsx:281 msgid "You've been invited to Hackers' Pub" msgstr "你被邀請加入 Hackers' Pub" -#: src/routes/(root)/[handle]/settings/index.tsx:353 +#: src/routes/(root)/[handle]/settings/index.tsx:354 msgid "Your avatar will be displayed on your profile and in your posts. You can upload a PNG, JPEG, GIF, or WebP image up to 5 MiB in size." msgstr "你的頭像將在你的個人資料頁面和你的貼文中顯示。可以上傳大小不超過 5 MiB 的 PNG、JPEG、GIF 或 WebP 圖片。" -#: src/routes/(root)/[handle]/settings/index.tsx:492 +#: src/routes/(root)/[handle]/settings/index.tsx:493 msgid "Your bio will be displayed on your profile. You can use Markdown to format it." msgstr "你的個人簡介將在你的個人資料頁面顯示。你可以用 Markdown 文件格式化。" @@ -3209,36 +3488,36 @@ msgstr "您的網路似乎不穩定。請檢查網路後重試。" msgid "Your email address will be used to sign in to your account." msgstr "你的電子郵件地址將用於登入。" -#: src/routes/(root)/[handle]/settings/invite.tsx:400 +#: src/routes/(root)/[handle]/settings/invite.tsx:401 msgid "Your friend will see this message in the invitation email." msgstr "你的朋友將在邀請郵件中看到此訊息。" -#: src/routes/(root)/[handle]/settings/index.tsx:478 +#: src/routes/(root)/[handle]/settings/index.tsx:479 #: src/routes/(root)/sign/up/[token].tsx:393 msgid "Your name will be displayed on your profile and in your posts." msgstr "你的暱稱將在你的個人資料頁面和你的貼文中顯示。" -#: src/routes/(root)/[handle]/settings/passkeys.tsx:267 +#: src/routes/(root)/[handle]/settings/passkeys.tsx:268 msgid "Your passkey has been registered and can now be used for authentication." msgstr "您的通行金鑰已經註冊,現在可以用於驗證。" -#: src/routes/(root)/[handle]/settings/preferences.tsx:134 +#: src/routes/(root)/[handle]/settings/preferences.tsx:135 msgid "Your preferences have been updated successfully." msgstr "您的設定已成功更新。" -#: src/routes/(root)/[handle]/settings/language.tsx:148 +#: src/routes/(root)/[handle]/settings/language.tsx:149 msgid "Your preferred languages have been updated." msgstr "您的偏好語言已更新。" -#: src/routes/(root)/[handle]/settings/index.tsx:329 +#: src/routes/(root)/[handle]/settings/index.tsx:330 msgid "Your profile settings have been updated successfully." msgstr "個人資料設定已成功更新。" -#: src/components/NoteComposeModal.tsx:128 +#: src/components/NoteComposeModal.tsx:129 msgid "Your unsaved draft will be lost." msgstr "未儲存的草稿將會遺失。" -#: src/routes/(root)/[handle]/settings/index.tsx:445 +#: src/routes/(root)/[handle]/settings/index.tsx:446 #: src/routes/(root)/sign/up/[token].tsx:367 msgid "Your username will be used to create your profile URL and your fediverse handle." msgstr "你的使用者名稱將用於建立你的個人資料 URL 和你的聯邦宇宙識別碼。" diff --git a/web-next/src/routes/(root)/admin/news.tsx b/web-next/src/routes/(root)/admin/news.tsx new file mode 100644 index 000000000..9b4d1a67b --- /dev/null +++ b/web-next/src/routes/(root)/admin/news.tsx @@ -0,0 +1,518 @@ +import { A, Navigate, revalidate, useNavigate } from "@solidjs/router"; +import { graphql } from "relay-runtime"; +import { createSignal, For, Show } from "solid-js"; +import { createMutation, loadQuery, useRelayEnvironment } from "solid-relay"; +import { NarrowContainer } from "~/components/NarrowContainer.tsx"; +import { Timestamp } from "~/components/Timestamp.tsx"; +import { Title } from "~/components/Title.tsx"; +import { Button } from "~/components/ui/button.tsx"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "~/components/ui/card.tsx"; +import { + TextField, + TextFieldInput, + TextFieldLabel, +} from "~/components/ui/text-field.tsx"; +import { showToast } from "~/components/ui/toast.tsx"; +import { msg, plural, useLingui } from "~/lib/i18n/macro.d.ts"; +import { + createStablePreloadedQuery, + routePreloadedQuery, +} from "~/lib/relayPreload.ts"; +import type { newsAdminPageQuery } from "./__generated__/newsAdminPageQuery.graphql.ts"; +import type { newsAdminRecomputeMutation } from "./__generated__/newsAdminRecomputeMutation.graphql.ts"; +import type { newsAdminAddPatternMutation } from "./__generated__/newsAdminAddPatternMutation.graphql.ts"; +import type { newsAdminRemovePatternMutation } from "./__generated__/newsAdminRemovePatternMutation.graphql.ts"; +import type { newsAdminClearPenaltyMutation } from "./__generated__/newsAdminClearPenaltyMutation.graphql.ts"; + +const newsAdminPageQuery = graphql` + query newsAdminPageQuery { + viewer { + moderator + } + newsScoreStatus { + scoredLinkCount + lastRecomputedAt + } + newsExcludedPatterns { + id + pattern + note + created + } + newsPenalizedStories { + uuid + url + title + penalty + } + } +`; + +const loadNewsAdminPageQuery = routePreloadedQuery( + () => + loadQuery( + useRelayEnvironment()(), + newsAdminPageQuery, + {}, + { fetchPolicy: "network-only" }, + ), + "loadNewsAdminPageQuery", +); + +const newsAdminRecomputeMutation = graphql` + mutation newsAdminRecomputeMutation { + recomputeNewsScores { + __typename + ... on RecomputeNewsScoresPayload { + linksUpdated + status { + scoredLinkCount + lastRecomputedAt + } + } + ... on NotAuthenticatedError { + notAuthenticated + } + ... on NotAuthorizedError { + notAuthorized + } + } + } +`; + +const newsAdminAddPatternMutation = graphql` + mutation newsAdminAddPatternMutation($pattern: String!, $note: String) { + addNewsExcludedPattern(pattern: $pattern, note: $note) { + __typename + ... on NewsExcludedPattern { + id + } + ... on InvalidInputError { + inputPath + } + ... on NotAuthenticatedError { + notAuthenticated + } + ... on NotAuthorizedError { + notAuthorized + } + } + } +`; + +const newsAdminRemovePatternMutation = graphql` + mutation newsAdminRemovePatternMutation($id: UUID!) { + removeNewsExcludedPattern(id: $id) { + __typename + ... on RemoveNewsExcludedPatternPayload { + removedId + } + ... on NotAuthenticatedError { + notAuthenticated + } + ... on NotAuthorizedError { + notAuthorized + } + } + } +`; + +const newsAdminClearPenaltyMutation = graphql` + mutation newsAdminClearPenaltyMutation($id: UUID!) { + setNewsScorePenalty(id: $id, penalty: NONE) { + __typename + ... on PostLink { + uuid + } + ... on NotAuthenticatedError { + notAuthenticated + } + ... on NotAuthorizedError { + notAuthorized + } + } + } +`; + +function host(url: string): string { + try { + return new URL(url).host.replace(/^www\./, ""); + } catch { + return url; + } +} + +export default function AdminNewsPage() { + const { i18n, t } = useLingui(); + const navigate = useNavigate(); + const data = createStablePreloadedQuery( + newsAdminPageQuery, + () => loadNewsAdminPageQuery(), + ); + const [recompute] = createMutation( + newsAdminRecomputeMutation, + ); + const [addPattern] = createMutation( + newsAdminAddPatternMutation, + ); + const [removePattern] = createMutation( + newsAdminRemovePatternMutation, + ); + const [clearPenalty] = createMutation( + newsAdminClearPenaltyMutation, + ); + const [submitting, setSubmitting] = createSignal(false); + const [patternInput, setPatternInput] = createSignal(""); + const [noteInput, setNoteInput] = createSignal(""); + const [adding, setAdding] = createSignal(false); + + const refresh = () => void revalidate("loadNewsAdminPageQuery"); + const onNotAuthenticated = () => + navigate("/sign?next=%2Fadmin%2Fnews", { replace: true }); + + function onRecompute() { + setSubmitting(true); + recompute({ + variables: {}, + onCompleted(response) { + setSubmitting(false); + const result = response.recomputeNewsScores; + if (result.__typename === "RecomputeNewsScoresPayload") { + showToast({ + title: i18n._( + msg`${ + plural(result.linksUpdated!, { + one: "Recomputed # link.", + other: "Recomputed # links.", + }) + }`, + ), + }); + refresh(); + } else if (result.__typename === "NotAuthenticatedError") { + onNotAuthenticated(); + } else { + showToast({ + title: t`Not authorized to recompute news scores.`, + variant: "error", + }); + } + }, + onError(error) { + setSubmitting(false); + console.error(error); + showToast({ + title: t`Failed to recompute news scores.`, + description: import.meta.env.DEV ? error.message : undefined, + variant: "error", + }); + }, + }); + } + + function onAddPattern(event: Event) { + event.preventDefault(); + const pattern = patternInput().trim(); + if (pattern.length < 1) return; + setAdding(true); + addPattern({ + variables: { pattern, note: noteInput().trim() || null }, + onCompleted(response) { + setAdding(false); + const result = response.addNewsExcludedPattern; + if (result.__typename === "NewsExcludedPattern") { + setPatternInput(""); + setNoteInput(""); + showToast({ title: t`Exclusion pattern added.` }); + refresh(); + } else if (result.__typename === "InvalidInputError") { + showToast({ + title: t`That is not a valid URL pattern.`, + variant: "error", + }); + } else if (result.__typename === "NotAuthenticatedError") { + onNotAuthenticated(); + } else { + showToast({ + title: t`Not authorized to manage exclusions.`, + variant: "error", + }); + } + }, + onError(error) { + setAdding(false); + showToast({ + title: t`Failed to add exclusion pattern.`, + description: import.meta.env.DEV ? error.message : undefined, + variant: "error", + }); + }, + }); + } + + function onRemovePattern( + id: `${string}-${string}-${string}-${string}-${string}`, + ) { + removePattern({ + variables: { id }, + onCompleted(response) { + const result = response.removeNewsExcludedPattern; + if (result?.__typename === "RemoveNewsExcludedPatternPayload") { + showToast({ title: t`Exclusion pattern removed.` }); + refresh(); + } else if (result?.__typename === "NotAuthenticatedError") { + onNotAuthenticated(); + } else { + showToast({ + title: t`Not authorized to manage exclusions.`, + variant: "error", + }); + } + }, + onError(error) { + showToast({ + title: t`Failed to remove exclusion pattern.`, + description: import.meta.env.DEV ? error.message : undefined, + variant: "error", + }); + }, + }); + } + + function onClearPenalty( + uuid: `${string}-${string}-${string}-${string}-${string}`, + ) { + clearPenalty({ + variables: { id: uuid }, + onCompleted(response) { + const result = response.setNewsScorePenalty; + if (result?.__typename === "PostLink") { + showToast({ title: t`Penalty cleared.` }); + refresh(); + } else if (result?.__typename === "NotAuthenticatedError") { + onNotAuthenticated(); + } else { + showToast({ + title: t`Not authorized to clear penalties.`, + variant: "error", + }); + } + }, + onError(error) { + showToast({ + title: t`Failed to clear penalty.`, + description: import.meta.env.DEV ? error.message : undefined, + variant: "error", + }); + }, + }); + } + + return ( + + {t`Hackers' Pub: Admin · News`} + + {(data) => ( + + : } + > + {(_) => { + const status = () => data.newsScoreStatus; + const patterns = () => data.newsExcludedPatterns ?? []; + const penalized = () => data.newsPenalizedStories ?? []; + return ( +
+

+ {t`News`} +

+ + + + {t`Recompute news scores`} + + {t`Rebuilds the popularity score of every shared link from scratch. The operation is idempotent and safe to run at any time; scores normally stay fresh on their own, so this is mainly a manual backstop and a development tool.`} + + + +

+ {i18n._( + msg`${ + plural(status()?.scoredLinkCount ?? 0, { + one: "# link is currently in the news feed.", + other: "# links are currently in the news feed.", + }) + }`, + )} +

+

+ + {t`Last recomputed:`} + {" "} + + {(ts) => } + +

+
+ + + +
+ + + + {t`Excluded URL patterns`} + + {t`Hide links matching a URL pattern from the news feed list (every sort order). Their discussion pages stay reachable by direct link. Patterns use the URLPattern syntax, e.g. https://example.com/* or https://*.example.com/*.`} + + + +
+ + + {t`URL pattern`} + + + setPatternInput(e.currentTarget.value)} + /> + + + + {t`Note (optional)`} + + setNoteInput(e.currentTarget.value)} + /> + + +
+ 0} + fallback={ +

+ {t`No exclusion patterns yet.`} +

+ } + > +
    + + {(p) => ( +
  • +
    +

    + {p.pattern} +

    + +

    + {p.note} +

    +
    +
    + +
  • + )} +
    +
+
+
+
+ + + + {t`Penalized links`} + + {t`Links a moderator has demoted in the popular feed. Clear a penalty to restore a link's normal ranking.`} + + + + 0} + fallback={ +

+ {t`No penalized links.`} +

+ } + > +
    + + {(link) => ( +
  • +
    + + {link.title || host(link.url)} + +

    + {host(link.url)} + {" · "} + {link.penalty === "BURY" + ? t`Buried` + : t`Demoted`} +

    +
    + +
  • + )} +
    +
+
+
+
+
+ ); + }} +
+ )} +
+
+ ); +} diff --git a/web-next/src/routes/(root)/index.tsx b/web-next/src/routes/(root)/index.tsx index 9d1eb92c3..f4bce83c8 100644 --- a/web-next/src/routes/(root)/index.tsx +++ b/web-next/src/routes/(root)/index.tsx @@ -37,7 +37,7 @@ export default function Home() { - + diff --git a/web-next/src/routes/(root)/news/[link_id]/index.tsx b/web-next/src/routes/(root)/news/[link_id]/index.tsx new file mode 100644 index 000000000..0facbfb56 --- /dev/null +++ b/web-next/src/routes/(root)/news/[link_id]/index.tsx @@ -0,0 +1,94 @@ +import { type Uuid, validateUuid } from "@hackerspub/models/uuid"; +import { Title } from "@solidjs/meta"; +import { useLocation, useParams } from "@solidjs/router"; +import { graphql } from "relay-runtime"; +import { createMemo, Show } from "solid-js"; +import { loadQuery, useRelayEnvironment } from "solid-relay"; +import { NarrowContainer } from "~/components/NarrowContainer.tsx"; +import { NewsDiscussion } from "~/components/NewsDiscussion.tsx"; +import { NewsStoryHeader } from "~/components/NewsStoryHeader.tsx"; +import { NotFoundPage } from "~/components/NotFoundPage.tsx"; +import { useLingui } from "~/lib/i18n/macro.d.ts"; +import { + createStablePreloadedQuery, + routePreloadedQuery, +} from "~/lib/relayPreload.ts"; +import type { LinkIdPageQuery } from "./__generated__/LinkIdPageQuery.graphql.ts"; + +const LinkIdPageQuery = graphql` + query LinkIdPageQuery($id: UUID!) { + newsStory(id: $id) { + title + ...NewsStoryHeader_story + ...NewsDiscussion_story + } + } +`; + +const loadLinkIdPageQuery = routePreloadedQuery( + (id: Uuid) => + loadQuery(useRelayEnvironment()(), LinkIdPageQuery, { + id, + }), + "loadLinkIdPageQuery", +); + +export default function NewsDiscussionPage() { + const { t } = useLingui(); + const params = useParams(); + const location = useLocation(); + + // Resolve the `#post-` fragment so the thread can auto-expand to it. + const targetUuid = createMemo(() => { + const m = /^#post-([0-9a-f-]+)$/i.exec(location.hash); + return m?.[1] ?? null; + }); + + return ( + + } + > + + + + ); +} + +function NewsDiscussionContent(props: { + linkId: Uuid; + targetUuid: string | null; + titleFallback: string; +}) { + const data = createStablePreloadedQuery( + LinkIdPageQuery, + () => loadLinkIdPageQuery(props.linkId), + ); + + return ( + + {(data) => ( + }> + {(story) => ( + <> + + {story.title + ? `Hackers' Pub: ${story.title}` + : props.titleFallback} + +
+ +
+ + + )} +
+ )} +
+ ); +} diff --git a/web-next/src/routes/(root)/news/index.tsx b/web-next/src/routes/(root)/news/index.tsx new file mode 100644 index 000000000..00835a9fe --- /dev/null +++ b/web-next/src/routes/(root)/news/index.tsx @@ -0,0 +1,55 @@ +import { Title } from "@solidjs/meta"; +import { graphql } from "relay-runtime"; +import { Show } from "solid-js"; +import { loadQuery, useRelayEnvironment } from "solid-relay"; +import { NarrowContainer } from "~/components/NarrowContainer.tsx"; +import { NewsList } from "~/components/NewsList.tsx"; +import { useLingui } from "~/lib/i18n/macro.d.ts"; +import { + createStablePreloadedQuery, + routePreloadedQuery, +} from "~/lib/relayPreload.ts"; +import { type NewsSort, useNewsSort } from "~/lib/useNewsSort.ts"; +import type { newsPageQuery } from "./__generated__/newsPageQuery.graphql.ts"; + +const newsPageQuery = graphql` + query newsPageQuery($order: NewsOrder) { + ...NewsList_stories @arguments(order: $order) + } +`; + +const loadNewsPageQuery = routePreloadedQuery( + (order: NewsSort) => + loadQuery(useRelayEnvironment()(), newsPageQuery, { order }), + "loadNewsPageQuery", +); + +export default function NewsPage() { + const { t } = useLingui(); + const { activeSort, initialSort, buildHref } = useNewsSort("/news"); + const data = createStablePreloadedQuery( + newsPageQuery, + () => loadNewsPageQuery(initialSort), + ); + + return ( + + {t`Hackers' Pub: News`} +
+

{t`News`}

+

+ {t`Links circulating across the fediverse, ranked by how much they are being shared and discussed.`} +

+
+ + {(data) => ( + + )} + +
+ ); +}