From c49aec2ae0d5695dde17067e0c39c3be102b7875 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 4 Oct 2022 15:08:22 -0400 Subject: [PATCH 01/32] Refactor UI library types --- .../components/ui/button/useButtonStyles.ts | 50 +++++++++---------- app/soapbox/components/ui/card/card.tsx | 8 +-- app/soapbox/components/ui/hstack/hstack.tsx | 14 +++--- app/soapbox/components/ui/modal/modal.tsx | 4 +- app/soapbox/components/ui/stack/stack.tsx | 12 ++--- app/soapbox/components/ui/text/text.tsx | 26 ++++------ 6 files changed, 52 insertions(+), 62 deletions(-) diff --git a/app/soapbox/components/ui/button/useButtonStyles.ts b/app/soapbox/components/ui/button/useButtonStyles.ts index ecec3de1f..4dc38997d 100644 --- a/app/soapbox/components/ui/button/useButtonStyles.ts +++ b/app/soapbox/components/ui/button/useButtonStyles.ts @@ -1,12 +1,32 @@ import classNames from 'clsx'; -type ButtonThemes = 'primary' | 'secondary' | 'tertiary' | 'accent' | 'danger' | 'transparent' | 'outline' -type ButtonSizes = 'sm' | 'md' | 'lg' +const themes = { + primary: + 'bg-primary-500 hover:bg-primary-400 dark:hover:bg-primary-600 border-transparent focus:bg-primary-500 text-gray-100 focus:ring-primary-300', + secondary: + 'border-transparent bg-primary-100 dark:bg-primary-800 hover:bg-primary-50 dark:hover:bg-primary-700 focus:bg-primary-100 dark:focus:bg-primary-800 text-primary-500 dark:text-primary-200', + tertiary: + 'bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500', + accent: 'border-transparent bg-secondary-500 hover:bg-secondary-400 focus:bg-secondary-500 text-gray-100 focus:ring-secondary-300', + danger: 'border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:bg-danger-800 dark:focus:bg-danger-600', + transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80', + outline: 'border-gray-100 border-2 bg-transparent text-gray-100 hover:bg-white/10', +}; + +const sizes = { + xs: 'px-3 py-1 text-xs', + sm: 'px-3 py-1.5 text-xs leading-4', + md: 'px-4 py-2 text-sm', + lg: 'px-6 py-3 text-base', +}; + +type ButtonSizes = keyof typeof sizes +type ButtonThemes = keyof typeof themes type IButtonStyles = { - theme: ButtonThemes, - block: boolean, - disabled: boolean, + theme: ButtonThemes + block: boolean + disabled: boolean size: ButtonSizes } @@ -17,26 +37,6 @@ const useButtonStyles = ({ disabled, size, }: IButtonStyles) => { - const themes = { - primary: - 'bg-primary-500 hover:bg-primary-400 dark:hover:bg-primary-600 border-transparent focus:bg-primary-500 text-gray-100 focus:ring-primary-300', - secondary: - 'border-transparent bg-primary-100 dark:bg-primary-800 hover:bg-primary-50 dark:hover:bg-primary-700 focus:bg-primary-100 dark:focus:bg-primary-800 text-primary-500 dark:text-primary-200', - tertiary: - 'bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500', - accent: 'border-transparent bg-secondary-500 hover:bg-secondary-400 focus:bg-secondary-500 text-gray-100 focus:ring-secondary-300', - danger: 'border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:bg-danger-800 dark:focus:bg-danger-600', - transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80', - outline: 'border-gray-100 border-2 bg-transparent text-gray-100 hover:bg-white/10', - }; - - const sizes = { - xs: 'px-3 py-1 text-xs', - sm: 'px-3 py-1.5 text-xs leading-4', - md: 'px-4 py-2 text-sm', - lg: 'px-6 py-3 text-base', - }; - const buttonStyle = classNames({ 'inline-flex items-center border font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 appearance-none transition-all': true, 'select-none disabled:opacity-75 disabled:cursor-default': disabled, diff --git a/app/soapbox/components/ui/card/card.tsx b/app/soapbox/components/ui/card/card.tsx index 816627326..59f6ee1bc 100644 --- a/app/soapbox/components/ui/card/card.tsx +++ b/app/soapbox/components/ui/card/card.tsx @@ -18,13 +18,13 @@ const messages = defineMessages({ interface ICard { /** The type of card. */ - variant?: 'default' | 'rounded', + variant?: 'default' | 'rounded' /** Card size preset. */ - size?: 'md' | 'lg' | 'xl', + size?: keyof typeof sizes /** Extra classnames for the
element. */ - className?: string, + className?: string /** Elements inside the card. */ - children: React.ReactNode, + children: React.ReactNode } /** An opaque backdrop to hold a collection of related elements. */ diff --git a/app/soapbox/components/ui/hstack/hstack.tsx b/app/soapbox/components/ui/hstack/hstack.tsx index f959cdd51..994da6aff 100644 --- a/app/soapbox/components/ui/hstack/hstack.tsx +++ b/app/soapbox/components/ui/hstack/hstack.tsx @@ -29,21 +29,21 @@ const spaces = { interface IHStack { /** Vertical alignment of children. */ - alignItems?: 'top' | 'bottom' | 'center' | 'start', + alignItems?: keyof typeof alignItemsOptions /** Extra class names on the
element. */ - className?: string, + className?: string /** Children */ - children?: React.ReactNode, + children?: React.ReactNode /** Horizontal alignment of children. */ - justifyContent?: 'between' | 'center' | 'start' | 'end' | 'around', + justifyContent?: keyof typeof justifyContentOptions /** Size of the gap between elements. */ - space?: 0.5 | 1 | 1.5 | 2 | 3 | 4 | 6 | 8, + space?: keyof typeof spaces /** Whether to let the flexbox grow. */ - grow?: boolean, + grow?: boolean /** Extra CSS styles for the
*/ style?: React.CSSProperties /** Whether to let the flexbox wrap onto multiple lines. */ - wrap?: boolean, + wrap?: boolean } /** Horizontal row of child elements. */ diff --git a/app/soapbox/components/ui/modal/modal.tsx b/app/soapbox/components/ui/modal/modal.tsx index e203a1460..969f7ae65 100644 --- a/app/soapbox/components/ui/modal/modal.tsx +++ b/app/soapbox/components/ui/modal/modal.tsx @@ -10,8 +10,6 @@ const messages = defineMessages({ confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, }); -type Widths = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' - const widths = { xs: 'max-w-xs', sm: 'max-w-sm', @@ -51,7 +49,7 @@ interface IModal { skipFocus?: boolean, /** Title text for the modal. */ title?: React.ReactNode, - width?: Widths, + width?: keyof typeof widths, } /** Displays a modal dialog box. */ diff --git a/app/soapbox/components/ui/stack/stack.tsx b/app/soapbox/components/ui/stack/stack.tsx index 64257ecf9..5f60f553f 100644 --- a/app/soapbox/components/ui/stack/stack.tsx +++ b/app/soapbox/components/ui/stack/stack.tsx @@ -1,8 +1,6 @@ import classNames from 'clsx'; import React from 'react'; -type SIZES = 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 10 - const spaces = { 0: 'space-y-0', '0.5': 'space-y-0.5', @@ -25,15 +23,15 @@ const alignItemsOptions = { interface IStack extends React.HTMLAttributes { /** Size of the gap between elements. */ - space?: SIZES, + space?: keyof typeof spaces /** Horizontal alignment of children. */ - alignItems?: 'center', + alignItems?: 'center' /** Vertical alignment of children. */ - justifyContent?: 'center', + justifyContent?: 'center' /** Extra class names on the
element. */ - className?: string, + className?: string /** Whether to let the flexbox grow. */ - grow?: boolean, + grow?: boolean } /** Vertical stack of child elements. */ diff --git a/app/soapbox/components/ui/text/text.tsx b/app/soapbox/components/ui/text/text.tsx index 2e0736809..7669f3d2a 100644 --- a/app/soapbox/components/ui/text/text.tsx +++ b/app/soapbox/components/ui/text/text.tsx @@ -1,16 +1,6 @@ import classNames from 'clsx'; import React from 'react'; -type Themes = 'default' | 'danger' | 'primary' | 'muted' | 'subtle' | 'success' | 'inherit' | 'white' -type Weights = 'normal' | 'medium' | 'semibold' | 'bold' -export type Sizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' -type Alignments = 'left' | 'center' | 'right' -type TrackingSizes = 'normal' | 'wide' -type TransformProperties = 'uppercase' | 'normal' -type Families = 'sans' | 'mono' -type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label' -type Directions = 'ltr' | 'rtl' - const themes = { default: 'text-gray-900 dark:text-gray-100', danger: 'text-danger-600', @@ -60,15 +50,19 @@ const families = { mono: 'font-mono', }; +export type Sizes = keyof typeof sizes +type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label' +type Directions = 'ltr' | 'rtl' + interface IText extends Pick, 'dangerouslySetInnerHTML'> { /** How to align the text. */ - align?: Alignments, + align?: keyof typeof alignments, /** Extra class names for the outer element. */ className?: string, /** Text direction. */ direction?: Directions, /** Typeface of the text. */ - family?: Families, + family?: keyof typeof families, /** The "for" attribute specifies which form element a label is bound to. */ htmlFor?: string, /** Font size of the text. */ @@ -76,15 +70,15 @@ interface IText extends Pick, 'danger /** HTML element name of the outer element. */ tag?: Tags, /** Theme for the text. */ - theme?: Themes, + theme?: keyof typeof themes, /** Letter-spacing of the text. */ - tracking?: TrackingSizes, + tracking?: keyof typeof trackingSizes, /** Transform (eg uppercase) for the text. */ - transform?: TransformProperties, + transform?: keyof typeof transformProperties, /** Whether to truncate the text if its container is too small. */ truncate?: boolean, /** Font weight of the text. */ - weight?: Weights, + weight?: keyof typeof weights, /** Tooltip title. */ title?: string, } From c960ad9d33ff48ce9d9f05fef94bbd2e0427a5d7 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 4 Oct 2022 15:17:26 -0400 Subject: [PATCH 02/32] Ensure space is number --- app/soapbox/components/ui/hstack/hstack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/components/ui/hstack/hstack.tsx b/app/soapbox/components/ui/hstack/hstack.tsx index 994da6aff..a109da608 100644 --- a/app/soapbox/components/ui/hstack/hstack.tsx +++ b/app/soapbox/components/ui/hstack/hstack.tsx @@ -17,7 +17,7 @@ const alignItemsOptions = { }; const spaces = { - '0.5': 'space-x-0.5', + [0.5]: 'space-x-0.5', 1: 'space-x-1', 1.5: 'space-x-1.5', 2: 'space-x-2', From 1c55e60055abbdca49cb07617b04f4a8c8ce2fee Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 4 Oct 2022 15:17:51 -0400 Subject: [PATCH 03/32] Ensure space is number --- app/soapbox/components/ui/stack/stack.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/ui/stack/stack.tsx b/app/soapbox/components/ui/stack/stack.tsx index 5f60f553f..b161d4949 100644 --- a/app/soapbox/components/ui/stack/stack.tsx +++ b/app/soapbox/components/ui/stack/stack.tsx @@ -3,9 +3,9 @@ import React from 'react'; const spaces = { 0: 'space-y-0', - '0.5': 'space-y-0.5', + [0.5]: 'space-y-0.5', 1: 'space-y-1', - '1.5': 'space-y-1.5', + [1.5]: 'space-y-1.5', 2: 'space-y-2', 3: 'space-y-3', 4: 'space-y-4', From 8903674cb151dfa13c3bd6837db50ec77f9c40ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 6 Oct 2022 00:02:32 +0200 Subject: [PATCH 04/32] Fix pinned status icon in status action bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/status-action-bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index 546d162a1..a5bd99d71 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -374,7 +374,7 @@ const StatusActionBar: React.FC = ({ menu.push({ text: intl.formatMessage(status.pinned ? messages.unpin : messages.pin), action: handlePinClick, - icon: mutingConversation ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg'), + icon: status.pinned ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg'), }); } else { if (status.visibility === 'private') { From 8e2b4b678005f9d63f375f022dc05ea92ee4eb9a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 9 Oct 2022 16:33:10 +0000 Subject: [PATCH 05/32] GitLab CI: tag `dind` --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aaca197c3..01c6314c9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -148,6 +148,8 @@ docker: image: docker:20.10.17 services: - docker:20.10.17-dind + tags: + - dind # https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df script: - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin From c1dcc91f76d0adc8a09a8b1b59d109e80b160b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 10 Oct 2022 00:32:09 +0200 Subject: [PATCH 06/32] Only show WhoToFollowPanel if authenticated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/pages/default_page.tsx | 2 +- app/soapbox/pages/home_page.tsx | 2 +- app/soapbox/pages/profile_page.tsx | 2 +- app/soapbox/pages/status_page.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/pages/default_page.tsx b/app/soapbox/pages/default_page.tsx index c013eb63d..9e918cc4e 100644 --- a/app/soapbox/pages/default_page.tsx +++ b/app/soapbox/pages/default_page.tsx @@ -39,7 +39,7 @@ const DefaultPage: React.FC = ({ children }) => { {Component => } )} - {features.suggestions && ( + {me && features.suggestions && ( {Component => } diff --git a/app/soapbox/pages/home_page.tsx b/app/soapbox/pages/home_page.tsx index dc8c2c9f4..de9375ace 100644 --- a/app/soapbox/pages/home_page.tsx +++ b/app/soapbox/pages/home_page.tsx @@ -103,7 +103,7 @@ const HomePage: React.FC = ({ children }) => { {Component => } )} - {features.suggestions && ( + {me && features.suggestions && ( {Component => } diff --git a/app/soapbox/pages/profile_page.tsx b/app/soapbox/pages/profile_page.tsx index 9f57e23ab..1cc5f64fe 100644 --- a/app/soapbox/pages/profile_page.tsx +++ b/app/soapbox/pages/profile_page.tsx @@ -137,7 +137,7 @@ const ProfilePage: React.FC = ({ params, children }) => { {Component => } - ) : features.suggestions && ( + ) : me && features.suggestions && ( {Component => } diff --git a/app/soapbox/pages/status_page.tsx b/app/soapbox/pages/status_page.tsx index 414df6783..a296b8fe0 100644 --- a/app/soapbox/pages/status_page.tsx +++ b/app/soapbox/pages/status_page.tsx @@ -43,7 +43,7 @@ const StatusPage: React.FC = ({ children }) => { {Component => } )} - {features.suggestions && ( + {me && features.suggestions && ( {Component => } From 134392840603a101492bd450a094c347257b4c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 10 Oct 2022 00:41:47 +0200 Subject: [PATCH 07/32] Make CtaBanner and ThreadLoginCta optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/soapbox_config/index.tsx | 8 ++++++++ .../features/status/components/thread-login-cta.tsx | 5 ++++- app/soapbox/features/ui/components/cta-banner.tsx | 4 ++-- app/soapbox/normalizers/soapbox/soapbox_config.ts | 1 + 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/soapbox_config/index.tsx b/app/soapbox/features/soapbox_config/index.tsx index fd339cf5c..222c44434 100644 --- a/app/soapbox/features/soapbox_config/index.tsx +++ b/app/soapbox/features/soapbox_config/index.tsx @@ -48,6 +48,7 @@ const messages = defineMessages({ promoPanelIconsLink: { id: 'soapbox_config.hints.promo_panel_icons.link', defaultMessage: 'Soapbox Icons List' }, authenticatedProfileLabel: { id: 'soapbox_config.authenticated_profile_label', defaultMessage: 'Profiles require authentication' }, authenticatedProfileHint: { id: 'soapbox_config.authenticated_profile_hint', defaultMessage: 'Users must be logged-in to view replies and media on user profiles.' }, + displayCtaLabel: { id: 'soapbox_config.cta_label', defaultMessage: 'Display call to action panels if not authenticated' }, singleUserModeLabel: { id: 'soapbox_config.single_user_mode_label', defaultMessage: 'Single user mode' }, singleUserModeHint: { id: 'soapbox_config.single_user_mode_hint', defaultMessage: 'Front page will redirect to a given user profile.' }, singleUserModeProfileLabel: { id: 'soapbox_config.single_user_mode_profile_label', defaultMessage: 'Main user handle' }, @@ -261,6 +262,13 @@ const SoapboxConfig: React.FC = () => { /> + + e.target.checked)} + /> + + { + const { displayCta } = useSoapboxConfig(); const siteTitle = useAppSelector(state => state.instance.title); + if (!displayCta) return null; + return ( diff --git a/app/soapbox/features/ui/components/cta-banner.tsx b/app/soapbox/features/ui/components/cta-banner.tsx index 9d83c6702..fa4ceba90 100644 --- a/app/soapbox/features/ui/components/cta-banner.tsx +++ b/app/soapbox/features/ui/components/cta-banner.tsx @@ -5,11 +5,11 @@ import { Banner, Button, HStack, Stack, Text } from 'soapbox/components/ui'; import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks'; const CtaBanner = () => { - const { singleUserMode } = useSoapboxConfig(); + const { displayCta, singleUserMode } = useSoapboxConfig(); const siteTitle = useAppSelector((state) => state.instance.title); const me = useAppSelector((state) => state.me); - if (me || singleUserMode) return null; + if (me || !displayCta || singleUserMode) return null; return (
diff --git a/app/soapbox/normalizers/soapbox/soapbox_config.ts b/app/soapbox/normalizers/soapbox/soapbox_config.ts index 75426e6c0..d9f1e5bb3 100644 --- a/app/soapbox/normalizers/soapbox/soapbox_config.ts +++ b/app/soapbox/normalizers/soapbox/soapbox_config.ts @@ -112,6 +112,7 @@ export const SoapboxConfigRecord = ImmutableRecord({ singleUserModeProfile: '', linkFooterMessage: '', links: ImmutableMap(), + displayCta: true, }, 'SoapboxConfig'); type SoapboxConfigMap = ImmutableMap; From bb1823ee6d6dfcc8ca162209624e23dd2027b14b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 10 Oct 2022 15:16:35 -0500 Subject: [PATCH 08/32] AttachmentThumbs: make visible --- app/soapbox/components/attachment-thumbs.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/attachment-thumbs.tsx b/app/soapbox/components/attachment-thumbs.tsx index 37b9fc9c6..3ac1dbf5f 100644 --- a/app/soapbox/components/attachment-thumbs.tsx +++ b/app/soapbox/components/attachment-thumbs.tsx @@ -6,9 +6,10 @@ import Bundle from 'soapbox/features/ui/components/bundle'; import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import type { List as ImmutableList } from 'immutable'; +import type { Attachment } from 'soapbox/types/entities'; interface IAttachmentThumbs { - media: ImmutableList> + media: ImmutableList onClick?(): void sensitive?: boolean } @@ -18,7 +19,7 @@ const AttachmentThumbs = (props: IAttachmentThumbs) => { const dispatch = useDispatch(); const renderLoading = () =>
; - const onOpenMedia = (media: Immutable.Record, index: number) => dispatch(openModal('MEDIA', { media, index })); + const onOpenMedia = (media: ImmutableList, index: number) => dispatch(openModal('MEDIA', { media, index })); return (
@@ -30,6 +31,7 @@ const AttachmentThumbs = (props: IAttachmentThumbs) => { height={50} compact sensitive={sensitive} + visible /> )} From 7dff0391b19883a5c7d374ec4cbee1c037441137 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 10 Oct 2022 15:56:11 -0500 Subject: [PATCH 09/32] Normalize chat attachments --- app/soapbox/components/media_gallery.tsx | 9 ++++----- .../features/chats/components/chat-message-list.tsx | 1 + app/soapbox/normalizers/chat_message.ts | 10 +++++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/soapbox/components/media_gallery.tsx b/app/soapbox/components/media_gallery.tsx index dfb9c7df5..4f672865b 100644 --- a/app/soapbox/components/media_gallery.tsx +++ b/app/soapbox/components/media_gallery.tsx @@ -263,14 +263,13 @@ const Item: React.FC = ({ interface IMediaGallery { sensitive?: boolean, media: ImmutableList, - size: number, height: number, onOpenMedia: (media: ImmutableList, index: number) => void, - defaultWidth: number, - cacheWidth: (width: number) => void, + defaultWidth?: number, + cacheWidth?: (width: number) => void, visible?: boolean, onToggleVisibility?: () => void, - displayMedia: string, + displayMedia?: string, compact: boolean, } @@ -278,7 +277,7 @@ const MediaGallery: React.FC = (props) => { const { media, sensitive = false, - defaultWidth, + defaultWidth = 0, onToggleVisibility, onOpenMedia, cacheWidth, diff --git a/app/soapbox/features/chats/components/chat-message-list.tsx b/app/soapbox/features/chats/components/chat-message-list.tsx index ad1a2f82d..9303f2b33 100644 --- a/app/soapbox/features/chats/components/chat-message-list.tsx +++ b/app/soapbox/features/chats/components/chat-message-list.tsx @@ -178,6 +178,7 @@ const ChatMessageList: React.FC = ({ chatId, chatMessageIds, a media={ImmutableList([attachment])} height={120} onOpenMedia={onOpenMedia} + visible /> )} diff --git a/app/soapbox/normalizers/chat_message.ts b/app/soapbox/normalizers/chat_message.ts index 71536acb5..f89af4de2 100644 --- a/app/soapbox/normalizers/chat_message.ts +++ b/app/soapbox/normalizers/chat_message.ts @@ -5,6 +5,8 @@ import { fromJS, } from 'immutable'; +import { normalizeAttachment } from 'soapbox/normalizers/attachment'; + import type { Attachment, Card, Emoji } from 'soapbox/types/entities'; export const ChatMessageRecord = ImmutableRecord({ @@ -22,8 +24,14 @@ export const ChatMessageRecord = ImmutableRecord({ pending: false, }); +const normalizeMedia = (status: ImmutableMap) => { + return status.update('attachment', null, normalizeAttachment); +}; + export const normalizeChatMessage = (chatMessage: Record) => { return ChatMessageRecord( - ImmutableMap(fromJS(chatMessage)), + ImmutableMap(fromJS(chatMessage)).withMutations(chatMessage => { + normalizeMedia(chatMessage); + }), ); }; From 936db6fac017a11c96a57c2a50428a31176ae26c Mon Sep 17 00:00:00 2001 From: Jonathan Kempf Date: Mon, 10 Oct 2022 17:25:19 -0400 Subject: [PATCH 10/32] Initial implementation of join date inclusion in hover profile card --- app/soapbox/components/profile-hover-card.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/profile-hover-card.tsx b/app/soapbox/components/profile-hover-card.tsx index 3775a107c..3bbe25704 100644 --- a/app/soapbox/components/profile-hover-card.tsx +++ b/app/soapbox/components/profile-hover-card.tsx @@ -1,6 +1,6 @@ import classNames from 'clsx'; import React, { useEffect, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { useIntl, FormattedMessage } from 'react-intl'; import { usePopper } from 'react-popper'; import { useHistory } from 'react-router-dom'; @@ -17,10 +17,11 @@ import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; import { showProfileHoverCard } from './hover_ref_wrapper'; -import { Card, CardBody, Stack, Text } from './ui'; +import { Card, CardBody, HStack, Icon, Stack, Text } from './ui'; import type { AppDispatch } from 'soapbox/store'; import type { Account } from 'soapbox/types/entities'; +import { isLocal } from 'soapbox/utils/accounts'; const getAccount = makeGetAccount(); @@ -60,6 +61,7 @@ interface IProfileHoverCard { export const ProfileHoverCard: React.FC = ({ visible = true }) => { const dispatch = useAppDispatch(); const history = useHistory(); + const intl = useIntl(); const [popperElement, setPopperElement] = useState(null); @@ -88,6 +90,7 @@ export const ProfileHoverCard: React.FC = ({ visible = true } if (!account) return null; const accountBio = { __html: account.note_emojified }; + const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' }); const followedBy = me !== account.id && account.relationship?.followed_by === true; return ( @@ -116,6 +119,23 @@ export const ProfileHoverCard: React.FC = ({ visible = true } )} + {isLocal(account) ? ( + + + + + + + + ) : null} + {account.source.get('note', '').length > 0 && ( )} From 763ae5c58a15315a393a29ed52e7b4d9753dd42d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 11 Oct 2022 13:22:54 -0500 Subject: [PATCH 11/32] yarn lint:js --fix --- app/soapbox/components/profile-hover-card.tsx | 2 +- app/soapbox/features/aliases/components/account.tsx | 2 +- .../features/compose/containers/quoted_status_container.tsx | 2 +- .../features/follow_requests/components/account_authorize.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/components/profile-hover-card.tsx b/app/soapbox/components/profile-hover-card.tsx index 3bbe25704..5baabeb86 100644 --- a/app/soapbox/components/profile-hover-card.tsx +++ b/app/soapbox/components/profile-hover-card.tsx @@ -15,13 +15,13 @@ import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; import { UserPanel } from 'soapbox/features/ui/util/async-components'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; +import { isLocal } from 'soapbox/utils/accounts'; import { showProfileHoverCard } from './hover_ref_wrapper'; import { Card, CardBody, HStack, Icon, Stack, Text } from './ui'; import type { AppDispatch } from 'soapbox/store'; import type { Account } from 'soapbox/types/entities'; -import { isLocal } from 'soapbox/utils/accounts'; const getAccount = makeGetAccount(); diff --git a/app/soapbox/features/aliases/components/account.tsx b/app/soapbox/features/aliases/components/account.tsx index be9178b8d..802d3e0f6 100644 --- a/app/soapbox/features/aliases/components/account.tsx +++ b/app/soapbox/features/aliases/components/account.tsx @@ -23,7 +23,7 @@ interface IAccount { const Account: React.FC = ({ accountId, aliases }) => { const intl = useIntl(); const dispatch = useAppDispatch(); - + const getAccount = useCallback(makeGetAccount(), []); const account = useAppSelector((state) => getAccount(state, accountId)); diff --git a/app/soapbox/features/compose/containers/quoted_status_container.tsx b/app/soapbox/features/compose/containers/quoted_status_container.tsx index 39416867b..00040e770 100644 --- a/app/soapbox/features/compose/containers/quoted_status_container.tsx +++ b/app/soapbox/features/compose/containers/quoted_status_container.tsx @@ -13,7 +13,7 @@ interface IQuotedStatusContainer { const QuotedStatusContainer: React.FC = ({ composeId }) => { const dispatch = useAppDispatch(); const getStatus = useCallback(makeGetStatus(), []); - + const status = useAppSelector(state => getStatus(state, { id: state.compose.get(composeId)?.quote! })); const onCancel = () => { diff --git a/app/soapbox/features/follow_requests/components/account_authorize.tsx b/app/soapbox/features/follow_requests/components/account_authorize.tsx index b820938d6..0d0428d44 100644 --- a/app/soapbox/features/follow_requests/components/account_authorize.tsx +++ b/app/soapbox/features/follow_requests/components/account_authorize.tsx @@ -23,7 +23,7 @@ interface IAccountAuthorize { const AccountAuthorize: React.FC = ({ id }) => { const intl = useIntl(); const dispatch = useDispatch(); - + const getAccount = useCallback(makeGetAccount(), []); const account = useAppSelector((state) => getAccount(state, id)); From d5f526448dd6f0595e79313ca747c582dd89e213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 11 Oct 2022 22:48:41 +0200 Subject: [PATCH 12/32] Update Polish translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/locales/pl.json | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 70a7b3902..12d21681d 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -49,6 +49,7 @@ "account.requested": "Oczekująca prośba, kliknij aby anulować", "account.requested_small": "Oczekująca prośba", "account.search": "Szukaj wpisów @{name}", + "account.search_self": "Szukaj własnych wpisów", "account.share": "Udostępnij profil @{name}", "account.show_reblogs": "Pokazuj podbicia od @{name}", "account.subscribe": "Subskrybuj wpisy @{name}", @@ -67,6 +68,18 @@ "account.verified": "Zweryfikowane konto", "account.welcome": "Welcome", "account_gallery.none": "Brak zawartości multimedialnej do wyświetlenia.", + "account_moderation_modal.admin_fe": "Otwórz w AdminFE", + "account_moderation_modal.fields.account_role": "Poziom uprawnień", + "account_moderation_modal.fields.badges": "Niestandaradowe odznaki", + "account_moderation_modal.fields.deactivate": "Dezaktywuj konto", + "account_moderation_modal.fields.delete": "Usuń konto", + "account_moderation_modal.fields.suggested": "Proponuj obserwację tego konta", + "account_moderation_modal.fields.verified": "Zweryfikowane konto", + "account_moderation_modal.info.id": "ID: {id}", + "account_moderation_modal.roles.admin": "Administrator", + "account_moderation_modal.roles.moderator": "Moderator", + "account_moderation_modal.roles.user": "Użytkownik", + "account_moderation_modal.title": "Moderuj @{acct}", "account_note.hint": "Możesz pozostawić dla siebie notatkę o tym użytkowniku (tylko ty ją zobaczysz):", "account_note.placeholder": "Nie wprowadzono opisu", "account_note.save": "Zapisz", @@ -125,6 +138,7 @@ "admin.users.actions.unsuggest_user": "Przestań polecać @{name}", "admin.users.actions.unverify_user": "Cofnij weryfikację @{name}", "admin.users.actions.verify_user": "Weryfikuj @{name}", + "admin.users.badges_saved_message": "Zaktualizowano niestandardowe odznaki.", "admin.users.remove_donor_message": "Usunięto @{acct} ze wspierających", "admin.users.set_donor_message": "Ustawiono @{acct} jako wspierającego", "admin.users.user_deactivated_message": "Zdezaktywowano @{acct}", @@ -173,6 +187,7 @@ "backups.empty_message": "Nie znaleziono kopii zapasowych. {action}", "backups.empty_message.action": "Chcesz utworzyć?", "backups.pending": "Oczekująca", + "badge_input.placeholder": "Wprowadź odznakę…", "beta.also_available": "Dostępne w językach:", "birthday_panel.title": "Urodziny", "birthdays_modal.empty": "Żaden z Twoich znajomych nie ma dziś urodzin.", @@ -736,8 +751,10 @@ "migration.fields.acct.placeholder": "konto@domena", "migration.fields.confirm_password.label": "Obecne hasło", "migration.hint": "Ta opcja przeniesie Twoich obserwujących na nowe konto. Żadne inne dane nie zostaną przeniesione. Aby dokonać migracji, musisz najpierw {link} na swoim nowym koncie.", + "migration.hint.cooldown_period": "Jeżeli przemigrujesz swoje konto, nie będziesz móc wykonać kolejnej migracji przez {cooldownPeriod, plural, one {jeden dzień} other {kolejne # dni}}.", "migration.hint.link": "utworzyć alias konta", "migration.move_account.fail": "Przenoszenie konta nie powiodło się.", + "migration.move_account.fail.cooldown_period": "Niedawno migrowałeś(-aś) swoje konto. Spróbuj ponownie później.", "migration.move_account.success": "Pomyślnie przeniesiono konto.", "migration.submit": "Przenieś obserwujących", "missing_description_modal.cancel": "Anuluj", @@ -747,6 +764,9 @@ "missing_indicator.label": "Nie znaleziono", "missing_indicator.sublabel": "Nie można odnaleźć tego zasobu", "mobile.also_available": "Dostępne w językach:", + "moderation_overlay.contact": "Kontakt", + "moderation_overlay.hide": "Ukryj", + "moderation_overlay.show": "Wyświetl", "morefollows.followers_label": "…i {count} więcej {count, plural, one {obserwujący(-a)} few {obserwujących} many {obserwujących} other {obserwujących}} na zdalnych stronach.", "morefollows.following_label": "…i {count} więcej {count, plural, one {obserwowany(-a)} few {obserwowanych} many {obserwowanych} other {obserwowanych}} na zdalnych stronach.", "mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?", @@ -843,6 +863,10 @@ "onboarding.display_name.subtitle": "Możesz ją zawsze zmienić później.", "onboarding.display_name.title": "Wybierz wyświetlaną nazwę", "onboarding.done": "Gotowe", + "onboarding.fediverse.its_you": "Oto Twoje konto! Inni ludzie mogą Cię obserwować z innych serwerów używając pełnej @nazwy.", + "onboarding.fediverse.next": "Dalej", + "onboarding.fediverse.title": "{siteTitle} to tylko jedna z części Fediwersum", + "onboarding.fediverse.other_instances": "Kiedy przeglądasz oś czasum, zwróć uwagę na pełną nazwę użytkownika po znaku @, aby wiedzieć z którego serwera pochodzi wpis.", "onboarding.finished.message": "Cieszymy się, że możemy powitać Cię w naszej społeczności! Naciśnij poniższy przycisk, aby rozpocząć.", "onboarding.finished.title": "Wprowadzenie ukończone", "onboarding.header.subtitle": "Będzie widoczny w górnej części Twojego profilu", @@ -1008,6 +1032,7 @@ "report.target": "Zgłaszanie {target}", "reset_password.fail": "Token wygasł, spróbuj ponownie.", "reset_password.header": "Ustaw nowe hasło", + "save": "Zapisz", "schedule.post_time": "Data/godzina publikacji", "schedule.remove": "Usuń zaplanowany wpis", "schedule_button.add_schedule": "Zaplanuj wpis na później", @@ -1128,7 +1153,7 @@ "sponsored.info.title": "Dlaczego widzę tę reklamę?", "sponsored.subtitle": "Wpis sponsorowany", "status.actions.more": "Więcej", - "status.admin_account": "Otwórz interfejs moderacyjny dla @{name}", + "status.admin_account": "Moderuj @{name}", "status.admin_status": "Otwórz ten wpis w interfejsie moderacyjnym", "status.block": "Zablokuj @{name}", "status.bookmark": "Dodaj do zakładek", From 21cba06d448cdb13ebefbf2faa94f3f30bdb3df1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Oct 2022 13:24:36 -0500 Subject: [PATCH 13/32] Delete focal point code --- app/images/reticle.png | Bin 1456 -> 0 bytes .../ui/components/focal_point_modal.js | 125 ------------------ .../features/ui/components/modal_root.js | 2 - .../features/ui/util/async-components.ts | 4 - app/styles/components/compose-form.scss | 36 ----- app/styles/components/modal.scss | 6 - 6 files changed, 173 deletions(-) delete mode 100644 app/images/reticle.png delete mode 100644 app/soapbox/features/ui/components/focal_point_modal.js diff --git a/app/images/reticle.png b/app/images/reticle.png deleted file mode 100644 index 1bcb3d261a277d466a6fbb97edc98a79c8f8e99f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1456 zcmeAS@N?(olHy`uVBq!ia0vp^DIm5v>UQt<9T~k}%*51+C)7v*;;-tw_rp=f+YxdkliD$|K&5q48X?L70`v3iEW52}(WwCYouebJWHv8x9v_dw% z?!})M{c-o=-2TeX<)3~$oAtvLqjHtj{%z@QR#InkKzN@;+ofoXaAU~8Op!!Rj}HhBOAHzmC5`F z>|IBiK#2sBK_(xJ-`@`Iw#}|C4V7$-qFD~H4SMDFHZu3hmxBZb- zFKcp*|3j-lE>rKa84L!xiUFKkna(uvhOFUcC^li$3U*I$St%8=N|mvG6?e1Wl4(zW z2)>Z%-5hg|Y3|w7Y5Zvp>CY6suM523*&SJz#XNVLzfQ8+1;e9}abnGWA5~AUwz+U} zcUmR4>z+7`!qr*+d+bcB7oE23lyp2Uk^Xa~WXUZ1jU~KI=O#oSPZ$3i9I^g8=iAvE z{f?L0^r<pz%XxvJjn?*F>P zyA1Wx+vbO>XuS+qd2nN4d~~DdD)#u&Ns{)_)vWvC)?HkG$Cu^P<#mqJ8s~4yu)nm? zj@|zJ8jsZtk9U1Ij_+pDap|6IuAf7xGmZRh*-Mcu&W;YAk733DH=+&*L5XuE}b~NqRU;U}yJ~q~cPG%LI zu-fTr3ghgFHw>Kk9v}Nzs~oaw;k<7NTF?GkJ`elzSyV=?Aj$D{#VyV`g?hpkJs0*> zsI=bC@#Eh*%iFbM>9cDKU!FBje3`K^_JOsi>Be6tYGthV@7!zIFS}cQt=|={AM1be Za=I^0n3a?H2bj+pJYD@<);T3K0RU ({ - media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), -}); - -const mapDispatchToProps = (dispatch, { id }) => ({ - - onSave: (x, y) => { - dispatch(changeUploadCompose(id, { focus: `${x.toFixed(2)},${y.toFixed(2)}` })); - }, - -}); - -export default @connect(mapStateToProps, mapDispatchToProps) -class FocalPointModal extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - }; - - state = { - x: 0, - y: 0, - focusX: 0, - focusY: 0, - dragging: false, - }; - - componentDidMount() { - this.updatePositionFromMedia(this.props.media); - } - - componentDidUpdate(prevProps) { - const { media } = this.props; - if (prevProps.media.get('id') !== media.get('id')) { - this.updatePositionFromMedia(media); - } - } - - componentWillUnmount() { - document.removeEventListener('mousemove', this.handleMouseMove); - document.removeEventListener('mouseup', this.handleMouseUp); - } - - handleMouseDown = e => { - document.addEventListener('mousemove', this.handleMouseMove); - document.addEventListener('mouseup', this.handleMouseUp); - - this.updatePosition(e); - this.setState({ dragging: true }); - } - - handleMouseMove = e => { - this.updatePosition(e); - } - - handleMouseUp = () => { - document.removeEventListener('mousemove', this.handleMouseMove); - document.removeEventListener('mouseup', this.handleMouseUp); - - this.setState({ dragging: false }); - this.props.onSave(this.state.focusX, this.state.focusY); - } - - updatePosition = e => { - const { x, y } = getPointerPosition(this.node, e); - const focusX = (x - .5) * 2; - const focusY = (y - .5) * -2; - - this.setState({ x, y, focusX, focusY }); - } - - updatePositionFromMedia = media => { - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); - - if (focusX && focusY) { - const x = (focusX / 2) + .5; - const y = (focusY / -2) + .5; - - this.setState({ x, y, focusX, focusY }); - } else { - this.setState({ x: 0.5, y: 0.5, focusX: 0, focusY: 0 }); - } - } - - setRef = c => { - this.node = c; - } - - render() { - const { media } = this.props; - const { x, y, dragging } = this.state; - - const width = media.getIn(['meta', 'original', 'width']) || null; - const height = media.getIn(['meta', 'original', 'height']) || null; - - return ( -
-
- - -
-
-
-
- ); - } - -} diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index 2c2de0530..32425cffd 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -15,7 +15,6 @@ import { ListAdder, MissingDescriptionModal, ActionsModal, - FocalPointModal, HotkeysModal, ComposeModal, ReplyMentionsModal, @@ -51,7 +50,6 @@ const MODAL_COMPONENTS = { 'ACTIONS': ActionsModal, 'EMBED': EmbedModal, 'LIST_EDITOR': ListEditor, - 'FOCAL_POINT': FocalPointModal, 'LIST_ADDER': ListAdder, 'HOTKEYS': HotkeysModal, 'COMPOSE': ComposeModal, diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index 9f1826fe8..18bbf71cf 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -146,10 +146,6 @@ export function ActionsModal() { return import(/* webpackChunkName: "features/ui" */'../components/actions_modal'); } -export function FocalPointModal() { - return import(/* webpackChunkName: "features/ui" */'../components/focal_point_modal'); -} - export function HotkeysModal() { return import(/* webpackChunkName: "features/ui" */'../components/hotkeys_modal'); } diff --git a/app/styles/components/compose-form.scss b/app/styles/components/compose-form.scss index 890016d61..0d76263d3 100644 --- a/app/styles/components/compose-form.scss +++ b/app/styles/components/compose-form.scss @@ -242,39 +242,3 @@ @apply block shadow-md; } } - -.focal-point { - position: relative; - cursor: pointer; - overflow: hidden; - - &.dragging { - cursor: move; - } - - img { - max-width: 80vw; - max-height: 80vh; - width: auto; - height: auto; - margin: auto; - } - - &__reticle { - position: absolute; - width: 100px; - height: 100px; - transform: translate(-50%, -50%); - background: url('../images/reticle.png') no-repeat 0 0; - border-radius: 50%; - box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35); - } - - &__overlay { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - } -} diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index 435c07c65..4410bdae0 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -387,12 +387,6 @@ } } -.focal-point-modal { - max-width: 80vw; - max-height: 80vh; - position: relative; -} - .column-inline-form { padding: 7px 15px; padding-right: 5px; From f95168b3e473612b2f0c5735f58e0296e82a335b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Oct 2022 15:22:50 -0500 Subject: [PATCH 14/32] ZoomableImage: convert to TSX --- .../{zoomable_image.js => zoomable_image.tsx} | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) rename app/soapbox/features/ui/components/{zoomable_image.js => zoomable_image.tsx} (64%) diff --git a/app/soapbox/features/ui/components/zoomable_image.js b/app/soapbox/features/ui/components/zoomable_image.tsx similarity index 64% rename from app/soapbox/features/ui/components/zoomable_image.js rename to app/soapbox/features/ui/components/zoomable_image.tsx index b9064fa4f..e502d9e94 100644 --- a/app/soapbox/features/ui/components/zoomable_image.js +++ b/app/soapbox/features/ui/components/zoomable_image.tsx @@ -1,28 +1,28 @@ -import PropTypes from 'prop-types'; import React from 'react'; const MIN_SCALE = 1; const MAX_SCALE = 4; -const getMidpoint = (p1, p2) => ({ +type Point = { x: number, y: number }; +type EventRemover = () => void; + +const getMidpoint = (p1: React.Touch, p2: React.Touch): Point => ({ x: (p1.clientX + p2.clientX) / 2, y: (p1.clientY + p2.clientY) / 2, }); -const getDistance = (p1, p2) => +const getDistance = (p1: React.Touch, p2: React.Touch): number => Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2)); -const clamp = (min, max, value) => Math.min(max, Math.max(min, value)); +const clamp = (min: number, max: number, value: number): number => Math.min(max, Math.max(min, value)); -export default class ZoomableImage extends React.PureComponent { +interface IZoomableImage { + alt?: string, + src: string, + onClick?: React.MouseEventHandler, +} - static propTypes = { - alt: PropTypes.string, - src: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - onClick: PropTypes.func, - } +export default class ZoomableImage extends React.PureComponent { static defaultProps = { alt: '', @@ -34,21 +34,22 @@ export default class ZoomableImage extends React.PureComponent { scale: MIN_SCALE, } - removers = []; - container = null; - image = null; + removers: EventRemover[] = []; + container: HTMLDivElement | null = null; + image: HTMLImageElement | null = null; lastTouchEndTime = 0; lastDistance = 0; + lastMidpoint: Point | undefined = undefined; componentDidMount() { let handler = this.handleTouchStart; - this.container.addEventListener('touchstart', handler); - this.removers.push(() => this.container.removeEventListener('touchstart', handler)); + this.container?.addEventListener('touchstart', handler); + this.removers.push(() => this.container?.removeEventListener('touchstart', handler)); handler = this.handleTouchMove; // on Chrome 56+, touch event listeners will default to passive // https://www.chromestatus.com/features/5093566007214080 - this.container.addEventListener('touchmove', handler, { passive: false }); - this.removers.push(() => this.container.removeEventListener('touchend', handler)); + this.container?.addEventListener('touchmove', handler, { passive: false }); + this.removers.push(() => this.container?.removeEventListener('touchend', handler)); } componentWillUnmount() { @@ -60,13 +61,16 @@ export default class ZoomableImage extends React.PureComponent { this.removers = []; } - handleTouchStart = e => { + handleTouchStart = (e: TouchEvent) => { if (e.touches.length !== 2) return; + const [p1, p2] = Array.from(e.touches); - this.lastDistance = getDistance(...e.touches); + this.lastDistance = getDistance(p1, p2); } - handleTouchMove = e => { + handleTouchMove = (e: TouchEvent) => { + if (!this.container) return; + const { scrollTop, scrollHeight, clientHeight } = this.container; if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) { // prevent propagating event to MediaModal @@ -78,8 +82,9 @@ export default class ZoomableImage extends React.PureComponent { e.preventDefault(); e.stopPropagation(); - const distance = getDistance(...e.touches); - const midpoint = getMidpoint(...e.touches); + const [p1, p2] = Array.from(e.touches); + const distance = getDistance(p1, p2); + const midpoint = getMidpoint(p1, p2); const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance); this.zoom(scale, midpoint); @@ -88,7 +93,9 @@ export default class ZoomableImage extends React.PureComponent { this.lastDistance = distance; } - zoom(nextScale, midpoint) { + zoom(nextScale: number, midpoint: Point) { + if (!this.container) return; + const { scale } = this.state; const { scrollLeft, scrollTop } = this.container; @@ -102,23 +109,24 @@ export default class ZoomableImage extends React.PureComponent { const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y; this.setState({ scale: nextScale }, () => { + if (!this.container) return; this.container.scrollLeft = nextScrollLeft; this.container.scrollTop = nextScrollTop; }); } - handleClick = e => { + handleClick: React.MouseEventHandler = e => { // don't propagate event to MediaModal e.stopPropagation(); const handler = this.props.onClick; - if (handler) handler(); + if (handler) handler(e); } - setContainerRef = c => { + setContainerRef = (c: HTMLDivElement) => { this.container = c; } - setImageRef = c => { + setImageRef = (c: HTMLImageElement) => { this.image = c; } From e6b0d17699c0b954f14d05f4e5265e9b0ab143b9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Oct 2022 15:26:34 -0500 Subject: [PATCH 15/32] ZoomableImage: refactor, clean up unused code --- .../features/ui/components/zoomable_image.tsx | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/app/soapbox/features/ui/components/zoomable_image.tsx b/app/soapbox/features/ui/components/zoomable_image.tsx index e502d9e94..84c66e1cb 100644 --- a/app/soapbox/features/ui/components/zoomable_image.tsx +++ b/app/soapbox/features/ui/components/zoomable_image.tsx @@ -4,7 +4,6 @@ const MIN_SCALE = 1; const MAX_SCALE = 4; type Point = { x: number, y: number }; -type EventRemover = () => void; const getMidpoint = (p1: React.Touch, p2: React.Touch): Point => ({ x: (p1.clientX + p2.clientX) / 2, @@ -22,7 +21,7 @@ interface IZoomableImage { onClick?: React.MouseEventHandler, } -export default class ZoomableImage extends React.PureComponent { +class ZoomableImage extends React.PureComponent { static defaultProps = { alt: '', @@ -34,31 +33,20 @@ export default class ZoomableImage extends React.PureComponent { scale: MIN_SCALE, } - removers: EventRemover[] = []; container: HTMLDivElement | null = null; image: HTMLImageElement | null = null; - lastTouchEndTime = 0; lastDistance = 0; - lastMidpoint: Point | undefined = undefined; componentDidMount() { - let handler = this.handleTouchStart; - this.container?.addEventListener('touchstart', handler); - this.removers.push(() => this.container?.removeEventListener('touchstart', handler)); - handler = this.handleTouchMove; + this.container?.addEventListener('touchstart', this.handleTouchStart); // on Chrome 56+, touch event listeners will default to passive // https://www.chromestatus.com/features/5093566007214080 - this.container?.addEventListener('touchmove', handler, { passive: false }); - this.removers.push(() => this.container?.removeEventListener('touchend', handler)); + this.container?.addEventListener('touchmove', this.handleTouchMove, { passive: false }); } componentWillUnmount() { - this.removeEventListeners(); - } - - removeEventListeners() { - this.removers.forEach(listeners => listeners()); - this.removers = []; + this.container?.removeEventListener('touchstart', this.handleTouchStart); + this.container?.removeEventListener('touchend', this.handleTouchMove); } handleTouchStart = (e: TouchEvent) => { @@ -89,7 +77,6 @@ export default class ZoomableImage extends React.PureComponent { this.zoom(scale, midpoint); - this.lastMidpoint = midpoint; this.lastDistance = distance; } @@ -158,3 +145,5 @@ export default class ZoomableImage extends React.PureComponent { } } + +export default ZoomableImage; \ No newline at end of file From f42e8520b546c81368f8f0b62d7e0f3dcc1d3bc3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Oct 2022 15:27:26 -0500 Subject: [PATCH 16/32] zoomable_image --> zoomable-image --- app/soapbox/features/ui/components/image_loader.js | 2 +- .../ui/components/{zoomable_image.tsx => zoomable-image.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/soapbox/features/ui/components/{zoomable_image.tsx => zoomable-image.tsx} (100%) diff --git a/app/soapbox/features/ui/components/image_loader.js b/app/soapbox/features/ui/components/image_loader.js index 8e6e8ec55..32eb2a4f9 100644 --- a/app/soapbox/features/ui/components/image_loader.js +++ b/app/soapbox/features/ui/components/image_loader.js @@ -2,7 +2,7 @@ import classNames from 'clsx'; import PropTypes from 'prop-types'; import React from 'react'; -import ZoomableImage from './zoomable_image'; +import ZoomableImage from './zoomable-image'; export default class ImageLoader extends React.PureComponent { diff --git a/app/soapbox/features/ui/components/zoomable_image.tsx b/app/soapbox/features/ui/components/zoomable-image.tsx similarity index 100% rename from app/soapbox/features/ui/components/zoomable_image.tsx rename to app/soapbox/features/ui/components/zoomable-image.tsx From 5885c454af981f04da53822dc0e0c5dd6621a48d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Oct 2022 16:23:16 -0500 Subject: [PATCH 17/32] ImageLoader: convert to TSX --- .../{image_loader.js => image_loader.tsx} | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) rename app/soapbox/features/ui/components/{image_loader.js => image_loader.tsx} (75%) diff --git a/app/soapbox/features/ui/components/image_loader.js b/app/soapbox/features/ui/components/image_loader.tsx similarity index 75% rename from app/soapbox/features/ui/components/image_loader.js rename to app/soapbox/features/ui/components/image_loader.tsx index 32eb2a4f9..ae60420ca 100644 --- a/app/soapbox/features/ui/components/image_loader.js +++ b/app/soapbox/features/ui/components/image_loader.tsx @@ -1,19 +1,20 @@ import classNames from 'clsx'; -import PropTypes from 'prop-types'; import React from 'react'; import ZoomableImage from './zoomable-image'; -export default class ImageLoader extends React.PureComponent { +type EventRemover = () => void; - static propTypes = { - alt: PropTypes.string, - src: PropTypes.string.isRequired, - previewSrc: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - onClick: PropTypes.func, - } +interface IImageLoader { + alt?: string, + src: string, + previewSrc?: string, + width?: number, + height?: number, + onClick?: React.MouseEventHandler, +} + +class ImageLoader extends React.PureComponent { static defaultProps = { alt: '', @@ -27,8 +28,9 @@ export default class ImageLoader extends React.PureComponent { width: null, } - removers = []; - canvas = null; + removers: EventRemover[] = []; + canvas: HTMLCanvasElement | null = null; + _canvasContext: CanvasRenderingContext2D | null = null; get canvasContext() { if (!this.canvas) { @@ -42,7 +44,7 @@ export default class ImageLoader extends React.PureComponent { this.loadImage(this.props); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: IImageLoader) { if (prevProps.src !== this.props.src) { this.loadImage(this.props); } @@ -52,7 +54,7 @@ export default class ImageLoader extends React.PureComponent { this.removeEventListeners(); } - loadImage(props) { + loadImage(props: IImageLoader) { this.removeEventListeners(); this.setState({ loading: true, error: false }); Promise.all([ @@ -66,7 +68,7 @@ export default class ImageLoader extends React.PureComponent { .catch(() => this.setState({ loading: false, error: true })); } - loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => { + loadPreviewCanvas = ({ previewSrc, width, height }: IImageLoader) => new Promise((resolve, reject) => { const image = new Image(); const removeEventListeners = () => { image.removeEventListener('error', handleError); @@ -78,21 +80,23 @@ export default class ImageLoader extends React.PureComponent { }; const handleLoad = () => { removeEventListeners(); - this.canvasContext.drawImage(image, 0, 0, width, height); + this.canvasContext?.drawImage(image, 0, 0, width || 0, height || 0); resolve(); }; image.addEventListener('error', handleError); image.addEventListener('load', handleLoad); - image.src = previewSrc; + image.src = previewSrc || ''; this.removers.push(removeEventListeners); }) clearPreviewCanvas() { - const { width, height } = this.canvas; - this.canvasContext.clearRect(0, 0, width, height); + if (this.canvas && this.canvasContext) { + const { width, height } = this.canvas; + this.canvasContext.clearRect(0, 0, width, height); + } } - loadOriginalImage = ({ src }) => new Promise((resolve, reject) => { + loadOriginalImage = ({ src }: IImageLoader) => new Promise((resolve, reject) => { const image = new Image(); const removeEventListeners = () => { image.removeEventListener('error', handleError); @@ -122,7 +126,7 @@ export default class ImageLoader extends React.PureComponent { return typeof width === 'number' && typeof height === 'number'; } - setCanvasRef = c => { + setCanvasRef = (c: HTMLCanvasElement) => { this.canvas = c; if (c) this.setState({ width: c.offsetWidth }); } @@ -157,3 +161,5 @@ export default class ImageLoader extends React.PureComponent { } } + +export default ImageLoader; \ No newline at end of file From 18b177d6c9bd43013dd2737e484223ee12cea398 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Oct 2022 16:26:19 -0500 Subject: [PATCH 18/32] image_loader --> image-loader --- .../ui/components/{image_loader.tsx => image-loader.tsx} | 0 app/soapbox/features/ui/components/media_modal.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/soapbox/features/ui/components/{image_loader.tsx => image-loader.tsx} (100%) diff --git a/app/soapbox/features/ui/components/image_loader.tsx b/app/soapbox/features/ui/components/image-loader.tsx similarity index 100% rename from app/soapbox/features/ui/components/image_loader.tsx rename to app/soapbox/features/ui/components/image-loader.tsx diff --git a/app/soapbox/features/ui/components/media_modal.js b/app/soapbox/features/ui/components/media_modal.js index dc988f270..61e7ff12d 100644 --- a/app/soapbox/features/ui/components/media_modal.js +++ b/app/soapbox/features/ui/components/media_modal.js @@ -13,7 +13,7 @@ import IconButton from 'soapbox/components/icon_button'; import Audio from 'soapbox/features/audio'; import Video from 'soapbox/features/video'; -import ImageLoader from './image_loader'; +import ImageLoader from './image-loader'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, From 80ce70e33ea7ed2fa6c9298bd613a4b139699d89 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Oct 2022 17:16:37 -0500 Subject: [PATCH 19/32] MediaModal: convert to TSX+FC --- .../features/ui/components/media_modal.js | 274 ---------------- .../features/ui/components/media_modal.tsx | 300 ++++++++++++++++++ 2 files changed, 300 insertions(+), 274 deletions(-) delete mode 100644 app/soapbox/features/ui/components/media_modal.js create mode 100644 app/soapbox/features/ui/components/media_modal.tsx diff --git a/app/soapbox/features/ui/components/media_modal.js b/app/soapbox/features/ui/components/media_modal.js deleted file mode 100644 index 61e7ff12d..000000000 --- a/app/soapbox/features/ui/components/media_modal.js +++ /dev/null @@ -1,274 +0,0 @@ -import classNames from 'clsx'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { withRouter } from 'react-router-dom'; -import ReactSwipeableViews from 'react-swipeable-views'; - -import ExtendedVideoPlayer from 'soapbox/components/extended_video_player'; -import Icon from 'soapbox/components/icon'; -import IconButton from 'soapbox/components/icon_button'; -import Audio from 'soapbox/features/audio'; -import Video from 'soapbox/features/video'; - -import ImageLoader from './image-loader'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, - previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, - next: { id: 'lightbox.next', defaultMessage: 'Next' }, -}); - -export default @injectIntl @withRouter -class MediaModal extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.list.isRequired, - status: ImmutablePropTypes.record, - account: ImmutablePropTypes.record, - index: PropTypes.number.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - history: PropTypes.object, - }; - - state = { - index: null, - navigationHidden: false, - }; - - handleSwipe = (index) => { - this.setState({ index: index % this.props.media.size }); - } - - handleNextClick = () => { - this.setState({ index: (this.getIndex() + 1) % this.props.media.size }); - } - - handlePrevClick = () => { - this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size }); - } - - handleChangeIndex = (e) => { - const index = Number(e.currentTarget.getAttribute('data-index')); - this.setState({ index: index % this.props.media.size }); - } - - handleKeyDown = (e) => { - switch (e.key) { - case 'ArrowLeft': - this.handlePrevClick(); - e.preventDefault(); - e.stopPropagation(); - break; - case 'ArrowRight': - this.handleNextClick(); - e.preventDefault(); - e.stopPropagation(); - break; - } - } - - componentDidMount() { - window.addEventListener('keydown', this.handleKeyDown, false); - } - - componentWillUnmount() { - window.removeEventListener('keydown', this.handleKeyDown); - } - - getIndex() { - return this.state.index !== null ? this.state.index : this.props.index; - } - - toggleNavigation = () => { - this.setState(prevState => ({ - navigationHidden: !prevState.navigationHidden, - })); - }; - - handleStatusClick = e => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - const { status, account } = this.props; - const acct = account.get('acct'); - const statusId = status.get('id'); - this.props.history.push(`/@${acct}/posts/${statusId}`); - this.props.onClose(null, true); - } - } - - handleCloserClick = ({ target }) => { - const whitelist = ['zoomable-image']; - const activeSlide = document.querySelector('.media-modal .react-swipeable-view-container > div[aria-hidden="false"]'); - - const isClickOutside = target === activeSlide || !activeSlide.contains(target); - const isWhitelisted = whitelist.some(w => target.classList.contains(w)); - - if (isClickOutside || isWhitelisted) { - this.props.onClose(); - } - } - - render() { - const { media, status, account, intl, onClose } = this.props; - const { navigationHidden } = this.state; - - const index = this.getIndex(); - let pagination = []; - - const leftNav = media.size > 1 && ( - - ); - - const rightNav = media.size > 1 && ( - - ); - - if (media.size > 1) { - pagination = media.map((item, i) => { - const classes = ['media-modal__button']; - if (i === index) { - classes.push('media-modal__button--active'); - } - return (
  • ); - }); - } - - const isMultiMedia = media.map((image) => { - if (image.get('type') !== 'image') { - return true; - } - - return false; - }).toArray(); - - const content = media.map(attachment => { - const width = attachment.getIn(['meta', 'original', 'width']) || null; - const height = attachment.getIn(['meta', 'original', 'height']) || null; - const link = (status && account && ); - - if (attachment.get('type') === 'image') { - return ( - - ); - } else if (attachment.get('type') === 'video') { - const { time } = this.props; - - return ( -