diff --git a/package.json b/package.json index 2821d595..19bb1522 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.20.116", + "version": "0.20.117", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/js/dataHelpers.js b/src/js/dataHelpers.js index 42605d46..db1e9966 100644 --- a/src/js/dataHelpers.js +++ b/src/js/dataHelpers.js @@ -896,6 +896,23 @@ export function getLabelNameAndDescription( }; } +export function getModerationWarningLabelName(labelDefinition) { + if ( + labelDefinition.identifier === "porn" || + labelDefinition.identifier === "sexual" + ) { + return "Adult Content"; + } + return getLabelNameAndDescription(labelDefinition).name; +} + +export function isSelfLabel(label) { + const subjectDid = label.uri.startsWith("did:") + ? label.uri + : parseUri(label.uri).repo; + return label.src === subjectDid; +} + export function getLabelerForLabel(label, labelers) { const matchingLabeler = labelers.find( (labeler) => labeler.creator.did === label.src, diff --git a/src/js/preferences.js b/src/js/preferences.js index 60c56cb5..7803a863 100644 --- a/src/js/preferences.js +++ b/src/js/preferences.js @@ -431,13 +431,13 @@ export class Preferences { } getLabelDefinitionAndLabeler(label) { + const labeler = getLabelerForLabel(label, this.labelerDefs); // First check global labels const globalDef = getGlobalLabelDefinition(label.val); if (globalDef) { - return { labelDefinition: globalDef, labeler: null }; + return { labelDefinition: globalDef, labeler }; } // Then check labeler-defined labels - const labeler = getLabelerForLabel(label, this.labelerDefs); if (!labeler) return null; const labelDefinition = getDefinitionForLabel(label, labeler); if (!labelDefinition) return null; diff --git a/src/js/templates/largePost.template.js b/src/js/templates/largePost.template.js index 2b886139..84bac661 100644 --- a/src/js/templates/largePost.template.js +++ b/src/js/templates/largePost.template.js @@ -60,6 +60,7 @@ function lockedContentWarningTemplate({ post, lockedContentLabel, children }) { } return moderationWarningTemplate({ className: "post-content-warning", + label: lockedContentLabel.label, labelDefinition: lockedContentLabel.labelDefinition, labeler: lockedContentLabel.labeler, noOverride: true, diff --git a/src/js/templates/moderationWarning.template.js b/src/js/templates/moderationWarning.template.js index 2082505c..d4bfe8b1 100644 --- a/src/js/templates/moderationWarning.template.js +++ b/src/js/templates/moderationWarning.template.js @@ -1,23 +1,32 @@ import { html } from "/js/lib/lit-html.js"; -import { getLabelNameAndDescription } from "/js/dataHelpers.js"; +import { BSKY_LABELER_DID } from "/js/config.js"; +import { getModerationWarningLabelName, isSelfLabel } from "/js/dataHelpers.js"; import { linkToLabeler } from "/js/navigation.js"; import { classnames } from "/js/utils.js"; export function moderationWarningTemplate({ className = "", isAuthorLabel, + label, labelDefinition, labeler, noOverride, children, }) { - let { name: labelName } = getLabelNameAndDescription(labelDefinition); + let labelName = getModerationWarningLabelName(labelDefinition); if (isAuthorLabel) { labelName += " (Account)"; } - // If no labeler, the labeler is the author of the post - const labelerName = labeler ? "@" + labeler.creator.handle : "the author."; - const labelerLink = labeler ? linkToLabeler(labeler) : null; + let labelerName = "an unknown labeler"; + let labelerLink = null; + if (isSelfLabel(label)) { + labelerName = "the author."; + } else if (labeler) { + labelerName = "@" + labeler.creator.handle; + labelerLink = linkToLabeler(labeler); + } else if (label.src === BSKY_LABELER_DID) { + labelerName = "moderation.bsky.app"; + } return html` { assert.deepEqual(result.labeler, null); }); + it("should resolve the labeler for a global label from a loaded labeler", () => { + const labelerDefs = [ + { + creator: { did: "did:plc:modservice", handle: "mod.test" }, + policies: { labelValueDefinitions: [] }, + }, + ]; + const preferences = new Preferences([], labelerDefs); + const post = { + labels: [{ src: "did:plc:modservice", val: "!warn" }], + }; + const result = preferences.getContentLabel(post); + + assert.deepEqual(result.labeler, labelerDefs[0]); + }); + it("should not allow user to override !hide visibility", () => { // User tries to set !hide to "ignore" - should still be "hide" const obj = [ diff --git a/tests/unit/specs/templates/moderationWarning.template.test.js b/tests/unit/specs/templates/moderationWarning.template.test.js index b27e8e1e..e8af5581 100644 --- a/tests/unit/specs/templates/moderationWarning.template.test.js +++ b/tests/unit/specs/templates/moderationWarning.template.test.js @@ -10,6 +10,12 @@ const mockLabelDefinition = { locales: [{ lang: "en", name: "NSFW", description: "Adult content" }], }; +const mockLabel = { + src: "did:plc:testlabeler", + uri: "at://did:plc:author/app.bsky.feed.post/abc", + val: "nsfw", +}; + const mockLabeler = { uri: "at://did:plc:testlabeler/app.bsky.labeler.service/self", creator: { @@ -22,6 +28,7 @@ const mockLabeler = { describe("moderationWarningTemplate", () => { it("should render moderation-warning element", () => { const result = moderationWarningTemplate({ + label: mockLabel, labelDefinition: mockLabelDefinition, labeler: mockLabeler, children: html`
Hidden content
`, @@ -33,6 +40,7 @@ describe("moderationWarningTemplate", () => { it("should set label attribute from label definition", () => { const result = moderationWarningTemplate({ + label: mockLabel, labelDefinition: mockLabelDefinition, labeler: mockLabeler, children: html`
Hidden content
`, @@ -43,8 +51,28 @@ describe("moderationWarningTemplate", () => { assert.deepEqual(warning.getAttribute("label"), "NSFW"); }); + it("should label sexual and porn warnings as Adult Content", () => { + for (const identifier of ["sexual", "porn"]) { + const result = moderationWarningTemplate({ + label: mockLabel, + labelDefinition: { + ...mockLabelDefinition, + identifier, + locales: [{ lang: "en", name: "Sexually Suggestive" }], + }, + labeler: mockLabeler, + children: html`
Hidden content
`, + }); + const container = document.createElement("div"); + render(result, container); + const warning = container.querySelector("moderation-warning"); + assert.deepEqual(warning.getAttribute("label"), "Adult Content"); + } + }); + it("should set labelerName attribute with @ prefix", () => { const result = moderationWarningTemplate({ + label: mockLabel, labelDefinition: mockLabelDefinition, labeler: mockLabeler, children: html`
Hidden content
`, @@ -57,6 +85,7 @@ describe("moderationWarningTemplate", () => { it("should set labelerLink attribute", () => { const result = moderationWarningTemplate({ + label: mockLabel, labelDefinition: mockLabelDefinition, labeler: mockLabeler, children: html`
Hidden content
`, @@ -69,6 +98,7 @@ describe("moderationWarningTemplate", () => { it("should render children content", () => { const result = moderationWarningTemplate({ + label: mockLabel, labelDefinition: mockLabelDefinition, labeler: mockLabeler, children: html`
Hidden content
`, @@ -79,28 +109,55 @@ describe("moderationWarningTemplate", () => { }); }); -describe("moderationWarningTemplate - no labeler", () => { - it("should show 'the author.' when labeler is null", () => { - const result = moderationWarningTemplate({ - labelDefinition: mockLabelDefinition, +describe("moderationWarningTemplate - labeler attribution", () => { + function renderWarning({ label, labeler }) { + const container = document.createElement("div"); + render( + moderationWarningTemplate({ + label, + labelDefinition: mockLabelDefinition, + labeler, + children: html`
Hidden content
`, + }), + container, + ); + return container.querySelector("moderation-warning"); + } + + it("should attribute a post self-label to the author", () => { + const warning = renderWarning({ + label: { ...mockLabel, src: "did:plc:author" }, labeler: null, - children: html`
Hidden content
`, }); - const container = document.createElement("div"); - render(result, container); - const warning = container.querySelector("moderation-warning"); assert.deepEqual(warning.getAttribute("labelerName"), "the author."); }); - it("should have null labelerLink when labeler is null", () => { - const result = moderationWarningTemplate({ - labelDefinition: mockLabelDefinition, + it("should attribute an account self-label to the author", () => { + const warning = renderWarning({ + label: { ...mockLabel, src: "did:plc:author", uri: "did:plc:author" }, labeler: null, - children: html`
Hidden content
`, }); - const container = document.createElement("div"); - render(result, container); - const warning = container.querySelector("moderation-warning"); + assert.deepEqual(warning.getAttribute("labelerName"), "the author."); + }); + + it("should attribute an unloaded Bluesky moderation label to moderation.bsky.app", () => { + const warning = renderWarning({ + label: { ...mockLabel, src: "did:plc:ar7c4by46qjdydhdevvrndac" }, + labeler: null, + }); + assert.deepEqual( + warning.getAttribute("labelerName"), + "moderation.bsky.app", + ); + }); + + it("should attribute a label from an unloaded labeler to an unknown labeler", () => { + const warning = renderWarning({ label: mockLabel, labeler: null }); + assert.deepEqual(warning.getAttribute("labelerName"), "an unknown labeler"); + }); + + it("should not link the labeler when the labeler is not loaded", () => { + const warning = renderWarning({ label: mockLabel, labeler: null }); // When labelerLink is null, the attribute value is empty or "null" string const labelerLink = warning.getAttribute("labelerLink"); assert( @@ -113,6 +170,7 @@ describe("moderationWarningTemplate - custom className", () => { it("should apply custom className", () => { const result = moderationWarningTemplate({ className: "custom-warning", + label: mockLabel, labelDefinition: mockLabelDefinition, labeler: mockLabeler, children: html`
Hidden content
`, @@ -127,6 +185,7 @@ describe("moderationWarningTemplate - custom className", () => { it("should keep post-moderation-warning class with custom className", () => { const result = moderationWarningTemplate({ className: "custom-warning", + label: mockLabel, labelDefinition: mockLabelDefinition, labeler: mockLabeler, children: html`
Hidden content
`, @@ -142,6 +201,7 @@ describe("moderationWarningTemplate - custom className", () => { describe("moderationWarningTemplate - noOverride", () => { it("should set the no-override attribute when noOverride is true", () => { const result = moderationWarningTemplate({ + label: mockLabel, labelDefinition: mockLabelDefinition, labeler: mockLabeler, noOverride: true, @@ -155,6 +215,7 @@ describe("moderationWarningTemplate - noOverride", () => { it("should not set the no-override attribute when noOverride is false", () => { const result = moderationWarningTemplate({ + label: mockLabel, labelDefinition: mockLabelDefinition, labeler: mockLabeler, noOverride: false, diff --git a/tests/unit/specs/templates/postEmbed.template.test.js b/tests/unit/specs/templates/postEmbed.template.test.js index 5a3c4222..cca97899 100644 --- a/tests/unit/specs/templates/postEmbed.template.test.js +++ b/tests/unit/specs/templates/postEmbed.template.test.js @@ -415,7 +415,11 @@ describe("postEmbedTemplate - external links", () => { }; const mediaLabel = { visibility: "hide", - label: { val: "porn", src: "did:plc:labeler" }, + label: { + val: "porn", + src: "did:plc:labeler", + uri: "at://did:plc:author/app.bsky.feed.post/abc", + }, labelDefinition: { identifier: "porn", blurs: "media",