diff options
author | Ansh <anshnanda10@gmail.com> | 2023-07-28 14:00:37 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-28 16:00:37 -0500 |
commit | 3b8b5622688807f6d04c52cbd4d6977b203b75b3 (patch) | |
tree | d14739a4cf680efead0f7dc63428f9ad88d7d5ef | |
parent | eec300d77241925e6b42e5e7e51894f2cba50e18 (diff) | |
download | voidsky-3b8b5622688807f6d04c52cbd4d6977b203b75b3.tar.zst |
[APP-737] Accessible native dropdown menu (#988)
* fix comments * add zeego package * get basic native dropdown working * add separator and icon components * refined native dropdown component * add android build properties to app.json * move `PostDropdownBtn` to its own component * fix selectors issue * move `PostDropdownBtn` to its own component * fix hitslop * fix post dropdown hitslop * fix android dropdown icons * move `UserAvatar.tsx` to native dropdown * use native dropdown in `ProfileHeader.tsx` * use native dropdown in `PostThreadItem.tsx` * use native dropdown in `UserBanner.tsx` * use native dropdown in `CustomFeed.tsx` * replace `testId` with `testID` (which is what is used everywhere) * move `Settings.tsx` to use native dropdown * create jest mocks for zeego * create jest mock for `zeego/dropdown-menu` * web styles for native dropdown * remove example native dropdown * adjust web styles * fix propagation * fix pressable in `Settings.tsx` * animate dropdown on web * add keyboard nav and hover styles * add hitslop to constants * add comments to NativeDropdown component * temporarily removed android icons * add testID to PostDropdownBtn * add testID back to all NativeDropdown button implementations * add postDropdownBtn testID * add testID to dropdown items * remove testID from dropdown menu item * refactor home-screen tests for native dropdown * refactor profile-screen tests for native dropdown * refactor thread-muting tests for native dropdown * refactor thread-screen tests for native dropdown * fix dropdown color for post dropdown button * remove icons from android dropdown menu * fix `create-account.test.ts` * fix `invite-codes.test.ts`
30 files changed, 1091 insertions, 340 deletions
diff --git a/__e2e__/tests/create-account.test.ts b/__e2e__/tests/create-account.test.ts index 7db4e912a..8706fae7c 100644 --- a/__e2e__/tests/create-account.test.ts +++ b/__e2e__/tests/create-account.test.ts @@ -25,6 +25,8 @@ describe('Create account', () => { await element(by.id('handleInput')).typeText('e2e-test') await device.takeScreenshot('4- entered handle') await element(by.id('nextBtn')).tap() + await expect(element(by.id('welcomeScreen'))).toBeVisible() + await element(by.id('continueBtn')).tap() await expect(element(by.id('homeScreen'))).toBeVisible() }) }) diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts index 7fa9ff28c..d0eeb6703 100644 --- a/__e2e__/tests/home-screen.test.ts +++ b/__e2e__/tests/home-screen.test.ts @@ -55,7 +55,7 @@ describe('Home screen', () => { await element(by.id('postDropdownBtn').withAncestor(carlaPosts)) .atIndex(0) .tap() - await element(by.id('postDropdownReportBtn')).tap() + await element(by.text('Report post')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() await element( by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), @@ -84,7 +84,7 @@ describe('Home screen', () => { await element(by.id('postDropdownBtn').withAncestor(alicePosts)) .atIndex(0) .tap() - await element(by.id('postDropdownDeleteBtn')).tap() + await element(by.text('Delete post')).tap() await expect(element(by.id('confirmModal'))).toBeVisible() await element(by.id('confirmBtn')).tap() await expect( diff --git a/__e2e__/tests/invite-codes.test.ts b/__e2e__/tests/invite-codes.test.ts index 846d3b768..74b80a8d0 100644 --- a/__e2e__/tests/invite-codes.test.ts +++ b/__e2e__/tests/invite-codes.test.ts @@ -42,6 +42,8 @@ describe('invite-codes', () => { await element(by.id('handleInput')).typeText('e2e-test') await device.takeScreenshot('4- entered handle') await element(by.id('nextBtn')).tap() + await expect(element(by.id('welcomeScreen'))).toBeVisible() + await element(by.id('continueBtn')).tap() await expect(element(by.id('homeScreen'))).toBeVisible() await element(by.id('viewHeaderDrawerBtn')).tap() await element(by.id('menuItemButton-Settings')).tap() diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts index a7bb93656..6c6d6db9c 100644 --- a/__e2e__/tests/profile-screen.test.ts +++ b/__e2e__/tests/profile-screen.test.ts @@ -62,10 +62,10 @@ describe('Profile screen', () => { await element(by.id('profileHeaderEditProfileButton')).tap() await expect(element(by.id('editProfileModal'))).toBeVisible() await element(by.id('changeBannerBtn')).tap() - await element(by.id('changeBannerLibraryBtn')).tap() + await element(by.text('Library')).tap() await sleep(3e3) await element(by.id('changeAvatarBtn')).tap() - await element(by.id('changeAvatarLibraryBtn')).tap() + await element(by.text('Library')).tap() await sleep(3e3) await element(by.id('editProfileSaveBtn')).tap() await expect(element(by.id('editProfileModal'))).not.toBeVisible() @@ -79,9 +79,9 @@ describe('Profile screen', () => { await element(by.id('profileHeaderEditProfileButton')).tap() await expect(element(by.id('editProfileModal'))).toBeVisible() await element(by.id('changeBannerBtn')).tap() - await element(by.id('changeBannerRemoveBtn')).tap() + await element(by.text('Remove')).tap() await element(by.id('changeAvatarBtn')).tap() - await element(by.id('changeAvatarRemoveBtn')).tap() + await element(by.text('Remove')).tap() await element(by.id('editProfileSaveBtn')).tap() await expect(element(by.id('editProfileModal'))).not.toBeVisible() await expect(element(by.id('userBannerFallback'))).toExist() @@ -109,16 +109,16 @@ describe('Profile screen', () => { it('Can mute/unmute another user', async () => { await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist() await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.id('profileHeaderDropdownMuteBtn')).tap() + await element(by.text('Mute Account')).tap() await expect(element(by.id('profileHeaderMutedNotice'))).toBeVisible() await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.id('profileHeaderDropdownMuteBtn')).tap() + await element(by.text('Unmute Account')).tap() await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist() }) it('Can report another user', async () => { await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.id('profileHeaderDropdownReportBtn')).tap() + await element(by.text('Report Account')).tap() await expect(element(by.id('reportAccountModal'))).toBeVisible() await element( by.id('reportAccountRadios-com.atproto.moderation.defs#reasonSpam'), @@ -166,7 +166,7 @@ describe('Profile screen', () => { it('Can report posts', async () => { const posts = by.id('feedItem-by-bob.test') await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap() - await element(by.id('postDropdownReportBtn')).tap() + await element(by.text('Report post')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() await element( by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), diff --git a/__e2e__/tests/thread-muting.test.ts b/__e2e__/tests/thread-muting.test.ts index a5cefdb26..8acd9d81f 100644 --- a/__e2e__/tests/thread-muting.test.ts +++ b/__e2e__/tests/thread-muting.test.ts @@ -45,7 +45,7 @@ describe('Thread muting', () => { await element(by.id('postDropdownBtn').withAncestor(bobNotifs)) .atIndex(0) .tap() - await element(by.id('postDropdownMuteThreadBtn')).tap() + await element(by.text('Mute thread')).tap() // have to wait for the toast to clear await waitFor(element(by.id('viewHeaderDrawerBtn'))) .toBeVisible() @@ -93,7 +93,7 @@ describe('Thread muting', () => { await element(by.id('postDropdownBtn').withAncestor(alicePosts)) .atIndex(0) .tap() - await element(by.id('postDropdownMuteThreadBtn')).tap() + await element(by.text('Mute thread')).tap() // TODO // the swipe down to trigger PTR isnt working and I dont want to block on this diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts index 8d3eacc88..081282a36 100644 --- a/__e2e__/tests/thread-screen.test.ts +++ b/__e2e__/tests/thread-screen.test.ts @@ -104,7 +104,7 @@ describe('Thread screen', () => { it('Can report the root post', async () => { const post = by.id('postThreadItem-by-bob.test') await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() - await element(by.id('postDropdownReportBtn')).tap() + await element(by.text('Report post')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() await element( by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), @@ -116,7 +116,7 @@ describe('Thread screen', () => { it('Can report a reply post', async () => { const post = by.id('postThreadItem-by-carla.test') await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() - await element(by.id('postDropdownReportBtn')).tap() + await element(by.text('Report post')).tap() await expect(element(by.id('reportPostModal'))).toBeVisible() await element( by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), diff --git a/__mocks__/zeego/dropdown-menu.js b/__mocks__/zeego/dropdown-menu.js new file mode 100644 index 000000000..1d51addca --- /dev/null +++ b/__mocks__/zeego/dropdown-menu.js @@ -0,0 +1,2 @@ +export const DropdownMenu = jest.fn().mockImplementation(() => {}) +export const create = jest.fn().mockImplementation(() => {}) diff --git a/app.json b/app.json index 2513fbba4..be643334b 100644 --- a/app.json +++ b/app.json @@ -80,6 +80,8 @@ { "android": { "compileSdkVersion": 34, + "targetSdkVersion": 34, + "buildToolsVersion": "34.0.0", "kotlinVersion": "1.8.0" } } diff --git a/package.json b/package.json index 4bb540878..1ede7bf10 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@react-native-clipboard/clipboard": "^1.10.0", "@react-native-community/blur": "^4.3.0", "@react-native-community/datetimepicker": "6.7.3", + "@react-native-menu/menu": "^0.8.0", "@react-navigation/bottom-tabs": "^6.5.7", "@react-navigation/drawer": "^6.6.2", "@react-navigation/native": "^6.1.6", @@ -120,6 +121,7 @@ "react-native-haptic-feedback": "^1.14.0", "react-native-image-crop-picker": "^0.38.1", "react-native-inappbrowser-reborn": "^3.6.3", + "react-native-ios-context-menu": "^1.15.3", "react-native-linear-gradient": "^2.6.2", "react-native-pager-view": "6.1.4", "react-native-progress": "bluesky-social/react-native-progress", @@ -139,6 +141,7 @@ "sentry-expo": "~6.1.0", "tippy.js": "^6.3.7", "tlds": "^1.234.0", + "zeego": "^1.6.2", "zod": "^3.20.2" }, "devDependencies": { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 34da35e4f..001cdf8c3 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,3 +1,5 @@ +import {Insets} from 'react-native' + const HELP_DESK_LANG = 'en-us' export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}` @@ -134,3 +136,15 @@ export function LINK_META_PROXY(serviceUrl: string) { } export const STATUS_PAGE_URL = 'https://status.bsky.app/' + +// Hitslop constants +export const createHitslop = (size: number): Insets => ({ + top: size, + left: size, + bottom: size, + right: size, +}) +export const HITSLOP_10 = createHitslop(10) +export const HITSLOP_20 = createHitslop(20) +export const HITSLOP_30 = createHitslop(30) +export const BACK_HITSLOP = HITSLOP_30 diff --git a/src/view/com/auth/onboarding/Welcome.tsx b/src/view/com/auth/onboarding/Welcome.tsx index e7c068ea0..87435c88a 100644 --- a/src/view/com/auth/onboarding/Welcome.tsx +++ b/src/view/com/auth/onboarding/Welcome.tsx @@ -10,7 +10,7 @@ export const Welcome = ({next}: {next: () => void}) => { const pal = usePalette('default') return ( <View style={[styles.container]}> - <View> + <View testID="welcomeScreen"> <Text style={[pal.text, styles.title]}>Welcome to </Text> <Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text> @@ -52,7 +52,12 @@ export const Welcome = ({next}: {next: () => void}) => { </View> </View> - <Button onPress={next} label="Continue" labelStyle={styles.buttonText} /> + <Button + onPress={next} + label="Continue" + testID="continueBtn" + labelStyle={styles.buttonText} + /> </View> ) } diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index 0f955984d..d58b17c58 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -10,11 +10,9 @@ import {useStores} from 'state/index' import {isDesktopWeb} from 'platform/detection' import {openCamera} from 'lib/media/picker' import {useCameraPermission} from 'lib/hooks/usePermissions' -import {POST_IMG_MAX} from 'lib/constants' +import {HITSLOP_10, POST_IMG_MAX} from 'lib/constants' import {GalleryModel} from 'state/models/media/gallery' -const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} - type Props = { gallery: GalleryModel } @@ -54,7 +52,7 @@ export function OpenCameraBtn({gallery}: Props) { testID="openCameraButton" onPress={onPressTakePicture} style={styles.button} - hitSlop={HITSLOP} + hitSlop={HITSLOP_10} accessibilityRole="button" accessibilityLabel="Camera" accessibilityHint="Opens camera on device"> diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx index aaf0477c7..081456f75 100644 --- a/src/view/com/composer/photos/SelectPhotoBtn.tsx +++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx @@ -9,8 +9,7 @@ import {useAnalytics} from 'lib/analytics/analytics' import {isDesktopWeb} from 'platform/detection' import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' import {GalleryModel} from 'state/models/media/gallery' - -const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} +import {HITSLOP_10} from 'lib/constants' type Props = { gallery: GalleryModel @@ -36,7 +35,7 @@ export function SelectPhotoBtn({gallery}: Props) { testID="openGalleryBtn" onPress={onPressSelectPhotos} style={styles.button} - hitSlop={HITSLOP} + hitSlop={HITSLOP_10} accessibilityRole="button" accessibilityLabel="Gallery" accessibilityHint="Opens device photo gallery"> diff --git a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx index 84e5f90fb..c95538c55 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx @@ -6,6 +6,7 @@ * */ +import {createHitslop} from 'lib/constants' import React from 'react' import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native' @@ -13,7 +14,7 @@ type Props = { onRequestClose: () => void } -const HIT_SLOP = {top: 16, left: 16, bottom: 16, right: 16} +const HIT_SLOP = createHitslop(16) const ImageDefaultHeader = ({onRequestClose}: Props) => ( <SafeAreaView style={styles.root}> diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 621173567..55a38803f 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -12,6 +12,7 @@ import {Text} from '../util/text/Text' import {CogIcon} from 'lib/icons' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {s} from 'lib/styles' +import {HITSLOP_10} from 'lib/constants' export const FeedsTabBar = observer( ( @@ -54,7 +55,7 @@ export const FeedsTabBar = observer( accessibilityRole="button" accessibilityLabel="Open navigation" accessibilityHint="Access profile and other navigation links" - hitSlop={10}> + hitSlop={HITSLOP_10}> <FontAwesomeIcon icon="bars" size={18} @@ -68,7 +69,7 @@ export const FeedsTabBar = observer( <View style={[pal.view]}> <Link href="/settings/saved-feeds" - hitSlop={10} + hitSlop={HITSLOP_10} accessibilityRole="button" accessibilityLabel="Edit Saved Feeds" accessibilityHint="Opens screen to edit Saved Feeds"> diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 0680bbc06..edf8d7749 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -11,7 +11,7 @@ import {PostThreadItemModel} from 'state/models/content/post-thread-item' import {Link} from '../util/Link' import {RichText} from '../util/text/RichText' import {Text} from '../util/text/Text' -import {PostDropdownBtn} from '../util/forms/DropdownButton' +import {PostDropdownBtn} from '../util/forms/PostDropdownBtn' import * as Toast from '../util/Toast' import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' @@ -202,7 +202,6 @@ export const PostThreadItem = observer(function PostThreadItem({ <View style={s.flex1} /> <PostDropdownBtn testID="postDropdownBtn" - style={[styles.metaItem, s.mt2, s.px5]} itemUri={itemUri} itemCid={itemCid} itemHref={itemHref} @@ -212,13 +211,8 @@ export const PostThreadItem = observer(function PostThreadItem({ onCopyPostText={onCopyPostText} onOpenTranslate={onOpenTranslate} onToggleThreadMute={onToggleThreadMute} - onDeletePost={onDeletePost}> - <FontAwesomeIcon - icon="ellipsis-h" - size={14} - style={[pal.textLight]} - /> - </PostDropdownBtn> + onDeletePost={onDeletePost} + /> </View> <View style={styles.meta}> <Link diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index b54aa64f4..a372f0d81 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -17,7 +17,6 @@ import {toShareUrl} from 'lib/strings/url-helpers' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {s, colors} from 'lib/styles' -import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton' import * as Toast from '../util/Toast' import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {Text} from '../util/text/Text' @@ -36,11 +35,11 @@ import {FollowState} from 'state/models/cache/my-follows' import {shareUrl} from 'lib/sharing' import {formatCount} from '../util/numeric/format' import {navigate} from '../../../Navigation' +import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown' +import {BACK_HITSLOP} from 'lib/constants' import {isInvalidHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' -const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30} - interface Props { view: ProfileModel onRefreshAll: () => void @@ -260,15 +259,29 @@ const ProfileHeaderLoaded = observer( testID: 'profileHeaderDropdownShareBtn', label: 'Share', onPress: onPressShare, + icon: { + ios: { + name: 'square.and.arrow.up', + }, + android: 'ic_menu_share', + web: 'share', + }, }, ] if (!isMe) { - items.push({sep: true}) + items.push({label: 'separator'}) // Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self! items.push({ testID: 'profileHeaderDropdownListAddRemoveBtn', label: 'Add to Lists', onPress: onPressAddRemoveLists, + icon: { + ios: { + name: 'list.bullet', + }, + android: 'ic_menu_add', + web: 'list', + }, }) if (!view.viewer.blocking) { items.push({ @@ -277,6 +290,13 @@ const ProfileHeaderLoaded = observer( onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount, + icon: { + ios: { + name: 'speaker.slash', + }, + android: 'ic_lock_silent_mode', + web: 'comment-slash', + }, }) } items.push({ @@ -285,11 +305,25 @@ const ProfileHeaderLoaded = observer( onPress: view.viewer.blocking ? onPressUnblockAccount : onPressBlockAccount, + icon: { + ios: { + name: 'person.fill.xmark', + }, + android: 'ic_menu_close_clear_cancel', + web: 'user-slash', + }, }) items.push({ testID: 'profileHeaderDropdownReportBtn', label: 'Report Account', onPress: onPressReportAccount, + icon: { + ios: { + name: 'exclamationmark.triangle', + }, + android: 'ic_menu_report_image', + web: 'circle-exclamation', + }, }) } return items @@ -380,13 +414,17 @@ const ProfileHeaderLoaded = observer( </> ) : null} {dropdownItems?.length ? ( - <DropdownButton + <NativeDropdown testID="profileHeaderDropdownBtn" - type="bare" - items={dropdownItems} - style={[styles.btn, styles.secondaryBtn, pal.btn]}> - <FontAwesomeIcon icon="ellipsis" style={[pal.text]} /> - </DropdownButton> + items={dropdownItems}> + <View style={[styles.btn, styles.secondaryBtn, pal.btn]}> + <FontAwesomeIcon + icon="ellipsis" + size={20} + style={[pal.text]} + /> + </View> + </NativeDropdown> ) : undefined} </View> <View> diff --git a/src/view/com/search/HeaderWithInput.tsx b/src/view/com/search/HeaderWithInput.tsx index 2ec079dde..f825c578e 100644 --- a/src/view/com/search/HeaderWithInput.tsx +++ b/src/view/com/search/HeaderWithInput.tsx @@ -10,8 +10,7 @@ import {useTheme} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' import {useStores} from 'state/index' import {useAnalytics} from 'lib/analytics/analytics' - -const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} +import {HITSLOP_10} from 'lib/constants' interface Props { isInputFocused: boolean @@ -55,7 +54,7 @@ export function HeaderWithInput({ <TouchableOpacity testID="viewHeaderBackOrMenuBtn" onPress={onPressMenu} - hitSlop={MENU_HITSLOP} + hitSlop={HITSLOP_10} style={styles.headerMenuBtn} accessibilityRole="button" accessibilityLabel="Menu" diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index eb6405f10..d999ffb31 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -2,7 +2,6 @@ import React, {useMemo} from 'react' import {StyleSheet, View} from 'react-native' import Svg, {Circle, Rect, Path} from 'react-native-svg' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {IconProp} from '@fortawesome/fontawesome-svg-core' import {HighPriorityImage} from 'view/com/util/images/Image' import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' import { @@ -11,12 +10,12 @@ import { } from 'lib/hooks/usePermissions' import {useStores} from 'state/index' import {colors} from 'lib/styles' -import {DropdownButton} from './forms/DropdownButton' import {usePalette} from 'lib/hooks/usePalette' import {isWeb, isAndroid} from 'platform/detection' import {Image as RNImage} from 'react-native-image-crop-picker' import {AvatarModeration} from 'lib/labeling/types' import {UserPreviewLink} from './UserPreviewLink' +import {DropdownItem, NativeDropdown} from './forms/NativeDropdown' type Type = 'user' | 'algo' | 'list' @@ -130,59 +129,81 @@ export function UserAvatar({ }, [type, size]) const dropdownItems = useMemo( - () => [ - !isWeb && { - testID: 'changeAvatarCameraBtn', - label: 'Camera', - icon: 'camera' as IconProp, - onPress: async () => { - if (!(await requestCameraAccessIfNeeded())) { - return - } + () => + [ + !isWeb && { + testID: 'changeAvatarCameraBtn', + label: 'Camera', + icon: { + ios: { + name: 'camera', + }, + android: 'ic_menu_camera', + web: 'camera', + }, + onPress: async () => { + if (!(await requestCameraAccessIfNeeded())) { + return + } - onSelectNewAvatar?.( - await openCamera(store, { - width: 1000, - height: 1000, - cropperCircleOverlay: true, - }), - ) + onSelectNewAvatar?.( + await openCamera(store, { + width: 1000, + height: 1000, + cropperCircleOverlay: true, + }), + ) + }, }, - }, - { - testID: 'changeAvatarLibraryBtn', - label: 'Library', - icon: 'image' as IconProp, - onPress: async () => { - if (!(await requestPhotoAccessIfNeeded())) { - return - } + { + testID: 'changeAvatarLibraryBtn', + label: 'Library', + icon: { + ios: { + name: 'photo.on.rectangle.angled', + }, + android: 'ic_menu_gallery', + web: 'gallery', + }, + onPress: async () => { + if (!(await requestPhotoAccessIfNeeded())) { + return + } - const items = await openPicker({ - aspect: [1, 1], - }) - const item = items[0] + const items = await openPicker({ + aspect: [1, 1], + }) + const item = items[0] - const croppedImage = await openCropper(store, { - mediaType: 'photo', - cropperCircleOverlay: true, - height: item.height, - width: item.width, - path: item.path, - }) + const croppedImage = await openCropper(store, { + mediaType: 'photo', + cropperCircleOverlay: true, + height: item.height, + width: item.width, + path: item.path, + }) - onSelectNewAvatar?.(croppedImage) + onSelectNewAvatar?.(croppedImage) + }, }, - }, - !!avatar && { - testID: 'changeAvatarRemoveBtn', - label: 'Remove', - icon: ['far', 'trash-can'] as IconProp, - onPress: async () => { - onSelectNewAvatar?.(null) + !!avatar && { + label: 'separator', }, - }, - ], + !!avatar && { + testID: 'changeAvatarRemoveBtn', + label: 'Remove', + icon: { + ios: { + name: 'trash', + }, + android: 'ic_delete', + web: 'trash', + }, + onPress: async () => { + onSelectNewAvatar?.(null) + }, + }, + ].filter(Boolean) as DropdownItem[], [ avatar, onSelectNewAvatar, @@ -209,14 +230,7 @@ export function UserAvatar({ // onSelectNewAvatar is only passed as prop on the EditProfile component return onSelectNewAvatar ? ( - <DropdownButton - testID="changeAvatarBtn" - type="bare" - items={dropdownItems} - openToRight - rightOffset={-10} - bottomOffset={-10} - menuWidth={170}> + <NativeDropdown testID="changeAvatarBtn" items={dropdownItems}> {avatar ? ( <HighPriorityImage testID="userAvatarImage" @@ -234,7 +248,7 @@ export function UserAvatar({ color={pal.text.color as string} /> </View> - </DropdownButton> + </NativeDropdown> ) : avatar && !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( <View style={{width: size, height: size}}> diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index cce0e839b..b7e91b5dd 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -1,7 +1,6 @@ -import React from 'react' +import React, {useMemo} from 'react' import {StyleSheet, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {IconProp} from '@fortawesome/fontawesome-svg-core' import {Image} from 'expo-image' import {colors} from 'lib/styles' import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' @@ -10,11 +9,11 @@ import { usePhotoLibraryPermission, useCameraPermission, } from 'lib/hooks/usePermissions' -import {DropdownButton} from './forms/DropdownButton' import {usePalette} from 'lib/hooks/usePalette' import {AvatarModeration} from 'lib/labeling/types' import {isWeb, isAndroid} from 'platform/detection' import {Image as RNImage} from 'react-native-image-crop-picker' +import {NativeDropdown, DropdownItem} from './forms/NativeDropdown' export function UserBanner({ banner, @@ -30,63 +29,84 @@ export function UserBanner({ const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() - const dropdownItems = [ - !isWeb && { - testID: 'changeBannerCameraBtn', - label: 'Camera', - icon: 'camera' as IconProp, - onPress: async () => { - if (!(await requestCameraAccessIfNeeded())) { - return - } - onSelectNewBanner?.( - await openCamera(store, { - width: 3000, - height: 1000, - }), - ) - }, - }, - { - testID: 'changeBannerLibraryBtn', - label: 'Library', - icon: 'image' as IconProp, - onPress: async () => { - if (!(await requestPhotoAccessIfNeeded())) { - return - } - const items = await openPicker() + const dropdownItems: DropdownItem[] = useMemo( + () => + [ + !isWeb && { + testID: 'changeBannerCameraBtn', + label: 'Camera', + icon: { + ios: { + name: 'camera', + }, + android: 'ic_menu_camera', + web: 'camera', + }, + onPress: async () => { + if (!(await requestCameraAccessIfNeeded())) { + return + } + onSelectNewBanner?.( + await openCamera(store, { + width: 3000, + height: 1000, + }), + ) + }, + }, + { + testID: 'changeBannerLibraryBtn', + label: 'Library', + icon: { + ios: { + name: 'photo.on.rectangle.angled', + }, + android: 'ic_menu_gallery', + web: 'gallery', + }, + onPress: async () => { + if (!(await requestPhotoAccessIfNeeded())) { + return + } + const items = await openPicker() - onSelectNewBanner?.( - await openCropper(store, { - mediaType: 'photo', - path: items[0].path, - width: 3000, - height: 1000, - }), - ) - }, - }, - !!banner && { - testID: 'changeBannerRemoveBtn', - label: 'Remove', - icon: ['far', 'trash-can'] as IconProp, - onPress: () => { - onSelectNewBanner?.(null) - }, - }, - ] + onSelectNewBanner?.( + await openCropper(store, { + mediaType: 'photo', + path: items[0].path, + width: 3000, + height: 1000, + }), + ) + }, + }, + !!banner && { + testID: 'changeBannerRemoveBtn', + label: 'Remove', + icon: { + ios: { + name: 'trash', + }, + android: 'ic_delete', + web: 'trash', + }, + onPress: () => { + onSelectNewBanner?.(null) + }, + }, + ].filter(Boolean) as DropdownItem[], + [ + banner, + onSelectNewBanner, + requestCameraAccessIfNeeded, + requestPhotoAccessIfNeeded, + store, + ], + ) // setUserBanner is only passed as prop on the EditProfile component return onSelectNewBanner ? ( - <DropdownButton - testID="changeBannerBtn" - type="bare" - items={dropdownItems} - openToRight - rightOffset={-200} - bottomOffset={-10} - menuWidth={170}> + <NativeDropdown testID="changeBannerBtn" items={dropdownItems}> {banner ? ( <Image testID="userBannerImage" @@ -109,7 +129,7 @@ export function UserBanner({ color={pal.text.color as string} /> </View> - </DropdownButton> + </NativeDropdown> ) : banner && !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( <Image diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx index 046610b29..a1ee3d589 100644 --- a/src/view/com/util/forms/DropdownButton.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -14,14 +14,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../text/Text' import {Button, ButtonType} from './Button' import {colors} from 'lib/styles' -import {toShareUrl} from 'lib/strings/url-helpers' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' -import {isWeb} from 'platform/detection' -import {shareUrl} from 'lib/sharing' +import {HITSLOP_10} from 'lib/constants' -const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} const ESTIMATED_BTN_HEIGHT = 50 const ESTIMATED_SEP_HEIGHT = 16 const ESTIMATED_HEADING_HEIGHT = 60 @@ -140,7 +136,7 @@ export function DropdownButton({ testID={testID} style={style} onPress={onPress} - hitSlop={HITSLOP} + hitSlop={HITSLOP_10} ref={ref1} accessibilityRole="button" accessibilityLabel={accessibilityLabel || `Opens ${numItems} options`} @@ -163,112 +159,6 @@ export function DropdownButton({ ) } -export function PostDropdownBtn({ - testID, - style, - children, - itemUri, - itemCid, - itemHref, - isAuthor, - isThreadMuted, - onCopyPostText, - onOpenTranslate, - onToggleThreadMute, - onDeletePost, -}: { - testID?: string - style?: StyleProp<ViewStyle> - children?: React.ReactNode - itemUri: string - itemCid: string - itemHref: string - itemTitle: string - isAuthor: boolean - isThreadMuted: boolean - onCopyPostText: () => void - onOpenTranslate: () => void - onToggleThreadMute: () => void - onDeletePost: () => void -}) { - const store = useStores() - - const dropdownItems: DropdownItem[] = [ - { - testID: 'postDropdownTranslateBtn', - icon: 'language', - label: 'Translate...', - onPress() { - onOpenTranslate() - }, - }, - { - testID: 'postDropdownCopyTextBtn', - icon: ['far', 'paste'], - label: 'Copy post text', - onPress() { - onCopyPostText() - }, - }, - { - testID: 'postDropdownShareBtn', - icon: 'share', - label: 'Share...', - onPress() { - const url = toShareUrl(itemHref) - shareUrl(url) - }, - }, - {sep: true}, - { - testID: 'postDropdownMuteThreadBtn', - icon: 'comment-slash', - label: isThreadMuted ? 'Unmute thread' : 'Mute thread', - onPress() { - onToggleThreadMute() - }, - }, - {sep: true}, - !isAuthor && { - testID: 'postDropdownReportBtn', - icon: 'circle-exclamation', - label: 'Report post', - onPress() { - store.shell.openModal({ - name: 'report-post', - postUri: itemUri, - postCid: itemCid, - }) - }, - }, - isAuthor && { - testID: 'postDropdownDeleteBtn', - icon: ['far', 'trash-can'], - label: 'Delete post', - onPress() { - store.shell.openModal({ - name: 'confirm', - title: 'Delete this post?', - message: 'Are you sure? This can not be undone.', - onPressConfirm: onDeletePost, - }) - }, - }, - ].filter(Boolean) as DropdownItem[] - - return ( - <DropdownButton - testID={testID} - style={style} - items={dropdownItems} - menuWidth={isWeb ? 220 : 200} - accessibilityLabel="Additional post actions" - accessibilityHint=""> - {children} - </DropdownButton> - ) -} - function createDropdownMenu( x: number, y: number, @@ -324,15 +214,16 @@ const DropdownItems = ({ const numItems = items.filter(isBtn).length + // TODO: Refactor dropdown components to: + // - (On web, if not handled by React Native) use semantic <select /> + // and <option /> elements for keyboard navigation out of the box + // - (On mobile) be buttons by default, accept `label` and `nativeID` + // props, and always have an explicit label return ( <> + {/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */} <TouchableWithoutFeedback onPress={onOuterPress} - // TODO: Refactor dropdown components to: - // - (On web, if not handled by React Native) use semantic <select /> - // and <option /> elements for keyboard navigation out of the box - // - (On mobile) be buttons by default, accept `label` and `nativeID` - // props, and always have an explicit label accessibilityRole="button" accessibilityLabel="Toggle dropdown" accessibilityHint=""> diff --git a/src/view/com/util/forms/NativeDropdown.tsx b/src/view/com/util/forms/NativeDropdown.tsx new file mode 100644 index 000000000..d8f16ce19 --- /dev/null +++ b/src/view/com/util/forms/NativeDropdown.tsx @@ -0,0 +1,250 @@ +import React from 'react' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import * as DropdownMenu from 'zeego/dropdown-menu' +import { + Pressable, + StyleSheet, + Platform, + StyleProp, + ViewStyle, +} from 'react-native' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' +import {usePalette} from 'lib/hooks/usePalette' +import {isWeb} from 'platform/detection' +import {useTheme} from 'lib/ThemeContext' +import {HITSLOP_10} from 'lib/constants' + +// Custom Dropdown Menu Components +// == +export const DropdownMenuRoot = DropdownMenu.Root +export const DropdownMenuTrigger = DropdownMenu.Trigger +export const DropdownMenuContent = DropdownMenu.Content +type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']> +export const DropdownMenuItem = DropdownMenu.create( + (props: ItemProps & {testID?: string}) => { + const pal = usePalette('default') + const theme = useTheme() + const [focused, setFocused] = React.useState(false) + const {borderColor: backgroundColor} = + theme.colorScheme === 'dark' ? pal.borderDark : pal.border + + return ( + <DropdownMenu.Item + {...props} + style={[styles.item, focused && {backgroundColor: backgroundColor}]} + onFocus={() => { + setFocused(true) + props.onFocus && props.onFocus() + }} + onBlur={() => { + setFocused(false) + props.onBlur && props.onBlur() + }} + /> + ) + }, + 'Item', +) +type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']> +export const DropdownMenuItemTitle = DropdownMenu.create( + (props: TitleProps) => { + const pal = usePalette('default') + return ( + <DropdownMenu.ItemTitle + {...props} + style={[props.style, pal.text, styles.itemTitle]} + /> + ) + }, + 'ItemTitle', +) +type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']> +export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => { + return <DropdownMenu.ItemIcon {...props} /> +}, 'ItemIcon') +type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']> +export const DropdownMenuSeparator = DropdownMenu.create( + (props: SeparatorProps) => { + const pal = usePalette('default') + const theme = useTheme() + const {borderColor: separatorColor} = + theme.colorScheme === 'dark' ? pal.borderDark : pal.border + return ( + <DropdownMenu.Separator + {...props} + style={[ + props.style, + styles.separator, + {backgroundColor: separatorColor}, + ]} + /> + ) + }, + 'Separator', +) + +// Types for Dropdown Menu and Items +export type DropdownItem = { + label: string | 'separator' + onPress?: () => void + testID?: string + icon?: { + ios: MenuItemCommonProps['ios'] + android: string + web: IconProp + } +} +type Props = { + items: DropdownItem[] + children?: React.ReactNode + testID?: string +} + +/* The `NativeDropdown` function uses native iOS and Android dropdown menus. + * It also creates a animated custom dropdown for web that uses + * Radix UI primitives under the hood + * @prop {DropdownItem[]} items - An array of dropdown items + * @prop {React.ReactNode} children - A custom dropdown trigger + */ +export function NativeDropdown({items, children, testID}: Props) { + const pal = usePalette('default') + const theme = useTheme() + const dropDownBackgroundColor = + theme.colorScheme === 'dark' ? pal.btn : pal.viewLight + const defaultCtrlColor = React.useMemo( + () => ({ + color: theme.palette.default.postCtrl, + }), + [theme], + ) as StyleProp<ViewStyle> + + return ( + <DropdownMenuRoot> + <DropdownMenuTrigger action="press"> + <Pressable + testID={testID} + accessibilityRole="button" + style={({pressed}) => [{opacity: pressed ? 0.5 : 1}]} + hitSlop={HITSLOP_10}> + {children ? ( + children + ) : ( + <FontAwesomeIcon + icon="ellipsis" + size={20} + style={[defaultCtrlColor, styles.ellipsis]} + /> + )} + </Pressable> + </DropdownMenuTrigger> + <DropdownMenuContent + style={[styles.content, dropDownBackgroundColor]} + loop> + {items.map((item, index) => { + if (item.label === 'separator') { + return ( + <DropdownMenuSeparator + key={getKey(item.label, index, item.testID)} + /> + ) + } + if (index > 1 && items[index - 1].label === 'separator') { + return ( + <DropdownMenu.Group key={getKey(item.label, index, item.testID)}> + <DropdownMenuItem + key={getKey(item.label, index, item.testID)} + onSelect={item.onPress}> + <DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle> + {item.icon && ( + <DropdownMenuItemIcon + ios={item.icon.ios} + // androidIconName={item.icon.android} TODO: Add custom android icon support, because these ones are based on https://developer.android.com/reference/android/R.drawable.html and they are ugly + > + <FontAwesomeIcon + icon={item.icon.web} + size={20} + style={[pal.text]} + /> + </DropdownMenuItemIcon> + )} + </DropdownMenuItem> + </DropdownMenu.Group> + ) + } + return ( + <DropdownMenuItem + key={getKey(item.label, index, item.testID)} + onSelect={item.onPress}> + <DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle> + {item.icon && ( + <DropdownMenuItemIcon + ios={item.icon.ios} + // androidIconName={item.icon.android} + > + <FontAwesomeIcon + icon={item.icon.web} + size={20} + style={[pal.text]} + /> + </DropdownMenuItemIcon> + )} + </DropdownMenuItem> + ) + })} + </DropdownMenuContent> + </DropdownMenuRoot> + ) +} + +const getKey = (label: string, index: number, id?: string) => { + if (id) { + return id + } + return `${label}_${index}` +} + +const styles = StyleSheet.create({ + separator: { + height: 1, + marginVertical: 4, + }, + ellipsis: { + padding: isWeb ? 0 : 10, + }, + content: { + backgroundColor: '#f0f0f0', + borderRadius: 8, + paddingVertical: 4, + paddingHorizontal: 4, + marginTop: 6, + ...Platform.select({ + web: { + animationDuration: '400ms', + animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', + willChange: 'transform, opacity', + animationKeyframes: { + '0%': {opacity: 0, transform: [{scale: 0.5}]}, + '100%': {opacity: 1, transform: [{scale: 1}]}, + }, + boxShadow: + '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)', + transformOrigin: 'var(--radix-dropdown-menu-content-transform-origin)', + }, + }), + }, + item: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + columnGap: 20, + // @ts-ignore -web + cursor: 'pointer', + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 8, + }, + itemTitle: { + fontSize: 18, + }, +}) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx new file mode 100644 index 000000000..ad9ba1619 --- /dev/null +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -0,0 +1,148 @@ +import React from 'react' +import {toShareUrl} from 'lib/strings/url-helpers' +import {useStores} from 'state/index' +import {shareUrl} from 'lib/sharing' +import { + NativeDropdown, + DropdownItem as NativeDropdownItem, +} from './NativeDropdown' +import {Pressable} from 'react-native' + +export function PostDropdownBtn({ + testID, + itemUri, + itemCid, + itemHref, + isAuthor, + isThreadMuted, + onCopyPostText, + onOpenTranslate, + onToggleThreadMute, + onDeletePost, +}: { + testID: string + itemUri: string + itemCid: string + itemHref: string + itemTitle: string + isAuthor: boolean + isThreadMuted: boolean + onCopyPostText: () => void + onOpenTranslate: () => void + onToggleThreadMute: () => void + onDeletePost: () => void +}) { + const store = useStores() + + const dropdownItems: NativeDropdownItem[] = [ + { + label: 'Translate', + onPress() { + onOpenTranslate() + }, + testID: 'postDropdownTranslateBtn', + icon: { + ios: { + name: 'character.book.closed', + }, + android: 'ic_menu_sort_alphabetically', + web: 'language', + }, + }, + { + label: 'Copy post text', + onPress() { + onCopyPostText() + }, + testID: 'postDropdownCopyTextBtn', + icon: { + ios: { + name: 'doc.on.doc', + }, + android: 'ic_menu_edit', + web: ['far', 'paste'], + }, + }, + { + label: 'Share', + onPress() { + const url = toShareUrl(itemHref) + shareUrl(url) + }, + testID: 'postDropdownShareBtn', + icon: { + ios: { + name: 'square.and.arrow.up', + }, + android: 'ic_menu_share', + web: 'share', + }, + }, + { + label: 'separator', + }, + { + label: isThreadMuted ? 'Unmute thread' : 'Mute thread', + onPress() { + onToggleThreadMute() + }, + testID: 'postDropdownMuteThreadBtn', + icon: { + ios: { + name: 'speaker.slash', + }, + android: 'ic_lock_silent_mode', + web: 'comment-slash', + }, + }, + { + label: 'separator', + }, + { + label: 'Report post', + onPress() { + store.shell.openModal({ + name: 'report-post', + postUri: itemUri, + postCid: itemCid, + }) + }, + testID: 'postDropdownReportBtn', + icon: { + ios: { + name: 'exclamationmark.triangle', + }, + android: 'ic_menu_report_image', + web: 'circle-exclamation', + }, + }, + isAuthor && { + label: 'separator', + }, + isAuthor && { + label: 'Delete post', + onPress() { + store.shell.openModal({ + name: 'confirm', + title: 'Delete this post?', + message: 'Are you sure? This can not be undone.', + onPressConfirm: onDeletePost, + }) + }, + testID: 'postDropdownDeleteBtn', + icon: { + ios: { + name: 'trash', + }, + android: 'ic_menu_delete', + web: ['far', 'trash-can'], + }, + }, + ].filter(Boolean) as NativeDropdownItem[] + + return ( + <Pressable testID={testID} accessibilityRole="button"> + <NativeDropdown items={dropdownItems} /> + </Pressable> + ) +} diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx index fefc540c0..c90e5dfb1 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx @@ -5,8 +5,7 @@ import {Text} from '../text/Text' import {usePalette} from 'lib/hooks/usePalette' import {LoadLatestBtn as LoadLatestBtnMobile} from './LoadLatestBtnMobile' import {isMobileWeb} from 'platform/detection' - -const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} +import {HITSLOP_20} from 'lib/constants' export const LoadLatestBtn = ({ onPress, @@ -40,7 +39,7 @@ export const LoadLatestBtn = ({ minimalShellMode && styles.loadLatestCenteredMinimal, ]} onPress={onPress} - hitSlop={HITSLOP} + hitSlop={HITSLOP_20} accessibilityRole="button" accessibilityLabel={label} accessibilityHint=""> @@ -52,7 +51,7 @@ export const LoadLatestBtn = ({ <TouchableOpacity style={[pal.view, pal.borderDark, styles.loadLatest]} onPress={onPress} - hitSlop={HITSLOP} + hitSlop={HITSLOP_20} accessibilityRole="button" accessibilityLabel={label} accessibilityHint=""> diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx index 412b9b803..eb7eaaa49 100644 --- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx @@ -7,8 +7,7 @@ import {clamp} from 'lodash' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {colors} from 'lib/styles' - -const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} +import {HITSLOP_20} from 'lib/constants' export const LoadLatestBtn = observer( ({ @@ -35,7 +34,7 @@ export const LoadLatestBtn = observer( }, ]} onPress={onPress} - hitSlop={HITSLOP} + hitSlop={HITSLOP_20} accessibilityRole="button" accessibilityLabel={label} accessibilityHint=""> diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index c544f6409..672e02693 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -6,17 +6,13 @@ import { View, ViewStyle, } from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' // DISABLED see #135 // import { // TriggerableAnimated, // TriggerableAnimatedRef, // } from './anim/TriggerableAnimated' import {Text} from '../text/Text' -import {PostDropdownBtn} from '../forms/DropdownButton' +import {PostDropdownBtn} from '../forms/PostDropdownBtn' import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons' import {s, colors} from 'lib/styles' import {pluralize} from 'lib/strings/helpers' @@ -24,6 +20,7 @@ import {useTheme} from 'lib/ThemeContext' import {useStores} from 'state/index' import {RepostButton} from './RepostButton' import {Haptics} from 'lib/haptics' +import {createHitslop} from 'lib/constants' interface PostCtrlsOpts { itemUri: string @@ -56,7 +53,7 @@ interface PostCtrlsOpts { onDeletePost: () => void } -const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} +const HITSLOP = createHitslop(5) // DISABLED see #135 /* @@ -222,36 +219,21 @@ export function PostCtrls(opts: PostCtrlsOpts) { </Text> ) : undefined} </TouchableOpacity> - <View> - {opts.big ? undefined : ( - <PostDropdownBtn - testID="postDropdownBtn" - style={styles.ctrl} - itemUri={opts.itemUri} - itemCid={opts.itemCid} - itemHref={opts.itemHref} - itemTitle={opts.itemTitle} - isAuthor={opts.isAuthor} - isThreadMuted={opts.isThreadMuted} - onCopyPostText={opts.onCopyPostText} - onOpenTranslate={opts.onOpenTranslate} - onToggleThreadMute={opts.onToggleThreadMute} - onDeletePost={opts.onDeletePost}> - <FontAwesomeIcon - icon="ellipsis-h" - size={18} - style={[ - s.mt2, - s.mr5, - { - color: - theme.colorScheme === 'light' ? colors.gray4 : colors.gray5, - } as FontAwesomeIconStyle, - ]} - /> - </PostDropdownBtn> - )} - </View> + {opts.big ? undefined : ( + <PostDropdownBtn + testID="postDropdownBtn" + itemUri={opts.itemUri} + itemCid={opts.itemCid} + itemHref={opts.itemHref} + itemTitle={opts.itemTitle} + isAuthor={opts.isAuthor} + isThreadMuted={opts.isThreadMuted} + onCopyPostText={opts.onCopyPostText} + onOpenTranslate={opts.onOpenTranslate} + onToggleThreadMute={opts.onToggleThreadMute} + onDeletePost={opts.onDeletePost} + /> + )} {/* used for adding pad to the right side */} <View /> </View> diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 4338e4c59..5fe62aefe 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -6,8 +6,9 @@ import {useTheme} from 'lib/ThemeContext' import {Text} from '../text/Text' import {pluralize} from 'lib/strings/helpers' import {useStores} from 'state/index' +import {createHitslop} from 'lib/constants' -const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} +const HITSLOP = createHitslop(5) interface Props { isReposted: boolean diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 61550c683..d5ecff042 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -29,10 +29,10 @@ import {Haptics} from 'lib/haptics' import {ComposeIcon2} from 'lib/icons' import {FAB} from '../com/util/fab/FAB' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' -import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {EmptyState} from 'view/com/util/EmptyState' import {useAnalytics} from 'lib/analytics/analytics' +import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' import {makeProfileLink} from 'lib/routes/links' type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'> @@ -121,11 +121,25 @@ export const CustomFeedScreen = withAuthRequired( testID: 'feedHeaderDropdownRemoveBtn', label: 'Remove from my feeds', onPress: onToggleSaved, + icon: { + ios: { + name: 'trash', + }, + android: 'ic_delete', + web: 'trash', + }, }, { testID: 'feedHeaderDropdownShareBtn', label: 'Share link', onPress: onPressShare, + icon: { + ios: { + name: 'square.and.arrow.up', + }, + android: 'ic_menu_share', + web: 'share', + }, }, ] return items @@ -163,17 +177,10 @@ export const CustomFeedScreen = withAuthRequired( </Button> ) : undefined} {currentFeed?.isSaved ? ( - <DropdownButton + <NativeDropdown testID="feedHeaderDropdownBtn" - type="default-light" items={dropdownItems} - menuWidth={250}> - <FontAwesomeIcon - icon="ellipsis" - color={pal.colors.textLight} - size={18} - /> - </DropdownButton> + /> ) : ( <Button type="default-light" diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index dd456c35e..47aa65585 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -3,6 +3,7 @@ import { ActivityIndicator, Linking, Platform, + Pressable, StyleSheet, TextStyle, TouchableOpacity, @@ -30,7 +31,6 @@ import {Link} from '../com/util/Link' import {Text} from '../com/util/text/Text' import * as Toast from '../com/util/Toast' import {UserAvatar} from '../com/util/UserAvatar' -import {DropdownButton} from 'view/com/util/forms/DropdownButton' import {ToggleButton} from 'view/com/util/forms/ToggleButton' import {SelectableBtn} from 'view/com/util/forms/SelectableBtn' import {usePalette} from 'lib/hooks/usePalette' @@ -50,6 +50,7 @@ import {makeProfileLink} from 'lib/routes/links' // -prf import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header' import {STATUS_PAGE_URL} from 'lib/constants' +import {DropdownItem, NativeDropdown} from 'view/com/util/forms/NativeDropdown' type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> export const SettingsScreen = withAuthRequired( @@ -565,24 +566,31 @@ export const SettingsScreen = withAuthRequired( function AccountDropdownBtn({handle}: {handle: string}) { const store = useStores() const pal = usePalette('default') - const items = [ + const items: DropdownItem[] = [ { label: 'Remove account', onPress: () => { store.session.removeAccount(handle) Toast.show('Account removed from quick access') }, + icon: { + ios: { + name: 'trash', + }, + android: 'ic_delete', + web: 'trash', + }, }, ] return ( - <View style={s.pl10}> - <DropdownButton type="bare" items={items}> + <Pressable accessibilityRole="button" style={s.pl10}> + <NativeDropdown testID="accountSettingsDropdownBtn" items={items}> <FontAwesomeIcon icon="ellipsis-h" style={pal.textLight as FontAwesomeIconStyle} /> - </DropdownButton> - </View> + </NativeDropdown> + </Pressable> ) } diff --git a/yarn.lock b/yarn.lock index fe1296aca..3f4a534e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2586,6 +2586,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.13.10": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" + integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/template@^7.0.0", "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -2833,6 +2840,11 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@dominicstop/ts-event-emitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@dominicstop/ts-event-emitter/-/ts-event-emitter-1.1.0.tgz#1f3d3fa878a1ccab686931280757954719cf88e4" + integrity sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw== + "@egjs/hammerjs@^2.0.17": version "2.0.17" resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" @@ -3241,6 +3253,25 @@ find-up "^5.0.0" js-yaml "^4.1.0" +"@floating-ui/core@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.1.tgz#4d795b649cc3b1cbb760d191c80dcb4353c9a366" + integrity sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g== + +"@floating-ui/dom@^1.3.0": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.4.5.tgz#336dfb9870c98b471ff5802002982e489b8bd1c5" + integrity sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw== + dependencies: + "@floating-ui/core" "^1.3.1" + +"@floating-ui/react-dom@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.1.tgz#7972a4fc488a8c746cded3cfe603b6057c308a91" + integrity sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA== + dependencies: + "@floating-ui/dom" "^1.3.0" + "@fortawesome/fontawesome-common-types@6.4.0": version "6.4.0" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz#88da2b70d6ca18aaa6ed3687832e11f39e80624b" @@ -3992,6 +4023,261 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@radix-ui/primitive@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd" + integrity sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-arrow@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" + integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + +"@radix-ui/react-collection@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159" + integrity sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + +"@radix-ui/react-compose-refs@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989" + integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-context-menu@^2.0.1": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.1.4.tgz#250420d259d3cebe026b7658414c516a1204de3f" + integrity sha512-HVHLUtZOBiR2Fh5l07qQ9y0IgX4dGZF0S9Gwdk4CVA+DL9afSphvFNa4nRiw6RNgb6quwLV4dLPF/gFDvNaOcQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-menu" "2.0.5" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-controllable-state" "1.0.1" + +"@radix-ui/react-context@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c" + integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-direction@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" + integrity sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-dismissable-layer@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz#883a48f5f938fa679427aa17fcba70c5494c6978" + integrity sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-escape-keydown" "1.0.3" + +"@radix-ui/react-dropdown-menu@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz#19bf4de8ffa348b4eb6a86842f14eff93d741170" + integrity sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-menu" "2.0.5" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + +"@radix-ui/react-focus-guards@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" + integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-focus-scope@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz#9c2e8d4ed1189a1d419ee61edd5c1828726472f9" + integrity sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + +"@radix-ui/react-id@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" + integrity sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "1.0.1" + +"@radix-ui/react-menu@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.5.tgz#a7d78b0808c4d38269240bf5d5c7ffea3e225e16" + integrity sha512-Gw4f9pwdH+w5w+49k0gLjN0PfRDHvxmAgG16AbyJZ7zhwZ6PBHKtWohvnSwfusfnK3L68dpBREHpVkj8wEM7ZA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.4" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.3" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.2" + "@radix-ui/react-portal" "1.0.3" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-callback-ref" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + +"@radix-ui/react-popper@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9" + integrity sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg== + dependencies: + "@babel/runtime" "^7.13.10" + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-use-rect" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/rect" "1.0.1" + +"@radix-ui/react-portal@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1" + integrity sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + +"@radix-ui/react-presence@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" + integrity sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + +"@radix-ui/react-primitive@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0" + integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-slot" "1.0.2" + +"@radix-ui/react-roving-focus@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" + integrity sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-controllable-state" "1.0.1" + +"@radix-ui/react-slot@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" + integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + +"@radix-ui/react-use-callback-ref@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a" + integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-controllable-state@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286" + integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.1" + +"@radix-ui/react-use-escape-keydown@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" + integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.1" + +"@radix-ui/react-use-layout-effect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" + integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-rect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2" + integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/rect" "1.0.1" + +"@radix-ui/react-use-size@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" + integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "1.0.1" + +"@radix-ui/rect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f" + integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@react-native-async-storage/async-storage@^1.15.15", "@react-native-async-storage/async-storage@^1.17.6": version "1.18.2" resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.18.2.tgz#ec8fd487a0b6c9500b43ece4b8779d1561f12e91" @@ -4219,6 +4505,11 @@ resolved "https://registry.yarnpkg.com/@react-native-community/eslint-plugin/-/eslint-plugin-1.3.0.tgz#9e558170c106bbafaa1ef502bd8e6d4651012bf9" integrity sha512-+zDZ20NUnSWghj7Ku5aFphMzuM9JulqCW+aPXT6IfIXFbb8tzYTTOSeRFOtuekJ99ibW2fUCSsjuKNlwDIbHFg== +"@react-native-menu/menu@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@react-native-menu/menu/-/menu-0.8.0.tgz#dbf227c2081e5ffd3d2073ee68ecc84cf8639727" + integrity sha512-kxiT6ySZsDbBvNWovrKVAfs4AQvAytKIf0f8KQLkVO6eNYMUmONBQPzi6onTTbVujXtZHambo7qr/PcedaR8Tg== + "@react-native/assets@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@react-native/assets/-/assets-1.0.0.tgz#c6f9bf63d274bafc8e970628de24986b30a55c8e" @@ -6850,6 +7141,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.1.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" + integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== + dependencies: + tslib "^2.0.0" + aria-query@^5.1.3: version "5.3.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" @@ -8978,6 +9276,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" @@ -10819,6 +11122,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ has-proto "^1.0.1" has-symbols "^1.0.3" +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -16750,6 +17058,13 @@ react-native-inappbrowser-reborn@^3.6.3: invariant "^2.2.4" opencollective-postinstall "^2.0.3" +react-native-ios-context-menu@^1.15.3: + version "1.15.3" + resolved "https://registry.yarnpkg.com/react-native-ios-context-menu/-/react-native-ios-context-menu-1.15.3.tgz#c02e6a7af2df8c08d0b3e1c8f3395484b3c9c760" + integrity sha512-UNkVl7ocvSpNaEpvBvE1aHOfDy/DFdZ5I+ElfnTXFsRxrVZmxLtST0b1q2wSWGWDmd2Ig2AYd7GRbYtcY222Ag== + dependencies: + "@dominicstop/ts-event-emitter" "^1.1.0" + react-native-linear-gradient@^2.6.2: version "2.7.3" resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.7.3.tgz#f77b71ed7c955e033f9cba5fc8478df57953eb27" @@ -16892,6 +17207,25 @@ react-refresh@^0.4.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.4.3.tgz#966f1750c191672e76e16c2efa569150cc73ab53" integrity sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA== +react-remove-scroll-bar@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9" + integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + +react-remove-scroll@2.5.5: + version "2.5.5" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" + integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== + dependencies: + react-remove-scroll-bar "^2.3.3" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + react-responsive@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/react-responsive/-/react-responsive-9.0.2.tgz#34531ca77a61e7a8775714016d21241df7e4205c" @@ -16965,6 +17299,15 @@ react-shallow-renderer@^16.15.0: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0 || ^18.0.0" +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + react-test-renderer@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.2.0.tgz#1dd912bd908ff26da5b9fca4fd1c489b9523d37e" @@ -17758,6 +18101,11 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +sf-symbols-typescript@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-1.0.0.tgz#94e9210bf27e7583f9749a0d07bd4f4937ea488f" + integrity sha512-DkS7q3nN68dEMb4E18HFPDAvyrjDZK9YAQQF2QxeFu9gp2xRDXFMF8qLJ1EmQ/qeEGQmop4lmMM1WtYJTIcCMw== + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -18966,7 +19314,7 @@ tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== @@ -19289,11 +19637,26 @@ url-parse@^1.5.3, url-parse@^1.5.9: querystringify "^2.1.1" requires-port "^1.0.0" +use-callback-ref@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" + integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + dependencies: + tslib "^2.0.0" + use-latest-callback@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.6.tgz#3fa6e7babbb5f9bfa24b5094b22939e1e92ebcf6" integrity sha512-VO/P91A/PmKH9bcN9a7O3duSuxe6M14ZoYXgA6a8dab8doWNdhiIHzEkX/jFeTTRBsX0Ubk6nG4q2NIjNsj+bg== +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" @@ -20202,6 +20565,15 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zeego@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/zeego/-/zeego-1.6.2.tgz#6051ecc99cd82ced2f49ab14b167398323f8618c" + integrity sha512-6SSKzW69Z0Px2v3kF5lsoVZeBOLf22Xl38XLsvIrdQS2Mq9WZOrEvLU4JA6pxagKw54f6LLHyqtiWAAM9U/9pg== + dependencies: + "@radix-ui/react-context-menu" "^2.0.1" + "@radix-ui/react-dropdown-menu" "^2.0.1" + sf-symbols-typescript "^1.0.0" + zod@^3.14.2, zod@^3.20.2, zod@^3.21.4: version "3.21.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" |