diff --git a/.gitignore b/.gitignore index cacdc217f..ca850a866 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .claude/settings.local.json .DS_Store +.playwright-mcp/ .pnpm-store/ .vocab-codegen.lock/ dist/ @@ -12,6 +13,7 @@ test/smoke/mastodon/.certs/ test/smoke/mastodon/mastodon.env test/smoke/sharkey/.certs/ test/smoke/sharkey/sharkey.env +tmp/ smoke.log t.ts t2.ts diff --git a/docs/manual/pragmatics.md b/docs/manual/pragmatics.md index c5b9da747..fdebac3ca 100644 --- a/docs/manual/pragmatics.md +++ b/docs/manual/pragmatics.md @@ -418,3 +418,317 @@ Mastodon](pragmatics/mastodon-followers.png) > [!NOTE] > Mastodon does not display the followers collection of a remote actor, > but other ActivityPub implementations may display it. + + +Objects +------- + +The following types of objects are commonly used to represent posts and other +public-facing content in the fediverse: + + - `Article` represents a multi-paragraph written work. + - `Note` is a short post. + - `Question` is a poll. + +Link-like objects such as `Mention`, `Hashtag`, and `Emoji` are usually attached +to these objects through their `tags` property. The exact way ActivityPub +implementations render these objects differs, but Mastodon and Misskey already +share a number of de facto conventions. + +### `Note`: Short posts + +The `Note` type is the most common object type for short posts. In Mastodon, +the `content` property becomes the post body, and `attachments` are rendered +below the body. + +~~~~ typescript twoslash +import { Hashtag, Image, Mention, Note } from "@fedify/vocab"; +// ---cut-before--- +new Note({ + content: // [!code highlight] + '
Hello ' + + '@friend@example.com! This note demonstrates ' + + '#fedify.
', + attachments: [ // [!code highlight] + new Image({ + url: new URL("https://picsum.photos/id/237/1200/800"), + mediaType: "image/jpeg", + name: "A placeholder dog photo", + }), + ], + tags: [ + new Mention({ + href: new URL("https://example.com/users/friend"), + name: "@friend@example.com", + }), + new Hashtag({ + href: new URL("https://example.com/tags/fedify"), + name: "#fedify", + }), + ], +}) +~~~~ + +> [!NOTE] +> The `content` property expects an HTML string. If it contains characters +> like `<`, `>`, and `&`, you should escape HTML entities. + +For example, the above `Note` object is displayed like the following in +Mastodon: + + + +### `summary`: Content warnings + +On `Note` objects, the `summary` property is commonly used as a content +warning. In Mastodon, it becomes the warning text shown above the collapsed +post body. + +~~~~ typescript twoslash +import { Note } from "@fedify/vocab"; +// ---cut-before--- +new Note({ + summary: "CW: Rendering pragmatics demo", // [!code highlight] + content: "Hello @friend@example.com! This note demonstrates #fedify.
", +}) +~~~~ + +> [!NOTE] +> The `summary` property also expects an HTML string. + +For example, the above `summary` is displayed like the following in Mastodon: + + + +### `Article`: Long-form posts + +The `Article` type is commonly used for long-form posts such as blog entries. +In Mastodon, the `name` property is displayed as the title, the `url` property +is shown as the canonical link, and `tags` can still surface hashtags below the +post body. + +~~~~ typescript twoslash +import { Article, Hashtag } from "@fedify/vocab"; +// ---cut-before--- +new Article({ + name: "Pragmatics of `Article` objects", // [!code highlight] + url: new URL("https://example.com/blog/pragmatics-article"), // [!code highlight] + content: // [!code highlight] + "This article demonstrates how a long-form object can expose a title " + + "and a canonical permalink.
", + tags: [ // [!code highlight] + new Hashtag({ + href: new URL("https://example.com/tags/activitypub"), + name: "#activitypub", + }), + ], +}) +~~~~ + +> [!NOTE] +> Many ActivityPub implementations render `Article` objects more compactly than +> `Note` objects. If you want a title-like presentation, populate the `name` +> property instead of relying on the body alone. + +For example, the above `Article` object is displayed like the following in +Mastodon: + + + +### `Question`: Polls + +The `Question` type is used for polls. In Mastodon, the question body comes +from `content`, the poll choices come from `exclusiveOptions` or +`inclusiveOptions`, and metadata such as `voters` and `closed` are displayed +below the choices. + +### `exclusiveOptions`: Single-choice polls + +~~~~ typescript twoslash +import { Collection, Note, Question } from "@fedify/vocab"; +import { Temporal } from "@js-temporal/polyfill"; +// ---cut-before--- +new Question({ + content: "Which pragmatics example should the manual explain first?
", // [!code highlight] + exclusiveOptions: [ // [!code highlight] + new Note({ name: "A short note", replies: new Collection({ totalItems: 4 }) }), + new Note({ name: "A long article", replies: new Collection({ totalItems: 2 }) }), + new Note({ + name: "A poll question", + replies: new Collection({ totalItems: 7 }), + }), + ], + voters: 13, // [!code highlight] + closed: Temporal.Instant.from("2026-04-28T12:00:00Z"), // [!code highlight] +}) +~~~~ + +> [!NOTE] +> Use `exclusiveOptions` for single-choice polls. A `Question` object should +> not contain both `exclusiveOptions` and `inclusiveOptions`. + +When Mastodon shows poll results, each option's `replies.totalItems` value is +used as that option's vote count. In the above example, the values 4, 2, and 7 +add up to 13, which matches `voters` and yields the percentages shown in the +results view. + +For example, the above `Question` object is displayed like the following in +Mastodon after opening the results view: + + + +### `inclusiveOptions`: Multiple-choice polls + +Use `inclusiveOptions` for polls where a voter may choose more than one option. + +~~~~ typescript twoslash +import { Collection, Note, Question } from "@fedify/vocab"; +import { Temporal } from "@js-temporal/polyfill"; +// ---cut-before--- +new Question({ + content: "Which pragmatics details should the manual cover next?
", // [!code highlight] + inclusiveOptions: [ // [!code highlight] + new Note({ + name: "Content warnings", + replies: new Collection({ totalItems: 6 }), + }), + new Note({ + name: "Custom emoji", + replies: new Collection({ totalItems: 8 }), + }), + new Note({ + name: "Poll results", + replies: new Collection({ totalItems: 3 }), + }), + ], + voters: 13, // [!code highlight] + closed: Temporal.Instant.from("2026-04-20T12:00:00Z"), // [!code highlight] +}) +~~~~ + +In Mastodon, the `replies.totalItems` values on `inclusiveOptions` still become +the per-option counts in the results view, but unlike a single-choice poll they +do not need to add up to `voters`, because each voter may select more than one +option. + +For example, the above multiple-choice `Question` object is initially displayed +like the following in Mastodon: + + + +After opening the results view, Mastodon shows the per-option counts derived +from `replies.totalItems`: + + + +### `Mention`: Mentioned accounts + +The `Mention` type is usually placed in an object's `tags` property to indicate +that a link in `content` points to another actor. In Mastodon, this makes the +linked handle render as a mention of the remote account. + +~~~~ typescript twoslash +import { Mention, Note } from "@fedify/vocab"; +// ---cut-before--- +new Note({ + content: // [!code highlight] + 'Hello ' + + '@friend@example.com!
', + tags: [ // [!code highlight] + new Mention({ + href: new URL("https://example.com/users/friend"), // [!code highlight] + name: "@friend@example.com", // [!code highlight] + }), + ], +}) +~~~~ + +> [!NOTE] +> In practice, you should keep the anchor in `content` and the `Mention` object +> in `tags` aligned with each other. If they point to different actors, +> ActivityPub implementations may render or notify inconsistently. +> For Mastodon-compatible mention rendering, the anchor in `content` should use +> `class="mention u-url"` so the link is treated as a mention instead of a +> generic profile URL. + +For example, the above `Mention` object is displayed like the following in +Mastodon: + + + +### `Hashtag`: Clickable hashtags + +The `Hashtag` type is also commonly placed in `tags`. In Mastodon, it turns a +matching hashtag in `content` into a clickable tag link. + +~~~~ typescript twoslash +import { Hashtag, Note } from "@fedify/vocab"; +// ---cut-before--- +new Note({ + content: // [!code highlight] + 'This note demonstrates ' + + '#fedify.
', + tags: [ // [!code highlight] + new Hashtag({ + href: new URL("https://example.com/tags/fedify"), // [!code highlight] + name: "#fedify", // [!code highlight] + }), + ], +}) +~~~~ + +> [!NOTE] +> For Mastodon-compatible hashtag rendering, the anchor in `content` should use +> `class="mention hashtag"` and `rel="tag"`. Without those attributes, +> Mastodon is more likely to treat it as a plain link instead of a hashtag. + +For example, the above `Hashtag` object is displayed like the following in +Mastodon: + + + +If you click the hashtag in Mastodon, it opens a hashtag-specific menu instead +of behaving like a plain external link: + + + +### `Emoji`: Custom emoji + +The `Emoji` type represents a custom emoji. In Mastodon, the shortcode in +`content` is replaced with the image from `icon` when a matching `Emoji` +object is present in `tags`. + +~~~~ typescript twoslash +import { Emoji, Image, Note } from "@fedify/vocab"; +// ---cut-before--- +new Note({ + content: "Let us celebrate with :fedify_party:.
", // [!code highlight] + tags: [ // [!code highlight] + new Emoji({ + name: ":fedify_party:", // [!code highlight] + icon: new Image({ + url: new URL("https://example.com/emojis/fedify-party.png"), // [!code highlight] + mediaType: "image/png", // [!code highlight] + }), + }), + ], +}) +~~~~ + +> [!NOTE] +> The shortcode in `content` should match `Emoji.name` exactly, usually in the +> `:shortcode:` form. If there is no matching `Emoji` object in `tags`, the +> shortcode is displayed as plain text. + +For example, the above `Emoji` object is displayed like the following in +Mastodon: + + diff --git a/docs/manual/pragmatics/mastodon-article.png b/docs/manual/pragmatics/mastodon-article.png new file mode 100644 index 000000000..d58dd8512 Binary files /dev/null and b/docs/manual/pragmatics/mastodon-article.png differ diff --git a/docs/manual/pragmatics/mastodon-emoji.png b/docs/manual/pragmatics/mastodon-emoji.png new file mode 100644 index 000000000..0bca4dffa Binary files /dev/null and b/docs/manual/pragmatics/mastodon-emoji.png differ diff --git a/docs/manual/pragmatics/mastodon-hashtag-dropdown.png b/docs/manual/pragmatics/mastodon-hashtag-dropdown.png new file mode 100644 index 000000000..776d0ae1d Binary files /dev/null and b/docs/manual/pragmatics/mastodon-hashtag-dropdown.png differ diff --git a/docs/manual/pragmatics/mastodon-hashtag.png b/docs/manual/pragmatics/mastodon-hashtag.png new file mode 100644 index 000000000..4dd93583e Binary files /dev/null and b/docs/manual/pragmatics/mastodon-hashtag.png differ diff --git a/docs/manual/pragmatics/mastodon-mention.png b/docs/manual/pragmatics/mastodon-mention.png new file mode 100644 index 000000000..b2e1f2615 Binary files /dev/null and b/docs/manual/pragmatics/mastodon-mention.png differ diff --git a/docs/manual/pragmatics/mastodon-note-body.png b/docs/manual/pragmatics/mastodon-note-body.png new file mode 100644 index 000000000..d848fd1c1 Binary files /dev/null and b/docs/manual/pragmatics/mastodon-note-body.png differ diff --git a/docs/manual/pragmatics/mastodon-note.png b/docs/manual/pragmatics/mastodon-note.png new file mode 100644 index 000000000..d864d1b8d Binary files /dev/null and b/docs/manual/pragmatics/mastodon-note.png differ diff --git a/docs/manual/pragmatics/mastodon-question-multi-choices.png b/docs/manual/pragmatics/mastodon-question-multi-choices.png new file mode 100644 index 000000000..b26aa7b9e Binary files /dev/null and b/docs/manual/pragmatics/mastodon-question-multi-choices.png differ diff --git a/docs/manual/pragmatics/mastodon-question-multi.png b/docs/manual/pragmatics/mastodon-question-multi.png new file mode 100644 index 000000000..33cfbc55c Binary files /dev/null and b/docs/manual/pragmatics/mastodon-question-multi.png differ diff --git a/docs/manual/pragmatics/mastodon-question.png b/docs/manual/pragmatics/mastodon-question.png new file mode 100644 index 000000000..d88b1155b Binary files /dev/null and b/docs/manual/pragmatics/mastodon-question.png differ