diff --git a/commons/src/interfaces/submission/default-options.interface.ts b/commons/src/interfaces/submission/default-options.interface.ts index c46b577c..b9220ffb 100644 --- a/commons/src/interfaces/submission/default-options.interface.ts +++ b/commons/src/interfaces/submission/default-options.interface.ts @@ -7,6 +7,7 @@ export interface DefaultOptions { tags: TagData; description: DescriptionData; rating?: SubmissionRating | string; + spoilerText?: string; sources: string[]; } diff --git a/commons/src/interfaces/websites/itaku/itaku.file.options.interface.ts b/commons/src/interfaces/websites/itaku/itaku.file.options.interface.ts index 4ae6a4cc..8202d390 100644 --- a/commons/src/interfaces/websites/itaku/itaku.file.options.interface.ts +++ b/commons/src/interfaces/websites/itaku/itaku.file.options.interface.ts @@ -5,4 +5,5 @@ export interface ItakuFileOptions extends DefaultFileOptions { visibility: string; shareOnFeed: boolean; spoilerText?: string; + spoilerTextOverwrite?: boolean; } diff --git a/commons/src/interfaces/websites/itaku/itaku.notification.options.interface.ts b/commons/src/interfaces/websites/itaku/itaku.notification.options.interface.ts index 0f5d9bf6..dad8447e 100644 --- a/commons/src/interfaces/websites/itaku/itaku.notification.options.interface.ts +++ b/commons/src/interfaces/websites/itaku/itaku.notification.options.interface.ts @@ -4,4 +4,5 @@ export interface ItakuNotificationOptions extends DefaultOptions { folders: string[]; visibility: string; spoilerText?: string; + spoilerTextOverwrite?: boolean; } diff --git a/commons/src/interfaces/websites/mastodon/mastodon.file.options.interface.ts b/commons/src/interfaces/websites/mastodon/mastodon.file.options.interface.ts index ecbe86a1..96354e72 100644 --- a/commons/src/interfaces/websites/mastodon/mastodon.file.options.interface.ts +++ b/commons/src/interfaces/websites/mastodon/mastodon.file.options.interface.ts @@ -3,6 +3,7 @@ import { DefaultFileOptions } from '../../submission/default-options.interface'; export interface MastodonFileOptions extends DefaultFileOptions { useTitle: boolean; spoilerText?: string; + spoilerTextOverwrite?: boolean; visibility: string; altText?: string; replyToUrl?: string; diff --git a/commons/src/interfaces/websites/mastodon/mastodon.notification.options.interface.ts b/commons/src/interfaces/websites/mastodon/mastodon.notification.options.interface.ts index 610d083f..9cedf5f5 100644 --- a/commons/src/interfaces/websites/mastodon/mastodon.notification.options.interface.ts +++ b/commons/src/interfaces/websites/mastodon/mastodon.notification.options.interface.ts @@ -3,6 +3,7 @@ import { DefaultOptions } from '../../submission/default-options.interface'; export interface MastodonNotificationOptions extends DefaultOptions { useTitle: boolean; spoilerText?: string; + spoilerTextOverwrite?: boolean; visibility: string; replyToUrl?: string; } diff --git a/commons/src/interfaces/websites/misskey/misskey.file.options.interface.ts b/commons/src/interfaces/websites/misskey/misskey.file.options.interface.ts index 184683ed..22abe618 100644 --- a/commons/src/interfaces/websites/misskey/misskey.file.options.interface.ts +++ b/commons/src/interfaces/websites/misskey/misskey.file.options.interface.ts @@ -3,6 +3,7 @@ import { DefaultFileOptions } from '../../submission/default-options.interface'; export interface MissKeyFileOptions extends DefaultFileOptions { useTitle: boolean; spoilerText?: string; + spoilerTextOverwrite?: boolean; visibility: string; altText?: string; } diff --git a/commons/src/interfaces/websites/misskey/misskey.notification.options.interface.ts b/commons/src/interfaces/websites/misskey/misskey.notification.options.interface.ts index 34de52ca..791c2d6d 100644 --- a/commons/src/interfaces/websites/misskey/misskey.notification.options.interface.ts +++ b/commons/src/interfaces/websites/misskey/misskey.notification.options.interface.ts @@ -3,5 +3,6 @@ import { DefaultOptions } from '../../submission/default-options.interface'; export interface MissKeyNotificationOptions extends DefaultOptions { useTitle: boolean; spoilerText?: string; + spoilerTextOverwrite?: boolean; visibility: string; } diff --git a/commons/src/interfaces/websites/pixelfed/pixelfed.file.options.interface.ts b/commons/src/interfaces/websites/pixelfed/pixelfed.file.options.interface.ts index e4bebe8a..d597b1c4 100644 --- a/commons/src/interfaces/websites/pixelfed/pixelfed.file.options.interface.ts +++ b/commons/src/interfaces/websites/pixelfed/pixelfed.file.options.interface.ts @@ -3,6 +3,7 @@ import { DefaultFileOptions } from '../../submission/default-options.interface'; export interface PixelfedFileOptions extends DefaultFileOptions { useTitle: boolean; spoilerText?: string; + spoilerTextOverwrite?: boolean; visibility: string; altText?: string; } diff --git a/commons/src/interfaces/websites/pleroma/pleroma.file.options.interface.ts b/commons/src/interfaces/websites/pleroma/pleroma.file.options.interface.ts index dd4fe0ca..7362b99b 100644 --- a/commons/src/interfaces/websites/pleroma/pleroma.file.options.interface.ts +++ b/commons/src/interfaces/websites/pleroma/pleroma.file.options.interface.ts @@ -3,6 +3,7 @@ import { DefaultFileOptions } from '../../submission/default-options.interface'; export interface PleromaFileOptions extends DefaultFileOptions { useTitle: boolean; spoilerText?: string; + spoilerTextOverwrite?: boolean; visibility: string; altText?: string; replyToUrl?: string; diff --git a/commons/src/interfaces/websites/pleroma/pleroma.notification.options.interface.ts b/commons/src/interfaces/websites/pleroma/pleroma.notification.options.interface.ts index a674afb0..746ca673 100644 --- a/commons/src/interfaces/websites/pleroma/pleroma.notification.options.interface.ts +++ b/commons/src/interfaces/websites/pleroma/pleroma.notification.options.interface.ts @@ -3,6 +3,7 @@ import { DefaultOptions } from '../../submission/default-options.interface'; export interface PleromaNotificationOptions extends DefaultOptions { useTitle: boolean; spoilerText?: string; + spoilerTextOverwrite?: boolean; visibility: string; replyToUrl?: string; } diff --git a/commons/src/models/default-options.entity.ts b/commons/src/models/default-options.entity.ts index 2a8f66c1..56c4b3a8 100644 --- a/commons/src/models/default-options.entity.ts +++ b/commons/src/models/default-options.entity.ts @@ -30,6 +30,10 @@ export class DefaultOptionsEntity implements DefaultOptions { @IsOptional() rating?: SubmissionRating | string; + @Expose() + @IsOptional() + spoilerText?: string; + @IsArray() sources: string[]; diff --git a/commons/src/websites/itaku/itaku.file.options.ts b/commons/src/websites/itaku/itaku.file.options.ts index cdc7d222..3e667912 100644 --- a/commons/src/websites/itaku/itaku.file.options.ts +++ b/commons/src/websites/itaku/itaku.file.options.ts @@ -26,6 +26,11 @@ export class ItakuFileOptionsEntity extends DefaultFileOptionsEntity implements @IsOptional() spoilerText?: string; + @Expose() + @IsBoolean() + @IsOptional() + spoilerTextOverwrite?: boolean; + constructor(entity?: Partial) { super(entity as DefaultFileOptions); } diff --git a/commons/src/websites/itaku/itaku.notification.options.ts b/commons/src/websites/itaku/itaku.notification.options.ts index 040476ee..60bf93a3 100644 --- a/commons/src/websites/itaku/itaku.notification.options.ts +++ b/commons/src/websites/itaku/itaku.notification.options.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { IsArray, IsString, IsOptional } from 'class-validator'; +import { IsArray, IsString, IsOptional, IsBoolean } from 'class-validator'; import { DefaultOptions } from '../../interfaces/submission/default-options.interface'; import { ItakuNotificationOptions } from '../../interfaces/websites/itaku/itaku.notification.options.interface'; import { DefaultValue } from '../../models/decorators/default-value.decorator'; @@ -24,6 +24,11 @@ export class ItakuNotificationOptionsEntity @IsOptional() spoilerText?: string; + @Expose() + @IsBoolean() + @IsOptional() + spoilerTextOverwrite?: boolean; + constructor(entity?: Partial) { super(entity as DefaultOptions); } diff --git a/commons/src/websites/mastodon/mastodon.file.options.ts b/commons/src/websites/mastodon/mastodon.file.options.ts index 55eef672..08078468 100644 --- a/commons/src/websites/mastodon/mastodon.file.options.ts +++ b/commons/src/websites/mastodon/mastodon.file.options.ts @@ -19,6 +19,11 @@ export class MastodonFileOptionsEntity @IsString() spoilerText?: string; + @Expose() + @IsBoolean() + @IsOptional() + spoilerTextOverwrite?: boolean; + @Expose() @IsString() @DefaultValue('public') diff --git a/commons/src/websites/mastodon/mastodon.notification.options.ts b/commons/src/websites/mastodon/mastodon.notification.options.ts index 485f3ba2..ded8441c 100644 --- a/commons/src/websites/mastodon/mastodon.notification.options.ts +++ b/commons/src/websites/mastodon/mastodon.notification.options.ts @@ -19,6 +19,11 @@ export class MastodonNotificationOptionsEntity @IsString() spoilerText?: string; + @Expose() + @IsBoolean() + @IsOptional() + spoilerTextOverwrite?: boolean; + @Expose() @IsString() @DefaultValue('public') diff --git a/commons/src/websites/misskey/misskey.file.options.ts b/commons/src/websites/misskey/misskey.file.options.ts index f85e9465..76664b1c 100644 --- a/commons/src/websites/misskey/misskey.file.options.ts +++ b/commons/src/websites/misskey/misskey.file.options.ts @@ -19,6 +19,11 @@ export class MissKeyFileOptionsEntity @IsString() spoilerText?: string; + @Expose() + @IsBoolean() + @IsOptional() + spoilerTextOverwrite?: boolean; + @Expose() @IsString() @DefaultValue('public') diff --git a/commons/src/websites/misskey/misskey.notification.options.ts b/commons/src/websites/misskey/misskey.notification.options.ts index 5c625f68..f6f18475 100644 --- a/commons/src/websites/misskey/misskey.notification.options.ts +++ b/commons/src/websites/misskey/misskey.notification.options.ts @@ -19,6 +19,11 @@ export class MissKeyNotificationOptionsEntity @IsString() spoilerText?: string; + @Expose() + @IsBoolean() + @IsOptional() + spoilerTextOverwrite?: boolean; + @Expose() @IsString() @DefaultValue('public') diff --git a/commons/src/websites/pixelfed/pixelfed.file.options.ts b/commons/src/websites/pixelfed/pixelfed.file.options.ts index 800403f3..174a4269 100644 --- a/commons/src/websites/pixelfed/pixelfed.file.options.ts +++ b/commons/src/websites/pixelfed/pixelfed.file.options.ts @@ -19,6 +19,11 @@ export class PixelfedFileOptionsEntity @IsString() spoilerText?: string; + @Expose() + @IsBoolean() + @IsOptional() + spoilerTextOverwrite?: boolean; + @Expose() @IsString() @DefaultValue('public') diff --git a/commons/src/websites/pleroma/pleroma.file.options.ts b/commons/src/websites/pleroma/pleroma.file.options.ts index 94d559db..761d0a74 100644 --- a/commons/src/websites/pleroma/pleroma.file.options.ts +++ b/commons/src/websites/pleroma/pleroma.file.options.ts @@ -16,7 +16,12 @@ export class PleromaFileOptionsEntity extends DefaultFileOptionsEntity @IsOptional() @IsString() spoilerText?: string; - + + @Expose() + @IsBoolean() + @IsOptional() + spoilerTextOverwrite?: boolean; + @Expose() @IsString() @DefaultValue('public') diff --git a/commons/src/websites/pleroma/pleroma.notification.options.ts b/commons/src/websites/pleroma/pleroma.notification.options.ts index b39ca0ab..2350c943 100644 --- a/commons/src/websites/pleroma/pleroma.notification.options.ts +++ b/commons/src/websites/pleroma/pleroma.notification.options.ts @@ -17,6 +17,11 @@ export class PleromaNotificationOptionsEntity extends DefaultOptionsEntity @IsString() spoilerText?: string; + @Expose() + @IsBoolean() + @IsOptional() + spoilerTextOverwrite?: boolean; + @Expose() @IsString() @DefaultValue('public') diff --git a/electron-app/src/server/submission/parser/parser.service.ts b/electron-app/src/server/submission/parser/parser.service.ts index e9eabd3c..c7ebc692 100644 --- a/electron-app/src/server/submission/parser/parser.service.ts +++ b/electron-app/src/server/submission/parser/parser.service.ts @@ -68,6 +68,7 @@ export class ParserService { submission, tags, title: this.getTitle(submission, defaultPart, websitePart), + spoilerText: this.getSpoilerText(defaultPart, websitePart), }; if (this.isFileSubmission(submission)) { @@ -127,6 +128,22 @@ export class ParserService { return (websitePart.data.title || defaultPart.data.title || submission.title).substring(0, 160); } + private getSpoilerText( + defaultPart: SubmissionPartEntity, + websitePart: SubmissionPartEntity, + ): string { + const overwrite = websitePart.data.spoilerTextOverwrite; + const defaultSpoilerText = defaultPart.data.spoilerText || ''; + const websiteSpoilerText = `${websitePart.data.spoilerText || ''}`; + if (overwrite === undefined) { + return websiteSpoilerText.trim() === '' ? defaultSpoilerText : websiteSpoilerText; + } else if (overwrite) { + return websiteSpoilerText; + } else { + return defaultSpoilerText; + } + } + private isFileSubmission(submission: Submission): submission is FileSubmission { return submission instanceof FileSubmissionEntity; } diff --git a/electron-app/src/server/submission/parser/section-parsers/description.parser.ts b/electron-app/src/server/submission/parser/section-parsers/description.parser.ts index 9d37de07..b43562f3 100644 --- a/electron-app/src/server/submission/parser/section-parsers/description.parser.ts +++ b/electron-app/src/server/submission/parser/section-parsers/description.parser.ts @@ -53,7 +53,7 @@ export class DescriptionParser { ).trim(); if (description.length) { - // Insert {default}, {title}, {tags} shortcuts + // Insert {default}, {title}, {tags}, {cw} shortcuts let tags = await this.parserService.parseTags(website, defaultPart, websitePart); description = this.insertDefaultShortcuts(description, [ { @@ -68,6 +68,10 @@ export class DescriptionParser { name: 'tags', content: website.generateTagsString(tags, description, websitePart), }, + { + name: 'cw', + content: FormContent.getSpoilerText(defaultPart.data, websitePart.data), + }, ]); // Parse all potential shortcut data diff --git a/electron-app/src/server/submission/post/interfaces/post-data.interface.ts b/electron-app/src/server/submission/post/interfaces/post-data.interface.ts index d829cd19..f52b4b53 100644 --- a/electron-app/src/server/submission/post/interfaces/post-data.interface.ts +++ b/electron-app/src/server/submission/post/interfaces/post-data.interface.ts @@ -11,4 +11,5 @@ export interface PostData { submission: T; tags: string[]; title: string; + spoilerText: string; } diff --git a/electron-app/src/server/utils/form-content.util.ts b/electron-app/src/server/utils/form-content.util.ts index 7ea043eb..b6f2141f 100644 --- a/electron-app/src/server/utils/form-content.util.ts +++ b/electron-app/src/server/utils/form-content.util.ts @@ -20,4 +20,20 @@ export default class FormContent { ? _.get(websiteDescription, 'value', '') : _.get(defaultDescription, 'value', ''); } + + static getSpoilerText( + defaultData: { spoilerText?: string }, + partData: { spoilerText?: string; spoilerTextOverwrite?: boolean }, + ): string { + const partSpoilerText = partData.spoilerText || ''; + const overwrite = + partData.spoilerTextOverwrite === undefined + ? partSpoilerText.trim() !== '' + : partData.spoilerTextOverwrite; + if (overwrite) { + return partSpoilerText; + } else { + return defaultData.spoilerText || ''; + } + } } diff --git a/electron-app/src/server/websites/itaku/itaku.service.ts b/electron-app/src/server/websites/itaku/itaku.service.ts index 798a1ba3..b7ffedb6 100644 --- a/electron-app/src/server/websites/itaku/itaku.service.ts +++ b/electron-app/src/server/websites/itaku/itaku.service.ts @@ -174,8 +174,8 @@ export class Itaku extends Website { postData.add_to_feed = 'true'; } - if (data.options.spoilerText) { - postData.content_warning = data.options.spoilerText; + if (data.spoilerText) { + postData.content_warning = data.spoilerText; } if (fileRecord.type === FileSubmissionType.IMAGE) { @@ -256,7 +256,7 @@ export class Itaku extends Website { validateFileSubmission( submission: FileSubmission, - submissionPart: SubmissionPart, + submissionPart: SubmissionPart, defaultPart: SubmissionPart, ): ValidationParts { const problems: string[] = []; @@ -290,6 +290,11 @@ export class Itaku extends Website { problems.push(`Posting multiple images requires share on feed to be enabled`); } + const spoilerText = FormContent.getSpoilerText(defaultPart.data, submissionPart.data); + if (spoilerText.length > 30) { + problems.push(`Max content warning length allowed is 30 characters`); + } + return { problems, warnings }; } diff --git a/electron-app/src/server/websites/megalodon/megalodon.service.ts b/electron-app/src/server/websites/megalodon/megalodon.service.ts index aa0d81a7..d7bfcca0 100644 --- a/electron-app/src/server/websites/megalodon/megalodon.service.ts +++ b/electron-app/src/server/websites/megalodon/megalodon.service.ts @@ -142,8 +142,8 @@ export abstract class Megalodon extends Website { statusOptions.in_reply_to_id = replyToId; } - if (data.options.spoilerText) { - statusOptions.spoiler_text = data.options.spoilerText; + if (data.spoilerText) { + statusOptions.spoiler_text = data.spoilerText; } // Mastodon may return a 422 error if the media is still processing, @@ -200,8 +200,8 @@ export abstract class Megalodon extends Website { let status = `${data.options.useTitle && data.title ? `${data.title}\n` : ''}${ data.description }`; - if (data.options.spoilerText) { - statusOptions.spoiler_text = data.options.spoilerText; + if (data.spoilerText) { + statusOptions.spoiler_text = data.spoilerText; } const replyToId = this.getPostIdFromUrl(data.options.replyToUrl); diff --git a/electron-app/src/server/websites/misskey/misskey.service.ts b/electron-app/src/server/websites/misskey/misskey.service.ts index 1e398eec..fd2c7847 100644 --- a/electron-app/src/server/websites/misskey/misskey.service.ts +++ b/electron-app/src/server/websites/misskey/misskey.service.ts @@ -155,8 +155,8 @@ export class MissKey extends Website { if (i !== 0) { statusOptions.in_reply_to_id = lastId; } - if (data.options.spoilerText) { - statusOptions.spoiler_text = data.options.spoilerText; + if (data.spoilerText) { + statusOptions.spoiler_text = data.spoilerText; } this.checkCancelled(cancellationToken); @@ -196,8 +196,8 @@ export class MissKey extends Website { let status = `${data.options.useTitle && data.title ? `${data.title}\n` : ''}${ data.description }`.substring(0, maxChars); - if (data.options.spoilerText) { - statusOptions.spoiler_text = data.options.spoilerText; + if (data.spoilerText) { + statusOptions.spoiler_text = data.spoilerText; } this.checkCancelled(cancellationToken); diff --git a/ui/src/views/submissions/submission-forms/form-components/DescriptionInput.tsx b/ui/src/views/submissions/submission-forms/form-components/DescriptionInput.tsx index a2c5ea00..34ca230e 100644 --- a/ui/src/views/submissions/submission-forms/form-components/DescriptionInput.tsx +++ b/ui/src/views/submissions/submission-forms/form-components/DescriptionInput.tsx @@ -181,6 +181,13 @@ export default class DescriptionInput extends React.Component { Inserts the website tags or the default tags separated by ' #' +
  • + {'{cw}'} + - + + Inserts the content warning + +
  • } diff --git a/ui/src/views/submissions/submission-forms/form-components/SpoilerTextInput.tsx b/ui/src/views/submissions/submission-forms/form-components/SpoilerTextInput.tsx new file mode 100644 index 00000000..bffa3269 --- /dev/null +++ b/ui/src/views/submissions/submission-forms/form-components/SpoilerTextInput.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { Form, Input, Switch } from 'antd'; +import { observer } from 'mobx-react'; + +interface Props { + label?: string; + maxLength?: number; + overwriteDefault?: boolean; + spoilerText?: string; + onChangeOverwriteDefault: (overwriteDefault: boolean) => void; + onChangeSpoilerText: (spoilerText: string) => void; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface State {} + +@observer +export default class SpoilerTextInput extends React.Component { + state: State = {}; + + private overwriteDefault: boolean; + private spoilerText: string; + + constructor(props: Props) { + super(props); + if (props.overwriteDefault === undefined) { + this.spoilerText = props.spoilerText || ''; + this.overwriteDefault = this.spoilerText.trim() !== ''; + if (this.overwriteDefault) { + this.props.onChangeOverwriteDefault(true); + } + } else { + this.overwriteDefault = !!props.overwriteDefault; + this.spoilerText = props.spoilerText || ''; + } + } + + handleOverwriteDefaultChange = (checked: boolean) => { + this.overwriteDefault = !checked; + this.props.onChangeOverwriteDefault(this.overwriteDefault); + if (!checked && this.props.overwriteDefault) { + this.handleSpoilerTextChange(this.props.spoilerText || ''); + } + }; + + handleSpoilerTextChange = (spoilerText: string) => { + this.spoilerText = spoilerText; + this.props.onChangeSpoilerText(this.spoilerText); + }; + + render() { + return ( + +
    + + + + Use default +
    + {this.props.overwriteDefault && ( + this.handleSpoilerTextChange(e.target.value)} + maxLength={this.props.maxLength} + /> + )} +
    + ); + } +} diff --git a/ui/src/views/submissions/submission-forms/form-sections/DefaultFormSection.tsx b/ui/src/views/submissions/submission-forms/form-sections/DefaultFormSection.tsx index ffa2fbad..168f96a1 100644 --- a/ui/src/views/submissions/submission-forms/form-sections/DefaultFormSection.tsx +++ b/ui/src/views/submissions/submission-forms/form-sections/DefaultFormSection.tsx @@ -62,6 +62,9 @@ export default class DefaultFormSection extends React.Component< Extreme + + + , - - - + , ); return elements; } diff --git a/ui/src/websites/mastodon/Mastodon.tsx b/ui/src/websites/mastodon/Mastodon.tsx index 2b3e2494..fc7ecebf 100644 --- a/ui/src/websites/mastodon/Mastodon.tsx +++ b/ui/src/websites/mastodon/Mastodon.tsx @@ -14,6 +14,7 @@ import GenericSubmissionSection from '../generic/GenericSubmissionSection'; import { LoginDialogProps } from '../interfaces/website.interface'; import { WebsiteImpl } from '../website.base'; import MastodonLogin from './MastodonLogin'; +import SpoilerTextInput from '../../views/submissions/submission-forms/form-components/SpoilerTextInput'; export class Mastodon extends WebsiteImpl { internalName: string = 'Mastodon'; @@ -72,12 +73,12 @@ class MastodonNotificationSubmissionForm extends GenericSubmissionSection< Use title , - - - , + , - , + , , - - - , + , - , + , , - - - , + , , - - - , + , - , + ,