diff --git a/packages/pl-fe/src/components/account.tsx b/packages/pl-fe/src/components/account.tsx index 1fcaf657a..caadcb88c 100644 --- a/packages/pl-fe/src/components/account.tsx +++ b/packages/pl-fe/src/components/account.tsx @@ -14,6 +14,7 @@ import VerificationBadge from 'pl-fe/components/verification-badge'; import Emojify from 'pl-fe/features/emoji/emojify'; import ActionButton from 'pl-fe/features/ui/components/action-button'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useSettings } from 'pl-fe/hooks/use-settings'; import { getAcct } from 'pl-fe/utils/accounts'; import { displayFqn } from 'pl-fe/utils/state'; @@ -140,6 +141,7 @@ const Account = ({ const me = useAppSelector((state) => state.me); const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null); + const { disableUserProvidedMedia } = useSettings(); const handleAction = () => { onActionClick!(account); @@ -220,16 +222,20 @@ const Account = ({
-
- - {emoji && ( - - )} -
+ {disableUserProvidedMedia ? ( + + ) : ( +
+ + {emoji && ( + + )} +
+ )}
@@ -250,7 +256,7 @@ const Account = ({ @{username} - {account.favicon && ( + {account.favicon && !disableUserProvidedMedia && ( )} @@ -271,7 +277,9 @@ const Account = ({
- {withAvatar && ( + {withAvatar && (disableUserProvidedMedia ? ( + + ) : ( {children}} @@ -287,7 +295,7 @@ const Account = ({ )} - )} + ))}
@{username} - {account.favicon && ( + {account.favicon && !disableUserProvidedMedia && ( )} diff --git a/packages/pl-fe/src/components/alt-indicator.tsx b/packages/pl-fe/src/components/alt-indicator.tsx index 2c7494898..ba1ac8ca6 100644 --- a/packages/pl-fe/src/components/alt-indicator.tsx +++ b/packages/pl-fe/src/components/alt-indicator.tsx @@ -6,16 +6,17 @@ import Icon from 'pl-fe/components/ui/icon'; interface IAltIndicator extends Pick, 'title' | 'className'> { warning?: boolean; + message?: JSX.Element; } -const AltIndicator: React.FC = React.forwardRef(({ className, warning, ...props }, ref) => ( +const AltIndicator: React.FC = React.forwardRef(({ className, warning, message, ...props }, ref) => ( {warning && } - + {message || } )); diff --git a/packages/pl-fe/src/components/media-gallery.tsx b/packages/pl-fe/src/components/media-gallery.tsx index 5168aebe2..54bcb3e14 100644 --- a/packages/pl-fe/src/components/media-gallery.tsx +++ b/packages/pl-fe/src/components/media-gallery.tsx @@ -17,6 +17,8 @@ import { truncateFilename } from 'pl-fe/utils/media'; import { isIOS } from '../is-mobile'; import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media-aspect-ratio'; +import HStack from './ui/hstack'; + import type { MediaAttachment } from 'pl-api'; const ATTACHMENT_LIMIT = 4; @@ -315,14 +317,53 @@ const MediaGallery: React.FC = (props) => { visible, } = props; + const { disableUserProvidedMedia } = useSettings(); + const [width, setWidth] = useState(defaultWidth); const node = useRef(null); + useLayoutEffect(() => { + if (node.current) { + const { offsetWidth } = node.current; + + if (cacheWidth) { + cacheWidth(offsetWidth); + } + + setWidth(offsetWidth); + } + }, [node.current]); + const handleClick = (index: number) => { onOpenMedia(media, index); }; + if (disableUserProvidedMedia) { + return ( + + {media.map((attachment, index) => ( + handleClick(index)}> + + + {attachment.description || { + image: , + video: , + gifv: , + audio: , + unknown: , + }[attachment.type]} + + + // + ))} + + ); + } + const getSizeDataSingle = (): SizeData => { const w = width || defaultWidth; const aspectRatio = getAspectRatio(media[0]); @@ -564,18 +605,6 @@ const MediaGallery: React.FC = (props) => { /> )); - useLayoutEffect(() => { - if (node.current) { - const { offsetWidth } = node.current; - - if (cacheWidth) { - cacheWidth(offsetWidth); - } - - setWidth(offsetWidth); - } - }, [node.current]); - return (
(); @@ -27,6 +33,7 @@ const fac = new FastAverageColor(); /** Round profile avatar for accounts. */ const Avatar = (props: IAvatar) => { const intl = useIntl(); + const { disableUserProvidedMedia } = useSettings(); const { alt, src, size = AVATAR_SIZE, className, isCat } = props; @@ -58,6 +65,29 @@ const Avatar = (props: IAvatar) => { color, }), [size, color]); + if (disableUserProvidedMedia) { + if (isAvatarMissing || !alt) return null; + return ( + + + + + + {alt} + + + } + isFlush + > + } /> + + ); + } + if (isAvatarMissing) { return (
, 'alt' | /** A single emoji image. */ const Emoji: React.FC = (props): JSX.Element | null => { + const { disableUserProvidedMedia } = useSettings(); const { emoji, alt, src, noGroup, ...rest } = props; let filename; @@ -24,6 +26,7 @@ const Emoji: React.FC = (props): JSX.Element | null => { if (!filename && !src) return null; if (src) { + if (disableUserProvidedMedia) return <>{alt || emoji}; return ( = (props): JSX.Element | null => { {alt ); diff --git a/packages/pl-fe/src/components/upload.tsx b/packages/pl-fe/src/components/upload.tsx index dcf751360..332785944 100644 --- a/packages/pl-fe/src/components/upload.tsx +++ b/packages/pl-fe/src/components/upload.tsx @@ -3,9 +3,12 @@ import fileCodeIcon from '@tabler/icons/outline/file-code.svg'; import fileSpreadsheetIcon from '@tabler/icons/outline/file-spreadsheet.svg'; import fileTextIcon from '@tabler/icons/outline/file-text.svg'; import fileZipIcon from '@tabler/icons/outline/file-zip.svg'; +import audioIcon from '@tabler/icons/outline/music.svg'; import defaultIcon from '@tabler/icons/outline/paperclip.svg'; import editIcon from '@tabler/icons/outline/pencil.svg'; +import imageIcon from '@tabler/icons/outline/photo.svg'; import presentationIcon from '@tabler/icons/outline/presentation.svg'; +import videoIcon from '@tabler/icons/outline/video.svg'; import xIcon from '@tabler/icons/outline/x.svg'; import zoomInIcon from '@tabler/icons/outline/zoom-in.svg'; import clsx from 'clsx'; @@ -55,6 +58,10 @@ const MIMETYPE_ICONS: Record = { 'application/x-abiword': fileTextIcon, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': fileTextIcon, 'application/vnd.oasis.opendocument.text': fileTextIcon, + image: imageIcon, + video: videoIcon, + gifv: videoIcon, + audio: audioIcon, }; const messages = defineMessages({ diff --git a/packages/pl-fe/src/features/account/components/header.tsx b/packages/pl-fe/src/features/account/components/header.tsx index 6cc3c773e..8c7a04fb0 100644 --- a/packages/pl-fe/src/features/account/components/header.tsx +++ b/packages/pl-fe/src/features/account/components/header.tsx @@ -1,4 +1,5 @@ import { useMutation } from '@tanstack/react-query'; +import clsx from 'clsx'; import { GOTOSOCIAL, MASTODON, mediaAttachmentSchema } from 'pl-api'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; @@ -9,12 +10,16 @@ import { biteAccount, blockAccount, pinAccount, removeFromFollowers, unblockAcco import { mentionCompose, directCompose } from 'pl-fe/actions/compose'; import { initReport, ReportableEntities } from 'pl-fe/actions/reports'; import { useFollow } from 'pl-fe/api/hooks/accounts/use-follow'; +import AltIndicator from 'pl-fe/components/alt-indicator'; import Badge from 'pl-fe/components/badge'; import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu'; import StillImage from 'pl-fe/components/still-image'; import Avatar from 'pl-fe/components/ui/avatar'; import HStack from 'pl-fe/components/ui/hstack'; import IconButton from 'pl-fe/components/ui/icon-button'; +import Popover from 'pl-fe/components/ui/popover'; +import Stack from 'pl-fe/components/ui/stack'; +import Text from 'pl-fe/components/ui/text'; import VerificationBadge from 'pl-fe/components/verification-badge'; import MovedNote from 'pl-fe/features/account-timeline/components/moved-note'; import ActionButton from 'pl-fe/features/ui/components/action-button'; @@ -23,11 +28,11 @@ import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useClient } from 'pl-fe/hooks/use-client'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useOwnAccount } from 'pl-fe/hooks/use-own-account'; +import { useSettings } from 'pl-fe/hooks/use-settings'; import { useChats } from 'pl-fe/queries/chats'; import { queryClient } from 'pl-fe/queries/client'; import { blockDomainMutationOptions, unblockDomainMutationOptions } from 'pl-fe/queries/settings/domain-blocks'; import { useModalsStore } from 'pl-fe/stores/modals'; -import { useSettingsStore } from 'pl-fe/stores/settings'; import toast from 'pl-fe/toast'; import { isDefaultHeader } from 'pl-fe/utils/accounts'; import copy from 'pl-fe/utils/copy'; @@ -98,7 +103,7 @@ const Header: React.FC = ({ account }) => { const { account: ownAccount } = useOwnAccount(); const { follow } = useFollow(); const { openModal } = useModalsStore(); - const { settings } = useSettingsStore(); + const settings = useSettings(); const { software } = features.version; @@ -566,6 +571,29 @@ const Header: React.FC = ({ account }) => { const renderHeader = () => { let header: React.ReactNode; + if (settings.disableUserProvidedMedia) { + if (!account.header_description) return null; + else return ( + + + + + + {account.header_description} + + + } + isFlush + > + } /> + + ); + } + if (account.header) { header = ( = ({ account }) => { )}
-
+
{renderHeader()}
diff --git a/packages/pl-fe/src/features/emoji/emojify.tsx b/packages/pl-fe/src/features/emoji/emojify.tsx index 9549b04ef..862132185 100644 --- a/packages/pl-fe/src/features/emoji/emojify.tsx +++ b/packages/pl-fe/src/features/emoji/emojify.tsx @@ -1,6 +1,7 @@ import split from 'graphemesplit'; import React from 'react'; +import { useSettings } from 'pl-fe/hooks/use-settings'; import { makeEmojiMap } from 'pl-fe/utils/normalizers'; import unicodeMapping from './mapping'; @@ -34,6 +35,8 @@ interface IEmojify { } const Emojify: React.FC = React.memo(({ text, emojis = {} }) => { + const { disableUserProvidedMedia } = useSettings(); + if (Array.isArray(emojis)) emojis = makeEmojiMap(emojis); const nodes = []; @@ -76,7 +79,7 @@ const Emojify: React.FC = React.memo(({ text, emojis = {} }) => { nodes.push( {unqualified}, ); - } else if (c === ':') { + } else if (!disableUserProvidedMedia && c === ':') { if (!open) { clearStack(); } diff --git a/packages/pl-fe/src/features/preferences/index.tsx b/packages/pl-fe/src/features/preferences/index.tsx index 7a0d41599..50e5d187c 100644 --- a/packages/pl-fe/src/features/preferences/index.tsx +++ b/packages/pl-fe/src/features/preferences/index.tsx @@ -282,6 +282,13 @@ const Preferences = () => { )} + + } + hint={} + > + + diff --git a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx index 9aad816cc..90a97aba5 100644 --- a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx @@ -25,7 +25,7 @@ interface IUserPanel { const UserPanel: React.FC = ({ accountId, action, badges, domain }) => { const intl = useIntl(); - const { demetricator } = useSettings(); + const { demetricator, disableUserProvidedMedia } = useSettings(); const { account } = useAccount(accountId); const fqn = useAppSelector((state) => displayFqn(state)); @@ -38,26 +38,30 @@ const UserPanel: React.FC = ({ accountId, action, badges, domain })
-
- {header && ( - - )} -
+ {!disableUserProvidedMedia && ( +
+ {header && ( + + )} +
+ )} - - - - + + {!disableUserProvidedMedia && ( + + + + )} {action && (
{action}
diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 2cf58fdac..c839f7c97 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -4,6 +4,7 @@ "accordion.expand": "Expand", "account.add_or_remove_from_list": "Add or Remove from lists", "account.avatar.alt": "Avatar", + "account.avatar.description": "Avatar description", "account.badges.bot": "Bot", "account.birthday": "Born {date}", "account.birthday_today": "Birthday is today!", @@ -31,6 +32,7 @@ "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "Follows you", "account.header.alt": "Profile header", + "account.header.description": "Header description", "account.hide_reblogs": "Hide reposts from @{name}", "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", @@ -657,12 +659,14 @@ "edit_federation.success": "{host} federation was updated", "edit_federation.unlisted": "Force posts unlisted", "edit_password.header": "Change password", + "edit_profile.custom_css.remaining_characters": "{remaining, plural, one {# character} other {# characters}} remaining", "edit_profile.error": "Profile update failed", "edit_profile.fields.bio_label": "Bio", "edit_profile.fields.bio_placeholder": "Tell us about yourself.", "edit_profile.fields.birthday_label": "Birthday", "edit_profile.fields.birthday_placeholder": "Your birthday", "edit_profile.fields.bot_label": "This is a bot account", + "edit_profile.fields.custom_css_label": "Custom CSS", "edit_profile.fields.discoverable_label": "Allow account discovery", "edit_profile.fields.display_name_label": "Display name", "edit_profile.fields.display_name_placeholder": "Name", @@ -681,6 +685,11 @@ "edit_profile.fields.rss_label": "Enable RSS feed for public posts", "edit_profile.fields.speak_as_cat_label": "The user speaks as a cat", "edit_profile.fields.stranger_notifications_label": "Block notifications from strangers", + "edit_profile.fields.web_layout.gallery": "Media-only gallery layout", + "edit_profile.fields.web_layout.microblog": "Classic microblog layout", + "edit_profile.fields.web_visibility.none": "Show no posts", + "edit_profile.fields.web_visibility.public": "Show public posts only", + "edit_profile.fields.web_visibility.unlisted": "Show public and unlisted posts", "edit_profile.header": "Edit profile", "edit_profile.hints.bot": "This account mainly performs automated actions and might not be monitored", "edit_profile.hints.discoverable": "Display account in profile directory and allow indexing by external services", @@ -1068,6 +1077,11 @@ "manage_group.fields.name_placeholder": "Group Name", "manage_group.pending_requests": "Pending requests", "media-gallery.description": "Image description", + "media.default_description.attachment": "Attachment", + "media.default_description.audio": "Audio", + "media.default_description.gifv": "GIFV", + "media.default_description.image": "Image", + "media.default_description.video": "Video", "media_panel.empty_message": "No media found.", "media_panel.title": "Media", "mfa.confirm.success_message": "MFA confirmed", @@ -1292,6 +1306,8 @@ "preferences.fields.demetricator_label": "Hide social media counters", "preferences.fields.demo_hint": "Use the default pl-fe logo and color scheme. Useful for taking screenshots.", "preferences.fields.demo_label": "Demo mode", + "preferences.fields.disable_user_provided_media_hint": "This will hide images, videos, and other media uploaded by users and display alternative text instead.", + "preferences.fields.disable_user_provided_media_label": "Do not display user-provided media", "preferences.fields.display_media.default": "Hide posts marked as sensitive", "preferences.fields.display_media.hide_all": "Always hide media posts", "preferences.fields.display_media.show_all": "Always show posts", @@ -1310,6 +1326,8 @@ "preferences.fields.theme_reset": "Reset theme", "preferences.fields.underline_links_label": "Always underline links in posts", "preferences.fields.unfollow_modal_label": "Show confirmation dialog before unfollowing someone", + "preferences.fields.web_layout_label": "Layout of the web view of your profile", + "preferences.fields.web_visibility_label": "Visibility level of posts displayed on your profile", "preferences.fields.wrench_label": "Display wrench reaction button", "preferences.hints.demetricator": "Decrease social media anxiety by hiding all numbers from the site.", "preferences.hints.mention_policy": "Applies to direct messages and public posts", diff --git a/packages/pl-fe/src/schemas/pl-fe/settings.ts b/packages/pl-fe/src/schemas/pl-fe/settings.ts index db1d1920f..7b98817f1 100644 --- a/packages/pl-fe/src/schemas/pl-fe/settings.ts +++ b/packages/pl-fe/src/schemas/pl-fe/settings.ts @@ -49,6 +49,7 @@ const settingsSchema = v.object({ redirectServices: v.fallback(v.record(v.string(), v.string()), {}), }), checkEmojiReactsSupport: v.fallback(v.boolean(), false), + disableUserProvidedMedia: v.fallback(v.boolean(), false), theme: v.fallback(v.optional(v.object({ brandColor: v.fallback(v.string(), ''),