From 8f623c3bdf8dbbdc4c4f10f19b0b2c134b4160cb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 8 Mar 2024 14:45:59 -0600 Subject: [PATCH 01/13] Refactor `PostDropdownBtn` to use new `Menu` (#3112) * Refactor PostDropdownBtn (cherry picked from commit 0adf6cb75e3d4b7c1630cf6153c0d7e289e1b859) * Update icons (cherry picked from commit ac89ef9b28721c00736b1388455f3f5f092de0ad) * Port over fixes * fix scrollbar disappearing * Try CSS solution * Disable arrow for now --------- Co-authored-by: Hailey --- ...bubbleQuestion_stroke2_corner0_rounded.svg | 1 + .../icons/filter_stroke2_corner0_rounded.svg | 1 + ...akerVolumeFull_stroke2_corner0_rounded.svg | 1 + .../icons/trash_stroke2_corner0_rounded.svg | 1 + .../icons/warning_stroke2_corner0_rounded.svg | 1 + src/components/Menu/index.web.tsx | 8 +- src/components/icons/Bubble.tsx | 5 + src/components/icons/Filter.tsx | 5 + src/components/icons/Speaker.tsx | 5 + src/components/icons/Trash.tsx | 5 + src/components/icons/Warning.tsx | 5 + src/view/com/util/EventStopper.tsx | 11 +- src/view/com/util/forms/PostDropdownBtn.tsx | 376 ++++++++++-------- src/view/com/util/post-ctrls/PostCtrls.tsx | 1 + 14 files changed, 249 insertions(+), 177 deletions(-) create mode 100644 assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg create mode 100644 assets/icons/filter_stroke2_corner0_rounded.svg create mode 100644 assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg create mode 100644 assets/icons/trash_stroke2_corner0_rounded.svg create mode 100644 assets/icons/warning_stroke2_corner0_rounded.svg create mode 100644 src/components/icons/Bubble.tsx create mode 100644 src/components/icons/Filter.tsx create mode 100644 src/components/icons/Speaker.tsx create mode 100644 src/components/icons/Trash.tsx create mode 100644 src/components/icons/Warning.tsx diff --git a/assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg b/assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..0bfcc48a0e --- /dev/null +++ b/assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/filter_stroke2_corner0_rounded.svg b/assets/icons/filter_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..1fbcfc5711 --- /dev/null +++ b/assets/icons/filter_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg b/assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..81357a12e3 --- /dev/null +++ b/assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/trash_stroke2_corner0_rounded.svg b/assets/icons/trash_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..d4b32f81fe --- /dev/null +++ b/assets/icons/trash_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/warning_stroke2_corner0_rounded.svg b/assets/icons/warning_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..d5b6f13d5f --- /dev/null +++ b/assets/icons/warning_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index ca2e40566d..054e51b01e 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -92,10 +92,8 @@ export function Trigger({children, label, style}: TriggerProps) { accessibilityLabel={label} onFocus={onFocus} onBlur={onBlur} - style={flatten([style, web({outline: 0})])} - onPointerDown={() => { - control.open() - }} + style={flatten([style, focused && web({outline: 0})])} + onPointerDown={() => control.open()} {...web({ onMouseEnter, onMouseLeave, @@ -131,6 +129,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) { {children} + {/* Disabled until we can fix positioning ) { .backgroundColor } /> + */} ) diff --git a/src/components/icons/Bubble.tsx b/src/components/icons/Bubble.tsx new file mode 100644 index 0000000000..d4e08f6d29 --- /dev/null +++ b/src/components/icons/Bubble.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z', +}) diff --git a/src/components/icons/Filter.tsx b/src/components/icons/Filter.tsx new file mode 100644 index 0000000000..02ac1c71b4 --- /dev/null +++ b/src/components/icons/Filter.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Filter_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-.293.707L15 14.414V20a1 1 0 0 1-.758.97l-4 1A1 1 0 0 1 9 21v-6.586L3.293 8.707A1 1 0 0 1 3 8V4Zm2 1v2.586l5.707 5.707A1 1 0 0 1 11 14v5.72l2-.5V14a1 1 0 0 1 .293-.707L19 7.586V5H5Z', +}) diff --git a/src/components/icons/Speaker.tsx b/src/components/icons/Speaker.tsx new file mode 100644 index 0000000000..365d5e1143 --- /dev/null +++ b/src/components/icons/Speaker.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const SpeakerVolumeFull_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.472 3.118A1 1 0 0 1 13 4v16a1 1 0 0 1-1.555.832L5.697 17H2a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3.697l5.748-3.832a1 1 0 0 1 1.027-.05ZM11 5.868 6.555 8.833A1 1 0 0 1 6 9H3v6h3a1 1 0 0 1 .555.168L11 18.131V5.87Zm7.364-1.645a1 1 0 0 1 1.414 0A10.969 10.969 0 0 1 23 12c0 3.037-1.232 5.788-3.222 7.778a1 1 0 1 1-1.414-1.414A8.969 8.969 0 0 0 21 12a8.969 8.969 0 0 0-2.636-6.364 1 1 0 0 1 0-1.414Zm-3.182 3.181a1 1 0 0 1 1.414 0A6.483 6.483 0 0 1 18.5 12a6.483 6.483 0 0 1-1.904 4.597 1 1 0 0 1-1.414-1.415A4.483 4.483 0 0 0 16.5 12a4.483 4.483 0 0 0-1.318-3.182 1 1 0 0 1 0-1.414Z', +}) diff --git a/src/components/icons/Trash.tsx b/src/components/icons/Trash.tsx new file mode 100644 index 0000000000..d09a3311ff --- /dev/null +++ b/src/components/icons/Trash.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Trash_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7.416 5H3a1 1 0 0 0 0 2h1.064l.938 14.067A1 1 0 0 0 6 22h12a1 1 0 0 0 .998-.933L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1-.888 0-1.687.386-2.236 1Zm6.087 2H6.07l.867 13h10.128l.867-13h-2.036a1 1 0 0 1-.044 0ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z', +}) diff --git a/src/components/icons/Warning.tsx b/src/components/icons/Warning.tsx new file mode 100644 index 0000000000..fc84b28945 --- /dev/null +++ b/src/components/icons/Warning.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Warning_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z', +}) diff --git a/src/view/com/util/EventStopper.tsx b/src/view/com/util/EventStopper.tsx index e743e89bbe..8f5f5cf54d 100644 --- a/src/view/com/util/EventStopper.tsx +++ b/src/view/com/util/EventStopper.tsx @@ -8,7 +8,14 @@ import {View, ViewStyle} from 'react-native' export function EventStopper({ children, style, -}: React.PropsWithChildren<{style?: ViewStyle | ViewStyle[]}>) { + onKeyDown = true, +}: React.PropsWithChildren<{ + style?: ViewStyle | ViewStyle[] + /** + * Default `true`. Set to `false` to allow onKeyDown to propagate + */ + onKeyDown?: boolean +}>) { const stop = (e: any) => { e.stopPropagation() } @@ -18,7 +25,7 @@ export function EventStopper({ onTouchEnd={stop} // @ts-ignore web only -prf onClick={stop} - onKeyDown={stop} + onKeyDown={onKeyDown ? stop : undefined} style={style}> {children} diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 09850a7f54..6f2ae55b2c 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -1,5 +1,11 @@ import React, {memo} from 'react' -import {StyleProp, View, ViewStyle} from 'react-native' +import { + StyleProp, + ViewStyle, + Pressable, + View, + PressableProps, +} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' @@ -12,10 +18,6 @@ import { import {toShareUrl} from 'lib/strings/url-helpers' import {useTheme} from 'lib/ThemeContext' import {shareUrl} from 'lib/sharing' -import { - NativeDropdown, - DropdownItem as NativeDropdownItem, -} from './NativeDropdown' import * as Toast from '../Toast' import {EventStopper} from '../EventStopper' import {useModalControls} from '#/state/modals' @@ -36,6 +38,19 @@ import {isWeb} from '#/platform/detection' import {richTextToString} from '#/lib/strings/rich-text-helpers' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {atoms as a, useTheme as useAlf, web} from '#/alf' +import * as Menu from '#/components/Menu' +import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' +import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' +import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' +import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' +import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' +import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' +import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' +import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' + let PostDropdownBtn = ({ testID, postAuthor, @@ -45,6 +60,7 @@ let PostDropdownBtn = ({ richText, style, showAppealLabelItem, + hitSlop, }: { testID: string postAuthor: AppBskyActorDefs.ProfileViewBasic @@ -54,9 +70,11 @@ let PostDropdownBtn = ({ richText: RichTextAPI style?: StyleProp showAppealLabelItem?: boolean + hitSlop?: PressableProps['hitSlop'] }): React.ReactNode => { const {hasSession, currentAccount} = useSession() const theme = useTheme() + const alf = useAlf() const {_} = useLingui() const defaultCtrlColor = theme.palette.default.postCtrl const {openModal} = useModalControls() @@ -151,173 +169,189 @@ let PostDropdownBtn = ({ hidePost({uri: postUri}) }, [postUri, hidePost]) - const dropdownItems: NativeDropdownItem[] = [ - { - label: _(msg`Translate`), - onPress() { - onOpenTranslate() - }, - testID: 'postDropdownTranslateBtn', - icon: { - ios: { - name: 'character.book.closed', - }, - android: 'ic_menu_sort_alphabetically', - web: 'language', - }, - }, - { - label: _(msg`Copy post text`), - onPress() { - onCopyPostText() - }, - testID: 'postDropdownCopyTextBtn', - icon: { - ios: { - name: 'doc.on.doc', - }, - android: 'ic_menu_edit', - web: ['far', 'paste'], - }, - }, - { - label: isWeb ? _(msg`Copy link to post`) : _(msg`Share`), - onPress() { - const url = toShareUrl(href) - shareUrl(url) - }, - testID: 'postDropdownShareBtn', - icon: { - ios: { - name: 'square.and.arrow.up', - }, - android: 'ic_menu_share', - web: 'share', - }, - }, - hasSession && { - label: 'separator', - }, - hasSession && { - label: isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`), - onPress() { - onToggleThreadMute() - }, - testID: 'postDropdownMuteThreadBtn', - icon: { - ios: { - name: 'speaker.slash', - }, - android: 'ic_lock_silent_mode', - web: 'comment-slash', - }, - }, - hasSession && { - label: _(msg`Mute words & tags`), - onPress() { - mutedWordsDialogControl.open() - }, - testID: 'postDropdownMuteWordsBtn', - icon: { - ios: { - name: 'speaker.slash', - }, - android: 'ic_lock_silent_mode', - web: 'filter', - }, - }, - hasSession && - !isAuthor && - !isPostHidden && { - label: _(msg`Hide post`), - onPress() { - openModal({ - name: 'confirm', - title: _(msg`Hide this post?`), - message: _(msg`This will hide this post from your feeds.`), - onPressConfirm: onHidePost, - }) - }, - testID: 'postDropdownHideBtn', - icon: { - ios: { - name: 'eye.slash', - }, - android: 'ic_menu_delete', - web: ['far', 'eye-slash'], - }, - }, - { - label: 'separator', - }, - !isAuthor && - hasSession && { - label: _(msg`Report post`), - onPress() { - openModal({ - name: 'report', - uri: postUri, - cid: postCid, - }) - }, - testID: 'postDropdownReportBtn', - icon: { - ios: { - name: 'exclamationmark.triangle', - }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', - }, - }, - isAuthor && { - label: _(msg`Delete post`), - onPress() { - openModal({ - name: 'confirm', - title: _(msg`Delete this post?`), - message: _(msg`Are you sure? This cannot be undone.`), - onPressConfirm: onDeletePost, - }) - }, - testID: 'postDropdownDeleteBtn', - icon: { - ios: { - name: 'trash', - }, - android: 'ic_menu_delete', - web: ['far', 'trash-can'], - }, - }, - showAppealLabelItem && { - label: 'separator', - }, - showAppealLabelItem && { - label: _(msg`Appeal content warning`), - onPress() { - openModal({name: 'appeal-label', uri: postUri, cid: postCid}) - }, - testID: 'postDropdownAppealBtn', - icon: { - ios: { - name: 'exclamationmark.triangle', - }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', - }, - }, - ].filter(Boolean) as NativeDropdownItem[] - return ( - - - - - - + + + + {({props, state}) => { + const styles = [ + style, + a.rounded_full, + (state.hovered || state.focused || state.pressed) && [ + web({outline: 0}), + alf.atoms.bg_contrast_25, + ], + ] + return isWeb ? ( + + + + ) : ( + + + + ) + }} + + + + + + {_(msg`Translate`)} + + + + + {_(msg`Copy post text`)} + + + + { + const url = toShareUrl(href) + shareUrl(url) + }}> + + {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} + + + + + + {hasSession && ( + <> + + + + + + {isThreadMuted + ? _(msg`Unmute thread`) + : _(msg`Mute thread`)} + + + + + mutedWordsDialogControl.open()}> + {_(msg`Mute words & tags`)} + + + + {!isAuthor && !isPostHidden && ( + { + openModal({ + name: 'confirm', + title: _(msg`Hide this post?`), + message: _( + msg`This will hide this post from your feeds.`, + ), + onPressConfirm: onHidePost, + }) + }}> + {_(msg`Hide post`)} + + + )} + + + )} + + + + + {!isAuthor && ( + { + openModal({ + name: 'report', + uri: postUri, + cid: postCid, + }) + }}> + {_(msg`Report post`)} + + + )} + + {isAuthor && ( + { + openModal({ + name: 'confirm', + title: _(msg`Delete this post?`), + message: _(msg`Are you sure? This cannot be undone.`), + onPressConfirm: onDeletePost, + }) + }}> + {_(msg`Delete post`)} + + + )} + + {showAppealLabelItem && ( + <> + + + { + openModal({ + name: 'appeal-label', + uri: postUri, + cid: postCid, + }) + }}> + + {_(msg`Appeal content warning`)} + + + + + )} + + + ) } diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index bd21ddda2f..b1ec32b3f5 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -231,6 +231,7 @@ let PostCtrls = ({ richText={richText} showAppealLabelItem={showAppealLabelItem} style={styles.btnPad} + hitSlop={big ? HITSLOP_20 : HITSLOP_10} /> From 0f9f08b1ef795215975c7b041d0e94a992d22124 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 8 Mar 2024 14:31:24 -0800 Subject: [PATCH 02/13] Fix reactivity of dialogs (Dialogs Pt. 1) (#3146) * Improve a11y on ios * Format * Remove android * Fix android * Revert some changes * use sharedvalue for `importantForAccessibility` * add back `isOpen` * fix some more types --------- Co-authored-by: Eric Bailey --- src/components/Dialog/context.ts | 6 ++-- src/components/Dialog/types.ts | 2 +- src/state/dialogs/index.tsx | 60 ++++++++++++++++++++++---------- src/view/shell/index.tsx | 19 +++------- 4 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 9b571e8e9c..859f8edd77 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -21,8 +21,7 @@ export function useDialogControl(): DialogOuterProps['control'] { open: () => {}, close: () => {}, }) - const {activeDialogs, openDialogs} = useDialogStateContext() - const isOpen = openDialogs.includes(id) + const {activeDialogs} = useDialogStateContext() React.useEffect(() => { activeDialogs.current.set(id, control) @@ -36,7 +35,6 @@ export function useDialogControl(): DialogOuterProps['control'] { () => ({ id, ref: control, - isOpen, open: () => { control.current.open() }, @@ -44,6 +42,6 @@ export function useDialogControl(): DialogOuterProps['control'] { control.current.close(cb) }, }), - [id, control, isOpen], + [id, control], ) } diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index fa9398fe05..4fc60ec394 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -22,7 +22,7 @@ export type DialogControlRefProps = { export type DialogControlProps = DialogControlRefProps & { id: string ref: React.RefObject - isOpen: boolean + isOpen?: boolean } export type DialogContextProps = { diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx index 90aaca4f8b..951105a509 100644 --- a/src/state/dialogs/index.tsx +++ b/src/state/dialogs/index.tsx @@ -1,8 +1,9 @@ import React from 'react' +import {SharedValue, useSharedValue} from 'react-native-reanimated' import {DialogControlRefProps} from '#/components/Dialog' import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' -const DialogContext = React.createContext<{ +interface IDialogContext { /** * The currently active `useDialogControl` hooks. */ @@ -13,13 +14,18 @@ const DialogContext = React.createContext<{ * The currently open dialogs, referenced by their IDs, generated from * `useId`. */ - openDialogs: string[] -}>({ - activeDialogs: { - current: new Map(), - }, - openDialogs: [], -}) + openDialogs: React.MutableRefObject> + /** + * The counterpart to `accessibilityViewIsModal` for Android. This property + * applies to the parent of all non-modal views, and prevents TalkBack from + * navigating within content beneath an open dialog. + * + * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android + */ + importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'> +} + +const DialogContext = React.createContext({} as IDialogContext) const DialogControlContext = React.createContext<{ closeAllDialogs(): boolean @@ -41,26 +47,42 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const activeDialogs = React.useRef< Map> >(new Map()) - const [openDialogs, setOpenDialogs] = React.useState([]) + const openDialogs = React.useRef>(new Set()) + const importantForAccessibility = useSharedValue< + 'auto' | 'no-hide-descendants' + >('auto') const closeAllDialogs = React.useCallback(() => { activeDialogs.current.forEach(dialog => dialog.current.close()) - return openDialogs.length > 0 - }, [openDialogs]) + return openDialogs.current.size > 0 + }, []) const setDialogIsOpen = React.useCallback( (id: string, isOpen: boolean) => { - setOpenDialogs(prev => { - const filtered = prev.filter(dialogId => dialogId !== id) as string[] - return isOpen ? [...filtered, id] : filtered - }) + if (isOpen) { + openDialogs.current.add(id) + importantForAccessibility.value = 'no-hide-descendants' + } else { + openDialogs.current.delete(id) + if (openDialogs.current.size < 1) { + importantForAccessibility.value = 'auto' + } + } }, - [setOpenDialogs], + [importantForAccessibility], ) - const context = React.useMemo( - () => ({activeDialogs, openDialogs}), - [openDialogs], + const context = React.useMemo( + () => ({ + activeDialogs: { + current: new Map(), + }, + openDialogs: { + current: new Set(), + }, + importantForAccessibility, + }), + [importantForAccessibility], ) const controls = React.useMemo( () => ({closeAllDialogs, setDialogIsOpen}), diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index bdba791741..76a7f8fb3a 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -30,7 +30,8 @@ import {useCloseAnyActiveElement} from '#/state/util' import * as notifications from 'lib/notifications/notifications' import {Outlet as PortalOutlet} from '#/components/Portal' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' -import {useDialogStateContext} from '#/state/dialogs' +import {useDialogStateContext} from 'state/dialogs' +import Animated from 'react-native-reanimated' function ShellInner() { const isDrawerOpen = useIsDrawerOpen() @@ -54,9 +55,9 @@ function ShellInner() { const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const {hasSession, currentAccount} = useSession() const closeAnyActiveElement = useCloseAnyActiveElement() + const {importantForAccessibility} = useDialogStateContext() // start undefined const currentAccountDid = React.useRef(undefined) - const {openDialogs} = useDialogStateContext() React.useEffect(() => { let listener = {remove() {}} @@ -80,19 +81,9 @@ function ShellInner() { } }, [currentAccount]) - /** - * The counterpart to `accessibilityViewIsModal` for Android. This property - * applies to the parent of all non-modal views, and prevents TalkBack from - * navigating within content beneath an open dialog. - * - * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android - */ - const importantForAccessibility = - openDialogs.length > 0 ? 'no-hide-descendants' : undefined - return ( <> - @@ -106,7 +97,7 @@ function ShellInner() { - + From 8ee325e73d927a34ef23a776c5404e9556f0d94a Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 8 Mar 2024 14:31:50 -0800 Subject: [PATCH 03/13] Make ALF prompt scrollable for accessibility (#3150) * make alf prompt scrollable * padding --- src/components/Prompt.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 8e55bd8347..28ec2d0366 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -3,7 +3,7 @@ import {View, PressableProps} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useTheme, atoms as a} from '#/alf' +import {useTheme, atoms as a, useBreakpoints} from '#/alf' import {Text} from '#/components/Typography' import {Button} from '#/components/Button' @@ -25,6 +25,7 @@ export function Outer({ }: React.PropsWithChildren<{ control: Dialog.DialogOuterProps['control'] }>) { + const {gtMobile} = useBreakpoints() const titleId = React.useId() const descriptionId = React.useId() @@ -38,12 +39,12 @@ export function Outer({ - + style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}> {children} - + ) @@ -71,8 +72,17 @@ export function Description({children}: React.PropsWithChildren<{}>) { } export function Actions({children}: React.PropsWithChildren<{}>) { + const {gtMobile} = useBreakpoints() + return ( - + {children} ) From 62e57c3b08020e17b3266876de342996c8bd12db Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 8 Mar 2024 14:43:28 -0800 Subject: [PATCH 04/13] Adjustments to ALF prompt buttons (Dialogs Pt. 2) (#3144) * Improve a11y on ios * Format * Remove android * Fix android * small adjustment to buttons in prompt * full width below gtmobile * Revert some changes * use sharedvalue for `importantForAccessibility` * add back `isOpen` * fix some more types * small adjustment to buttons in prompt * full width below gtmobile --------- Co-authored-by: Eric Bailey --- src/components/Button.tsx | 4 +++- src/components/Prompt.tsx | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 5361be963a..d3bf73cc3e 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -27,7 +27,7 @@ export type ButtonColor = | 'gradient_sunset' | 'gradient_nordic' | 'gradient_bonfire' -export type ButtonSize = 'tiny' | 'small' | 'large' +export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large' export type ButtonShape = 'round' | 'square' | 'default' export type VariantProps = { /** @@ -274,6 +274,8 @@ export function Button({ if (shape === 'default') { if (size === 'large') { baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) + } else if (size === 'medium') { + baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md) } else if (size === 'small') { baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) } else if (size === 'tiny') { diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 28ec2d0366..3b245c440f 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -78,10 +78,9 @@ export function Actions({children}: React.PropsWithChildren<{}>) { {children} @@ -92,12 +91,13 @@ export function Cancel({ children, }: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) { const {_} = useLingui() + const {gtMobile} = useBreakpoints() const {close} = Dialog.useDialogContext() return ( + + + + ) + } + + return likes.length ? ( + item.actor.did} + refreshing={isPTRing} + onRefresh={onRefresh} + onEndReached={onEndReached} + renderItem={renderItem} + initialNumToRender={15} + contentContainerStyle={{borderWidth: 0}} + // FIXME(dan) + // eslint-disable-next-line react/no-unstable-nested-components + ListFooterComponent={() => ( + + {(isFetching || isFetchingNextPage) && } + + )} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + ) : ( + + + + + Nobody has liked this yet. Maybe you should be the first! + + + + + ) +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 59495401d5..95af2f237d 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -21,6 +21,7 @@ export type CommonNavigatorParams = { PostRepostedBy: {name: string; rkey: string} ProfileFeed: {name: string; rkey: string} ProfileFeedLikedBy: {name: string; rkey: string} + ProfileLabelerLikedBy: {name: string} Debug: undefined DebugMod: undefined Log: undefined diff --git a/src/routes.ts b/src/routes.ts index 23ec880de2..e5326b0fdc 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -21,6 +21,7 @@ export const router = new Router({ PostRepostedBy: '/profile/:name/post/:rkey/reposted-by', ProfileFeed: '/profile/:name/feed/:rkey', ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by', + ProfileLabelerLikedBy: '/profile/:name/labeler/liked-by', Debug: '/sys/debug', DebugMod: '/sys/debug-mod', Log: '/sys/log', diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index b6e1de28fd..2c3a99891f 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -35,10 +35,9 @@ import { Heart2_Stroke2_Corner0_Rounded as Heart, Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, } from '#/components/icons/Heart2' -import {LikesDialog} from '#/components/LikesDialog' -import * as Dialog from '#/components/Dialog' import {DialogOuterProps} from '#/components/Dialog' import * as Prompt from '#/components/Prompt' +import {Link} from '#/components/Link' interface Props { profile: AppBskyActorDefs.ProfileViewDetailed @@ -64,7 +63,6 @@ let ProfileHeaderLabeler = ({ const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() const {track} = useAnalytics() - const likesControl = Dialog.useDialogControl() const cantSubscribePrompt = Prompt.usePromptControl() const moderation = useMemo( @@ -237,9 +235,14 @@ let ProfileHeaderLabeler = ({ {typeof labeler.likeCount === 'number' && ( - + )} )} - ) diff --git a/src/screens/Profile/ProfileLabelerLikedBy.tsx b/src/screens/Profile/ProfileLabelerLikedBy.tsx new file mode 100644 index 0000000000..6ea5d580ac --- /dev/null +++ b/src/screens/Profile/ProfileLabelerLikedBy.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' + +import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types' +import {ViewHeader} from '#/view/com/util/ViewHeader' +import {LikedByList as PostLikedByComponent} from '#/components/LikedByList' +import {useSetMinimalShellMode} from '#/state/shell' +import {makeRecordUri} from '#/lib/strings/url-helpers' + +import {atoms as a, useTheme, useBreakpoints} from '#/alf' + +export function ProfileLabelerLikedByScreen({ + route, +}: NativeStackScreenProps) { + const setMinimalShellMode = useSetMinimalShellMode() + const {name: handleOrDid} = route.params + const uri = makeRecordUri(handleOrDid, 'app.bsky.labeler.service', 'self') + const {_} = useLingui() + const t = useTheme() + const {gtMobile} = useBreakpoints() + + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) + + return ( + + + + + ) +} From db5749124f8f70b9910ba125e85bb46fe4073390 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sun, 10 Mar 2024 12:31:21 -0500 Subject: [PATCH 12/13] Fix optimistic like --- .../Profile/Header/ProfileHeaderLabeler.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 2c3a99891f..c7ca56e598 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -84,9 +84,7 @@ let ProfileHeaderLabeler = ({ const [likeUri, setLikeUri] = React.useState( labeler.viewer?.like || '', ) - const isLiked = !!likeUri - const likeCount = - isLiked && likeUri ? (labeler.likeCount || 0) + 1 : labeler.likeCount || 0 + const [likeCount, setLikeCount] = React.useState(labeler.likeCount || 0) const onToggleLiked = React.useCallback(async () => { if (!labeler) { @@ -95,13 +93,15 @@ let ProfileHeaderLabeler = ({ try { Haptics.default() - if (isLiked && likeUri) { + if (likeUri) { await unlikeMod({uri: likeUri}) track('CustomFeed:Unlike') + setLikeCount(c => c - 1) setLikeUri('') } else { const res = await likeMod({uri: labeler.uri, cid: labeler.cid}) track('CustomFeed:Like') + setLikeCount(c => c + 1) setLikeUri(res.uri) } } catch (e: any) { @@ -112,7 +112,7 @@ let ProfileHeaderLabeler = ({ ) logger.error(`Failed to toggle labeler like`, {message: e.message}) } - }, [labeler, likeUri, isLiked, likeMod, unlikeMod, track, _]) + }, [labeler, likeUri, likeMod, unlikeMod, track, _]) const onPressEditProfile = React.useCallback(() => { track('ProfileHeader:EditProfileButtonClicked') @@ -227,14 +227,14 @@ let ProfileHeaderLabeler = ({ label={_(msg`Like this feed`)} disabled={!hasSession || isLikePending || isUnlikePending} onPress={onToggleLiked}> - {isLiked ? ( + {likeUri ? ( ) : ( )} - {typeof labeler.likeCount === 'number' && ( + {typeof likeCount === 'number' && ( Date: Sun, 10 Mar 2024 12:35:16 -0500 Subject: [PATCH 13/13] Cleanup --- src/screens/Profile/ProfileLabelerLikedBy.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/screens/Profile/ProfileLabelerLikedBy.tsx b/src/screens/Profile/ProfileLabelerLikedBy.tsx index 6ea5d580ac..0b13139f9a 100644 --- a/src/screens/Profile/ProfileLabelerLikedBy.tsx +++ b/src/screens/Profile/ProfileLabelerLikedBy.tsx @@ -6,7 +6,7 @@ import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types' import {ViewHeader} from '#/view/com/util/ViewHeader' -import {LikedByList as PostLikedByComponent} from '#/components/LikedByList' +import {LikedByList} from '#/components/LikedByList' import {useSetMinimalShellMode} from '#/state/shell' import {makeRecordUri} from '#/lib/strings/url-helpers' @@ -43,8 +43,8 @@ export function ProfileLabelerLikedByScreen({ }, ], ]}> - - + + ) }