Bluesky app fork with some witchin' additions 💫

[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`

authored by Ansh and committed by GitHub 3b8b5622 eec300d7

+2
__e2e__/tests/create-account.test.ts
··· 25 25 await element(by.id('handleInput')).typeText('e2e-test') 26 26 await device.takeScreenshot('4- entered handle') 27 27 await element(by.id('nextBtn')).tap() 28 + await expect(element(by.id('welcomeScreen'))).toBeVisible() 29 + await element(by.id('continueBtn')).tap() 28 30 await expect(element(by.id('homeScreen'))).toBeVisible() 29 31 }) 30 32 })
+2 -2
__e2e__/tests/home-screen.test.ts
··· 55 55 await element(by.id('postDropdownBtn').withAncestor(carlaPosts)) 56 56 .atIndex(0) 57 57 .tap() 58 - await element(by.id('postDropdownReportBtn')).tap() 58 + await element(by.text('Report post')).tap() 59 59 await expect(element(by.id('reportPostModal'))).toBeVisible() 60 60 await element( 61 61 by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), ··· 84 84 await element(by.id('postDropdownBtn').withAncestor(alicePosts)) 85 85 .atIndex(0) 86 86 .tap() 87 - await element(by.id('postDropdownDeleteBtn')).tap() 87 + await element(by.text('Delete post')).tap() 88 88 await expect(element(by.id('confirmModal'))).toBeVisible() 89 89 await element(by.id('confirmBtn')).tap() 90 90 await expect(
+2
__e2e__/tests/invite-codes.test.ts
··· 42 42 await element(by.id('handleInput')).typeText('e2e-test') 43 43 await device.takeScreenshot('4- entered handle') 44 44 await element(by.id('nextBtn')).tap() 45 + await expect(element(by.id('welcomeScreen'))).toBeVisible() 46 + await element(by.id('continueBtn')).tap() 45 47 await expect(element(by.id('homeScreen'))).toBeVisible() 46 48 await element(by.id('viewHeaderDrawerBtn')).tap() 47 49 await element(by.id('menuItemButton-Settings')).tap()
+8 -8
__e2e__/tests/profile-screen.test.ts
··· 62 62 await element(by.id('profileHeaderEditProfileButton')).tap() 63 63 await expect(element(by.id('editProfileModal'))).toBeVisible() 64 64 await element(by.id('changeBannerBtn')).tap() 65 - await element(by.id('changeBannerLibraryBtn')).tap() 65 + await element(by.text('Library')).tap() 66 66 await sleep(3e3) 67 67 await element(by.id('changeAvatarBtn')).tap() 68 - await element(by.id('changeAvatarLibraryBtn')).tap() 68 + await element(by.text('Library')).tap() 69 69 await sleep(3e3) 70 70 await element(by.id('editProfileSaveBtn')).tap() 71 71 await expect(element(by.id('editProfileModal'))).not.toBeVisible() ··· 79 79 await element(by.id('profileHeaderEditProfileButton')).tap() 80 80 await expect(element(by.id('editProfileModal'))).toBeVisible() 81 81 await element(by.id('changeBannerBtn')).tap() 82 - await element(by.id('changeBannerRemoveBtn')).tap() 82 + await element(by.text('Remove')).tap() 83 83 await element(by.id('changeAvatarBtn')).tap() 84 - await element(by.id('changeAvatarRemoveBtn')).tap() 84 + await element(by.text('Remove')).tap() 85 85 await element(by.id('editProfileSaveBtn')).tap() 86 86 await expect(element(by.id('editProfileModal'))).not.toBeVisible() 87 87 await expect(element(by.id('userBannerFallback'))).toExist() ··· 109 109 it('Can mute/unmute another user', async () => { 110 110 await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist() 111 111 await element(by.id('profileHeaderDropdownBtn')).tap() 112 - await element(by.id('profileHeaderDropdownMuteBtn')).tap() 112 + await element(by.text('Mute Account')).tap() 113 113 await expect(element(by.id('profileHeaderMutedNotice'))).toBeVisible() 114 114 await element(by.id('profileHeaderDropdownBtn')).tap() 115 - await element(by.id('profileHeaderDropdownMuteBtn')).tap() 115 + await element(by.text('Unmute Account')).tap() 116 116 await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist() 117 117 }) 118 118 119 119 it('Can report another user', async () => { 120 120 await element(by.id('profileHeaderDropdownBtn')).tap() 121 - await element(by.id('profileHeaderDropdownReportBtn')).tap() 121 + await element(by.text('Report Account')).tap() 122 122 await expect(element(by.id('reportAccountModal'))).toBeVisible() 123 123 await element( 124 124 by.id('reportAccountRadios-com.atproto.moderation.defs#reasonSpam'), ··· 166 166 it('Can report posts', async () => { 167 167 const posts = by.id('feedItem-by-bob.test') 168 168 await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap() 169 - await element(by.id('postDropdownReportBtn')).tap() 169 + await element(by.text('Report post')).tap() 170 170 await expect(element(by.id('reportPostModal'))).toBeVisible() 171 171 await element( 172 172 by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+2 -2
__e2e__/tests/thread-muting.test.ts
··· 45 45 await element(by.id('postDropdownBtn').withAncestor(bobNotifs)) 46 46 .atIndex(0) 47 47 .tap() 48 - await element(by.id('postDropdownMuteThreadBtn')).tap() 48 + await element(by.text('Mute thread')).tap() 49 49 // have to wait for the toast to clear 50 50 await waitFor(element(by.id('viewHeaderDrawerBtn'))) 51 51 .toBeVisible() ··· 93 93 await element(by.id('postDropdownBtn').withAncestor(alicePosts)) 94 94 .atIndex(0) 95 95 .tap() 96 - await element(by.id('postDropdownMuteThreadBtn')).tap() 96 + await element(by.text('Mute thread')).tap() 97 97 98 98 // TODO 99 99 // the swipe down to trigger PTR isnt working and I dont want to block on this
+2 -2
__e2e__/tests/thread-screen.test.ts
··· 104 104 it('Can report the root post', async () => { 105 105 const post = by.id('postThreadItem-by-bob.test') 106 106 await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() 107 - await element(by.id('postDropdownReportBtn')).tap() 107 + await element(by.text('Report post')).tap() 108 108 await expect(element(by.id('reportPostModal'))).toBeVisible() 109 109 await element( 110 110 by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'), ··· 116 116 it('Can report a reply post', async () => { 117 117 const post = by.id('postThreadItem-by-carla.test') 118 118 await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() 119 - await element(by.id('postDropdownReportBtn')).tap() 119 + await element(by.text('Report post')).tap() 120 120 await expect(element(by.id('reportPostModal'))).toBeVisible() 121 121 await element( 122 122 by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+2
__mocks__/zeego/dropdown-menu.js
··· 1 + export const DropdownMenu = jest.fn().mockImplementation(() => {}) 2 + export const create = jest.fn().mockImplementation(() => {})
+2
app.json
··· 80 80 { 81 81 "android": { 82 82 "compileSdkVersion": 34, 83 + "targetSdkVersion": 34, 84 + "buildToolsVersion": "34.0.0", 83 85 "kotlinVersion": "1.8.0" 84 86 } 85 87 }
+3
package.json
··· 42 42 "@react-native-clipboard/clipboard": "^1.10.0", 43 43 "@react-native-community/blur": "^4.3.0", 44 44 "@react-native-community/datetimepicker": "6.7.3", 45 + "@react-native-menu/menu": "^0.8.0", 45 46 "@react-navigation/bottom-tabs": "^6.5.7", 46 47 "@react-navigation/drawer": "^6.6.2", 47 48 "@react-navigation/native": "^6.1.6", ··· 120 121 "react-native-haptic-feedback": "^1.14.0", 121 122 "react-native-image-crop-picker": "^0.38.1", 122 123 "react-native-inappbrowser-reborn": "^3.6.3", 124 + "react-native-ios-context-menu": "^1.15.3", 123 125 "react-native-linear-gradient": "^2.6.2", 124 126 "react-native-pager-view": "6.1.4", 125 127 "react-native-progress": "bluesky-social/react-native-progress", ··· 139 141 "sentry-expo": "~6.1.0", 140 142 "tippy.js": "^6.3.7", 141 143 "tlds": "^1.234.0", 144 + "zeego": "^1.6.2", 142 145 "zod": "^3.20.2" 143 146 }, 144 147 "devDependencies": {
+14
src/lib/constants.ts
··· 1 + import {Insets} from 'react-native' 2 + 1 3 const HELP_DESK_LANG = 'en-us' 2 4 export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}` 3 5 ··· 134 136 } 135 137 136 138 export const STATUS_PAGE_URL = 'https://status.bsky.app/' 139 + 140 + // Hitslop constants 141 + export const createHitslop = (size: number): Insets => ({ 142 + top: size, 143 + left: size, 144 + bottom: size, 145 + right: size, 146 + }) 147 + export const HITSLOP_10 = createHitslop(10) 148 + export const HITSLOP_20 = createHitslop(20) 149 + export const HITSLOP_30 = createHitslop(30) 150 + export const BACK_HITSLOP = HITSLOP_30
+7 -2
src/view/com/auth/onboarding/Welcome.tsx
··· 10 10 const pal = usePalette('default') 11 11 return ( 12 12 <View style={[styles.container]}> 13 - <View> 13 + <View testID="welcomeScreen"> 14 14 <Text style={[pal.text, styles.title]}>Welcome to </Text> 15 15 <Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text> 16 16 ··· 52 52 </View> 53 53 </View> 54 54 55 - <Button onPress={next} label="Continue" labelStyle={styles.buttonText} /> 55 + <Button 56 + onPress={next} 57 + label="Continue" 58 + testID="continueBtn" 59 + labelStyle={styles.buttonText} 60 + /> 56 61 </View> 57 62 ) 58 63 }
+2 -4
src/view/com/composer/photos/OpenCameraBtn.tsx
··· 10 10 import {isDesktopWeb} from 'platform/detection' 11 11 import {openCamera} from 'lib/media/picker' 12 12 import {useCameraPermission} from 'lib/hooks/usePermissions' 13 - import {POST_IMG_MAX} from 'lib/constants' 13 + import {HITSLOP_10, POST_IMG_MAX} from 'lib/constants' 14 14 import {GalleryModel} from 'state/models/media/gallery' 15 - 16 - const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} 17 15 18 16 type Props = { 19 17 gallery: GalleryModel ··· 54 52 testID="openCameraButton" 55 53 onPress={onPressTakePicture} 56 54 style={styles.button} 57 - hitSlop={HITSLOP} 55 + hitSlop={HITSLOP_10} 58 56 accessibilityRole="button" 59 57 accessibilityLabel="Camera" 60 58 accessibilityHint="Opens camera on device">
+2 -3
src/view/com/composer/photos/SelectPhotoBtn.tsx
··· 9 9 import {isDesktopWeb} from 'platform/detection' 10 10 import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' 11 11 import {GalleryModel} from 'state/models/media/gallery' 12 - 13 - const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} 12 + import {HITSLOP_10} from 'lib/constants' 14 13 15 14 type Props = { 16 15 gallery: GalleryModel ··· 36 35 testID="openGalleryBtn" 37 36 onPress={onPressSelectPhotos} 38 37 style={styles.button} 39 - hitSlop={HITSLOP} 38 + hitSlop={HITSLOP_10} 40 39 accessibilityRole="button" 41 40 accessibilityLabel="Gallery" 42 41 accessibilityHint="Opens device photo gallery">
+2 -1
src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
··· 6 6 * 7 7 */ 8 8 9 + import {createHitslop} from 'lib/constants' 9 10 import React from 'react' 10 11 import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native' 11 12 ··· 13 14 onRequestClose: () => void 14 15 } 15 16 16 - const HIT_SLOP = {top: 16, left: 16, bottom: 16, right: 16} 17 + const HIT_SLOP = createHitslop(16) 17 18 18 19 const ImageDefaultHeader = ({onRequestClose}: Props) => ( 19 20 <SafeAreaView style={styles.root}>
+3 -2
src/view/com/pager/FeedsTabBarMobile.tsx
··· 12 12 import {CogIcon} from 'lib/icons' 13 13 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 14 14 import {s} from 'lib/styles' 15 + import {HITSLOP_10} from 'lib/constants' 15 16 16 17 export const FeedsTabBar = observer( 17 18 ( ··· 54 55 accessibilityRole="button" 55 56 accessibilityLabel="Open navigation" 56 57 accessibilityHint="Access profile and other navigation links" 57 - hitSlop={10}> 58 + hitSlop={HITSLOP_10}> 58 59 <FontAwesomeIcon 59 60 icon="bars" 60 61 size={18} ··· 68 69 <View style={[pal.view]}> 69 70 <Link 70 71 href="/settings/saved-feeds" 71 - hitSlop={10} 72 + hitSlop={HITSLOP_10} 72 73 accessibilityRole="button" 73 74 accessibilityLabel="Edit Saved Feeds" 74 75 accessibilityHint="Opens screen to edit Saved Feeds">
+3 -9
src/view/com/post-thread/PostThreadItem.tsx
··· 11 11 import {Link} from '../util/Link' 12 12 import {RichText} from '../util/text/RichText' 13 13 import {Text} from '../util/text/Text' 14 - import {PostDropdownBtn} from '../util/forms/DropdownButton' 14 + import {PostDropdownBtn} from '../util/forms/PostDropdownBtn' 15 15 import * as Toast from '../util/Toast' 16 16 import {PreviewableUserAvatar} from '../util/UserAvatar' 17 17 import {s} from 'lib/styles' ··· 202 202 <View style={s.flex1} /> 203 203 <PostDropdownBtn 204 204 testID="postDropdownBtn" 205 - style={[styles.metaItem, s.mt2, s.px5]} 206 205 itemUri={itemUri} 207 206 itemCid={itemCid} 208 207 itemHref={itemHref} ··· 212 211 onCopyPostText={onCopyPostText} 213 212 onOpenTranslate={onOpenTranslate} 214 213 onToggleThreadMute={onToggleThreadMute} 215 - onDeletePost={onDeletePost}> 216 - <FontAwesomeIcon 217 - icon="ellipsis-h" 218 - size={14} 219 - style={[pal.textLight]} 220 - /> 221 - </PostDropdownBtn> 214 + onDeletePost={onDeletePost} 215 + /> 222 216 </View> 223 217 <View style={styles.meta}> 224 218 <Link
+48 -10
src/view/com/profile/ProfileHeader.tsx
··· 17 17 import {sanitizeDisplayName} from 'lib/strings/display-names' 18 18 import {sanitizeHandle} from 'lib/strings/handles' 19 19 import {s, colors} from 'lib/styles' 20 - import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton' 21 20 import * as Toast from '../util/Toast' 22 21 import {LoadingPlaceholder} from '../util/LoadingPlaceholder' 23 22 import {Text} from '../util/text/Text' ··· 36 35 import {shareUrl} from 'lib/sharing' 37 36 import {formatCount} from '../util/numeric/format' 38 37 import {navigate} from '../../../Navigation' 38 + import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown' 39 + import {BACK_HITSLOP} from 'lib/constants' 39 40 import {isInvalidHandle} from 'lib/strings/handles' 40 41 import {makeProfileLink} from 'lib/routes/links' 41 - 42 - const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30} 43 42 44 43 interface Props { 45 44 view: ProfileModel ··· 260 259 testID: 'profileHeaderDropdownShareBtn', 261 260 label: 'Share', 262 261 onPress: onPressShare, 262 + icon: { 263 + ios: { 264 + name: 'square.and.arrow.up', 265 + }, 266 + android: 'ic_menu_share', 267 + web: 'share', 268 + }, 263 269 }, 264 270 ] 265 271 if (!isMe) { 266 - items.push({sep: true}) 272 + items.push({label: 'separator'}) 267 273 // Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self! 268 274 items.push({ 269 275 testID: 'profileHeaderDropdownListAddRemoveBtn', 270 276 label: 'Add to Lists', 271 277 onPress: onPressAddRemoveLists, 278 + icon: { 279 + ios: { 280 + name: 'list.bullet', 281 + }, 282 + android: 'ic_menu_add', 283 + web: 'list', 284 + }, 272 285 }) 273 286 if (!view.viewer.blocking) { 274 287 items.push({ ··· 277 290 onPress: view.viewer.muted 278 291 ? onPressUnmuteAccount 279 292 : onPressMuteAccount, 293 + icon: { 294 + ios: { 295 + name: 'speaker.slash', 296 + }, 297 + android: 'ic_lock_silent_mode', 298 + web: 'comment-slash', 299 + }, 280 300 }) 281 301 } 282 302 items.push({ ··· 285 305 onPress: view.viewer.blocking 286 306 ? onPressUnblockAccount 287 307 : onPressBlockAccount, 308 + icon: { 309 + ios: { 310 + name: 'person.fill.xmark', 311 + }, 312 + android: 'ic_menu_close_clear_cancel', 313 + web: 'user-slash', 314 + }, 288 315 }) 289 316 items.push({ 290 317 testID: 'profileHeaderDropdownReportBtn', 291 318 label: 'Report Account', 292 319 onPress: onPressReportAccount, 320 + icon: { 321 + ios: { 322 + name: 'exclamationmark.triangle', 323 + }, 324 + android: 'ic_menu_report_image', 325 + web: 'circle-exclamation', 326 + }, 293 327 }) 294 328 } 295 329 return items ··· 380 414 </> 381 415 ) : null} 382 416 {dropdownItems?.length ? ( 383 - <DropdownButton 417 + <NativeDropdown 384 418 testID="profileHeaderDropdownBtn" 385 - type="bare" 386 - items={dropdownItems} 387 - style={[styles.btn, styles.secondaryBtn, pal.btn]}> 388 - <FontAwesomeIcon icon="ellipsis" style={[pal.text]} /> 389 - </DropdownButton> 419 + items={dropdownItems}> 420 + <View style={[styles.btn, styles.secondaryBtn, pal.btn]}> 421 + <FontAwesomeIcon 422 + icon="ellipsis" 423 + size={20} 424 + style={[pal.text]} 425 + /> 426 + </View> 427 + </NativeDropdown> 390 428 ) : undefined} 391 429 </View> 392 430 <View>
+2 -3
src/view/com/search/HeaderWithInput.tsx
··· 10 10 import {usePalette} from 'lib/hooks/usePalette' 11 11 import {useStores} from 'state/index' 12 12 import {useAnalytics} from 'lib/analytics/analytics' 13 - 14 - const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} 13 + import {HITSLOP_10} from 'lib/constants' 15 14 16 15 interface Props { 17 16 isInputFocused: boolean ··· 55 54 <TouchableOpacity 56 55 testID="viewHeaderBackOrMenuBtn" 57 56 onPress={onPressMenu} 58 - hitSlop={MENU_HITSLOP} 57 + hitSlop={HITSLOP_10} 59 58 style={styles.headerMenuBtn} 60 59 accessibilityRole="button" 61 60 accessibilityLabel="Menu"
+71 -57
src/view/com/util/UserAvatar.tsx
··· 2 2 import {StyleSheet, View} from 'react-native' 3 3 import Svg, {Circle, Rect, Path} from 'react-native-svg' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {IconProp} from '@fortawesome/fontawesome-svg-core' 6 5 import {HighPriorityImage} from 'view/com/util/images/Image' 7 6 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' 8 7 import { ··· 11 10 } from 'lib/hooks/usePermissions' 12 11 import {useStores} from 'state/index' 13 12 import {colors} from 'lib/styles' 14 - import {DropdownButton} from './forms/DropdownButton' 15 13 import {usePalette} from 'lib/hooks/usePalette' 16 14 import {isWeb, isAndroid} from 'platform/detection' 17 15 import {Image as RNImage} from 'react-native-image-crop-picker' 18 16 import {AvatarModeration} from 'lib/labeling/types' 19 17 import {UserPreviewLink} from './UserPreviewLink' 18 + import {DropdownItem, NativeDropdown} from './forms/NativeDropdown' 20 19 21 20 type Type = 'user' | 'algo' | 'list' 22 21 ··· 130 129 }, [type, size]) 131 130 132 131 const dropdownItems = useMemo( 133 - () => [ 134 - !isWeb && { 135 - testID: 'changeAvatarCameraBtn', 136 - label: 'Camera', 137 - icon: 'camera' as IconProp, 138 - onPress: async () => { 139 - if (!(await requestCameraAccessIfNeeded())) { 140 - return 141 - } 132 + () => 133 + [ 134 + !isWeb && { 135 + testID: 'changeAvatarCameraBtn', 136 + label: 'Camera', 137 + icon: { 138 + ios: { 139 + name: 'camera', 140 + }, 141 + android: 'ic_menu_camera', 142 + web: 'camera', 143 + }, 144 + onPress: async () => { 145 + if (!(await requestCameraAccessIfNeeded())) { 146 + return 147 + } 142 148 143 - onSelectNewAvatar?.( 144 - await openCamera(store, { 145 - width: 1000, 146 - height: 1000, 147 - cropperCircleOverlay: true, 148 - }), 149 - ) 149 + onSelectNewAvatar?.( 150 + await openCamera(store, { 151 + width: 1000, 152 + height: 1000, 153 + cropperCircleOverlay: true, 154 + }), 155 + ) 156 + }, 150 157 }, 151 - }, 152 - { 153 - testID: 'changeAvatarLibraryBtn', 154 - label: 'Library', 155 - icon: 'image' as IconProp, 156 - onPress: async () => { 157 - if (!(await requestPhotoAccessIfNeeded())) { 158 - return 159 - } 158 + { 159 + testID: 'changeAvatarLibraryBtn', 160 + label: 'Library', 161 + icon: { 162 + ios: { 163 + name: 'photo.on.rectangle.angled', 164 + }, 165 + android: 'ic_menu_gallery', 166 + web: 'gallery', 167 + }, 168 + onPress: async () => { 169 + if (!(await requestPhotoAccessIfNeeded())) { 170 + return 171 + } 160 172 161 - const items = await openPicker({ 162 - aspect: [1, 1], 163 - }) 164 - const item = items[0] 173 + const items = await openPicker({ 174 + aspect: [1, 1], 175 + }) 176 + const item = items[0] 165 177 166 - const croppedImage = await openCropper(store, { 167 - mediaType: 'photo', 168 - cropperCircleOverlay: true, 169 - height: item.height, 170 - width: item.width, 171 - path: item.path, 172 - }) 178 + const croppedImage = await openCropper(store, { 179 + mediaType: 'photo', 180 + cropperCircleOverlay: true, 181 + height: item.height, 182 + width: item.width, 183 + path: item.path, 184 + }) 173 185 174 - onSelectNewAvatar?.(croppedImage) 186 + onSelectNewAvatar?.(croppedImage) 187 + }, 175 188 }, 176 - }, 177 - !!avatar && { 178 - testID: 'changeAvatarRemoveBtn', 179 - label: 'Remove', 180 - icon: ['far', 'trash-can'] as IconProp, 181 - onPress: async () => { 182 - onSelectNewAvatar?.(null) 189 + !!avatar && { 190 + label: 'separator', 183 191 }, 184 - }, 185 - ], 192 + !!avatar && { 193 + testID: 'changeAvatarRemoveBtn', 194 + label: 'Remove', 195 + icon: { 196 + ios: { 197 + name: 'trash', 198 + }, 199 + android: 'ic_delete', 200 + web: 'trash', 201 + }, 202 + onPress: async () => { 203 + onSelectNewAvatar?.(null) 204 + }, 205 + }, 206 + ].filter(Boolean) as DropdownItem[], 186 207 [ 187 208 avatar, 188 209 onSelectNewAvatar, ··· 209 230 210 231 // onSelectNewAvatar is only passed as prop on the EditProfile component 211 232 return onSelectNewAvatar ? ( 212 - <DropdownButton 213 - testID="changeAvatarBtn" 214 - type="bare" 215 - items={dropdownItems} 216 - openToRight 217 - rightOffset={-10} 218 - bottomOffset={-10} 219 - menuWidth={170}> 233 + <NativeDropdown testID="changeAvatarBtn" items={dropdownItems}> 220 234 {avatar ? ( 221 235 <HighPriorityImage 222 236 testID="userAvatarImage" ··· 234 248 color={pal.text.color as string} 235 249 /> 236 250 </View> 237 - </DropdownButton> 251 + </NativeDropdown> 238 252 ) : avatar && 239 253 !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( 240 254 <View style={{width: size, height: size}}>
+77 -57
src/view/com/util/UserBanner.tsx
··· 1 - import React from 'react' 1 + import React, {useMemo} from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 - import {IconProp} from '@fortawesome/fontawesome-svg-core' 5 4 import {Image} from 'expo-image' 6 5 import {colors} from 'lib/styles' 7 6 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' ··· 10 9 usePhotoLibraryPermission, 11 10 useCameraPermission, 12 11 } from 'lib/hooks/usePermissions' 13 - import {DropdownButton} from './forms/DropdownButton' 14 12 import {usePalette} from 'lib/hooks/usePalette' 15 13 import {AvatarModeration} from 'lib/labeling/types' 16 14 import {isWeb, isAndroid} from 'platform/detection' 17 15 import {Image as RNImage} from 'react-native-image-crop-picker' 16 + import {NativeDropdown, DropdownItem} from './forms/NativeDropdown' 18 17 19 18 export function UserBanner({ 20 19 banner, ··· 30 29 const {requestCameraAccessIfNeeded} = useCameraPermission() 31 30 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 32 31 33 - const dropdownItems = [ 34 - !isWeb && { 35 - testID: 'changeBannerCameraBtn', 36 - label: 'Camera', 37 - icon: 'camera' as IconProp, 38 - onPress: async () => { 39 - if (!(await requestCameraAccessIfNeeded())) { 40 - return 41 - } 42 - onSelectNewBanner?.( 43 - await openCamera(store, { 44 - width: 3000, 45 - height: 1000, 46 - }), 47 - ) 48 - }, 49 - }, 50 - { 51 - testID: 'changeBannerLibraryBtn', 52 - label: 'Library', 53 - icon: 'image' as IconProp, 54 - onPress: async () => { 55 - if (!(await requestPhotoAccessIfNeeded())) { 56 - return 57 - } 58 - const items = await openPicker() 32 + const dropdownItems: DropdownItem[] = useMemo( 33 + () => 34 + [ 35 + !isWeb && { 36 + testID: 'changeBannerCameraBtn', 37 + label: 'Camera', 38 + icon: { 39 + ios: { 40 + name: 'camera', 41 + }, 42 + android: 'ic_menu_camera', 43 + web: 'camera', 44 + }, 45 + onPress: async () => { 46 + if (!(await requestCameraAccessIfNeeded())) { 47 + return 48 + } 49 + onSelectNewBanner?.( 50 + await openCamera(store, { 51 + width: 3000, 52 + height: 1000, 53 + }), 54 + ) 55 + }, 56 + }, 57 + { 58 + testID: 'changeBannerLibraryBtn', 59 + label: 'Library', 60 + icon: { 61 + ios: { 62 + name: 'photo.on.rectangle.angled', 63 + }, 64 + android: 'ic_menu_gallery', 65 + web: 'gallery', 66 + }, 67 + onPress: async () => { 68 + if (!(await requestPhotoAccessIfNeeded())) { 69 + return 70 + } 71 + const items = await openPicker() 59 72 60 - onSelectNewBanner?.( 61 - await openCropper(store, { 62 - mediaType: 'photo', 63 - path: items[0].path, 64 - width: 3000, 65 - height: 1000, 66 - }), 67 - ) 68 - }, 69 - }, 70 - !!banner && { 71 - testID: 'changeBannerRemoveBtn', 72 - label: 'Remove', 73 - icon: ['far', 'trash-can'] as IconProp, 74 - onPress: () => { 75 - onSelectNewBanner?.(null) 76 - }, 77 - }, 78 - ] 73 + onSelectNewBanner?.( 74 + await openCropper(store, { 75 + mediaType: 'photo', 76 + path: items[0].path, 77 + width: 3000, 78 + height: 1000, 79 + }), 80 + ) 81 + }, 82 + }, 83 + !!banner && { 84 + testID: 'changeBannerRemoveBtn', 85 + label: 'Remove', 86 + icon: { 87 + ios: { 88 + name: 'trash', 89 + }, 90 + android: 'ic_delete', 91 + web: 'trash', 92 + }, 93 + onPress: () => { 94 + onSelectNewBanner?.(null) 95 + }, 96 + }, 97 + ].filter(Boolean) as DropdownItem[], 98 + [ 99 + banner, 100 + onSelectNewBanner, 101 + requestCameraAccessIfNeeded, 102 + requestPhotoAccessIfNeeded, 103 + store, 104 + ], 105 + ) 79 106 80 107 // setUserBanner is only passed as prop on the EditProfile component 81 108 return onSelectNewBanner ? ( 82 - <DropdownButton 83 - testID="changeBannerBtn" 84 - type="bare" 85 - items={dropdownItems} 86 - openToRight 87 - rightOffset={-200} 88 - bottomOffset={-10} 89 - menuWidth={170}> 109 + <NativeDropdown testID="changeBannerBtn" items={dropdownItems}> 90 110 {banner ? ( 91 111 <Image 92 112 testID="userBannerImage" ··· 109 129 color={pal.text.color as string} 110 130 /> 111 131 </View> 112 - </DropdownButton> 132 + </NativeDropdown> 113 133 ) : banner && 114 134 !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( 115 135 <Image
+8 -117
src/view/com/util/forms/DropdownButton.tsx
··· 14 14 import {Text} from '../text/Text' 15 15 import {Button, ButtonType} from './Button' 16 16 import {colors} from 'lib/styles' 17 - import {toShareUrl} from 'lib/strings/url-helpers' 18 - import {useStores} from 'state/index' 19 17 import {usePalette} from 'lib/hooks/usePalette' 20 18 import {useTheme} from 'lib/ThemeContext' 21 - import {isWeb} from 'platform/detection' 22 - import {shareUrl} from 'lib/sharing' 19 + import {HITSLOP_10} from 'lib/constants' 23 20 24 - const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} 25 21 const ESTIMATED_BTN_HEIGHT = 50 26 22 const ESTIMATED_SEP_HEIGHT = 16 27 23 const ESTIMATED_HEADING_HEIGHT = 60 ··· 140 136 testID={testID} 141 137 style={style} 142 138 onPress={onPress} 143 - hitSlop={HITSLOP} 139 + hitSlop={HITSLOP_10} 144 140 ref={ref1} 145 141 accessibilityRole="button" 146 142 accessibilityLabel={accessibilityLabel || `Opens ${numItems} options`} ··· 163 159 ) 164 160 } 165 161 166 - export function PostDropdownBtn({ 167 - testID, 168 - style, 169 - children, 170 - itemUri, 171 - itemCid, 172 - itemHref, 173 - isAuthor, 174 - isThreadMuted, 175 - onCopyPostText, 176 - onOpenTranslate, 177 - onToggleThreadMute, 178 - onDeletePost, 179 - }: { 180 - testID?: string 181 - style?: StyleProp<ViewStyle> 182 - children?: React.ReactNode 183 - itemUri: string 184 - itemCid: string 185 - itemHref: string 186 - itemTitle: string 187 - isAuthor: boolean 188 - isThreadMuted: boolean 189 - onCopyPostText: () => void 190 - onOpenTranslate: () => void 191 - onToggleThreadMute: () => void 192 - onDeletePost: () => void 193 - }) { 194 - const store = useStores() 195 - 196 - const dropdownItems: DropdownItem[] = [ 197 - { 198 - testID: 'postDropdownTranslateBtn', 199 - icon: 'language', 200 - label: 'Translate...', 201 - onPress() { 202 - onOpenTranslate() 203 - }, 204 - }, 205 - { 206 - testID: 'postDropdownCopyTextBtn', 207 - icon: ['far', 'paste'], 208 - label: 'Copy post text', 209 - onPress() { 210 - onCopyPostText() 211 - }, 212 - }, 213 - { 214 - testID: 'postDropdownShareBtn', 215 - icon: 'share', 216 - label: 'Share...', 217 - onPress() { 218 - const url = toShareUrl(itemHref) 219 - shareUrl(url) 220 - }, 221 - }, 222 - {sep: true}, 223 - { 224 - testID: 'postDropdownMuteThreadBtn', 225 - icon: 'comment-slash', 226 - label: isThreadMuted ? 'Unmute thread' : 'Mute thread', 227 - onPress() { 228 - onToggleThreadMute() 229 - }, 230 - }, 231 - {sep: true}, 232 - !isAuthor && { 233 - testID: 'postDropdownReportBtn', 234 - icon: 'circle-exclamation', 235 - label: 'Report post', 236 - onPress() { 237 - store.shell.openModal({ 238 - name: 'report-post', 239 - postUri: itemUri, 240 - postCid: itemCid, 241 - }) 242 - }, 243 - }, 244 - isAuthor && { 245 - testID: 'postDropdownDeleteBtn', 246 - icon: ['far', 'trash-can'], 247 - label: 'Delete post', 248 - onPress() { 249 - store.shell.openModal({ 250 - name: 'confirm', 251 - title: 'Delete this post?', 252 - message: 'Are you sure? This can not be undone.', 253 - onPressConfirm: onDeletePost, 254 - }) 255 - }, 256 - }, 257 - ].filter(Boolean) as DropdownItem[] 258 - 259 - return ( 260 - <DropdownButton 261 - testID={testID} 262 - style={style} 263 - items={dropdownItems} 264 - menuWidth={isWeb ? 220 : 200} 265 - accessibilityLabel="Additional post actions" 266 - accessibilityHint=""> 267 - {children} 268 - </DropdownButton> 269 - ) 270 - } 271 - 272 162 function createDropdownMenu( 273 163 x: number, 274 164 y: number, ··· 324 214 325 215 const numItems = items.filter(isBtn).length 326 216 217 + // TODO: Refactor dropdown components to: 218 + // - (On web, if not handled by React Native) use semantic <select /> 219 + // and <option /> elements for keyboard navigation out of the box 220 + // - (On mobile) be buttons by default, accept `label` and `nativeID` 221 + // props, and always have an explicit label 327 222 return ( 328 223 <> 224 + {/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */} 329 225 <TouchableWithoutFeedback 330 226 onPress={onOuterPress} 331 - // TODO: Refactor dropdown components to: 332 - // - (On web, if not handled by React Native) use semantic <select /> 333 - // and <option /> elements for keyboard navigation out of the box 334 - // - (On mobile) be buttons by default, accept `label` and `nativeID` 335 - // props, and always have an explicit label 336 227 accessibilityRole="button" 337 228 accessibilityLabel="Toggle dropdown" 338 229 accessibilityHint="">
+250
src/view/com/util/forms/NativeDropdown.tsx
··· 1 + import React from 'react' 2 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 3 + import * as DropdownMenu from 'zeego/dropdown-menu' 4 + import { 5 + Pressable, 6 + StyleSheet, 7 + Platform, 8 + StyleProp, 9 + ViewStyle, 10 + } from 'react-native' 11 + import {IconProp} from '@fortawesome/fontawesome-svg-core' 12 + import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' 13 + import {usePalette} from 'lib/hooks/usePalette' 14 + import {isWeb} from 'platform/detection' 15 + import {useTheme} from 'lib/ThemeContext' 16 + import {HITSLOP_10} from 'lib/constants' 17 + 18 + // Custom Dropdown Menu Components 19 + // == 20 + export const DropdownMenuRoot = DropdownMenu.Root 21 + export const DropdownMenuTrigger = DropdownMenu.Trigger 22 + export const DropdownMenuContent = DropdownMenu.Content 23 + type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']> 24 + export const DropdownMenuItem = DropdownMenu.create( 25 + (props: ItemProps & {testID?: string}) => { 26 + const pal = usePalette('default') 27 + const theme = useTheme() 28 + const [focused, setFocused] = React.useState(false) 29 + const {borderColor: backgroundColor} = 30 + theme.colorScheme === 'dark' ? pal.borderDark : pal.border 31 + 32 + return ( 33 + <DropdownMenu.Item 34 + {...props} 35 + style={[styles.item, focused && {backgroundColor: backgroundColor}]} 36 + onFocus={() => { 37 + setFocused(true) 38 + props.onFocus && props.onFocus() 39 + }} 40 + onBlur={() => { 41 + setFocused(false) 42 + props.onBlur && props.onBlur() 43 + }} 44 + /> 45 + ) 46 + }, 47 + 'Item', 48 + ) 49 + type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']> 50 + export const DropdownMenuItemTitle = DropdownMenu.create( 51 + (props: TitleProps) => { 52 + const pal = usePalette('default') 53 + return ( 54 + <DropdownMenu.ItemTitle 55 + {...props} 56 + style={[props.style, pal.text, styles.itemTitle]} 57 + /> 58 + ) 59 + }, 60 + 'ItemTitle', 61 + ) 62 + type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']> 63 + export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => { 64 + return <DropdownMenu.ItemIcon {...props} /> 65 + }, 'ItemIcon') 66 + type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']> 67 + export const DropdownMenuSeparator = DropdownMenu.create( 68 + (props: SeparatorProps) => { 69 + const pal = usePalette('default') 70 + const theme = useTheme() 71 + const {borderColor: separatorColor} = 72 + theme.colorScheme === 'dark' ? pal.borderDark : pal.border 73 + return ( 74 + <DropdownMenu.Separator 75 + {...props} 76 + style={[ 77 + props.style, 78 + styles.separator, 79 + {backgroundColor: separatorColor}, 80 + ]} 81 + /> 82 + ) 83 + }, 84 + 'Separator', 85 + ) 86 + 87 + // Types for Dropdown Menu and Items 88 + export type DropdownItem = { 89 + label: string | 'separator' 90 + onPress?: () => void 91 + testID?: string 92 + icon?: { 93 + ios: MenuItemCommonProps['ios'] 94 + android: string 95 + web: IconProp 96 + } 97 + } 98 + type Props = { 99 + items: DropdownItem[] 100 + children?: React.ReactNode 101 + testID?: string 102 + } 103 + 104 + /* The `NativeDropdown` function uses native iOS and Android dropdown menus. 105 + * It also creates a animated custom dropdown for web that uses 106 + * Radix UI primitives under the hood 107 + * @prop {DropdownItem[]} items - An array of dropdown items 108 + * @prop {React.ReactNode} children - A custom dropdown trigger 109 + */ 110 + export function NativeDropdown({items, children, testID}: Props) { 111 + const pal = usePalette('default') 112 + const theme = useTheme() 113 + const dropDownBackgroundColor = 114 + theme.colorScheme === 'dark' ? pal.btn : pal.viewLight 115 + const defaultCtrlColor = React.useMemo( 116 + () => ({ 117 + color: theme.palette.default.postCtrl, 118 + }), 119 + [theme], 120 + ) as StyleProp<ViewStyle> 121 + 122 + return ( 123 + <DropdownMenuRoot> 124 + <DropdownMenuTrigger action="press"> 125 + <Pressable 126 + testID={testID} 127 + accessibilityRole="button" 128 + style={({pressed}) => [{opacity: pressed ? 0.5 : 1}]} 129 + hitSlop={HITSLOP_10}> 130 + {children ? ( 131 + children 132 + ) : ( 133 + <FontAwesomeIcon 134 + icon="ellipsis" 135 + size={20} 136 + style={[defaultCtrlColor, styles.ellipsis]} 137 + /> 138 + )} 139 + </Pressable> 140 + </DropdownMenuTrigger> 141 + <DropdownMenuContent 142 + style={[styles.content, dropDownBackgroundColor]} 143 + loop> 144 + {items.map((item, index) => { 145 + if (item.label === 'separator') { 146 + return ( 147 + <DropdownMenuSeparator 148 + key={getKey(item.label, index, item.testID)} 149 + /> 150 + ) 151 + } 152 + if (index > 1 && items[index - 1].label === 'separator') { 153 + return ( 154 + <DropdownMenu.Group key={getKey(item.label, index, item.testID)}> 155 + <DropdownMenuItem 156 + key={getKey(item.label, index, item.testID)} 157 + onSelect={item.onPress}> 158 + <DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle> 159 + {item.icon && ( 160 + <DropdownMenuItemIcon 161 + ios={item.icon.ios} 162 + // 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 163 + > 164 + <FontAwesomeIcon 165 + icon={item.icon.web} 166 + size={20} 167 + style={[pal.text]} 168 + /> 169 + </DropdownMenuItemIcon> 170 + )} 171 + </DropdownMenuItem> 172 + </DropdownMenu.Group> 173 + ) 174 + } 175 + return ( 176 + <DropdownMenuItem 177 + key={getKey(item.label, index, item.testID)} 178 + onSelect={item.onPress}> 179 + <DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle> 180 + {item.icon && ( 181 + <DropdownMenuItemIcon 182 + ios={item.icon.ios} 183 + // androidIconName={item.icon.android} 184 + > 185 + <FontAwesomeIcon 186 + icon={item.icon.web} 187 + size={20} 188 + style={[pal.text]} 189 + /> 190 + </DropdownMenuItemIcon> 191 + )} 192 + </DropdownMenuItem> 193 + ) 194 + })} 195 + </DropdownMenuContent> 196 + </DropdownMenuRoot> 197 + ) 198 + } 199 + 200 + const getKey = (label: string, index: number, id?: string) => { 201 + if (id) { 202 + return id 203 + } 204 + return `${label}_${index}` 205 + } 206 + 207 + const styles = StyleSheet.create({ 208 + separator: { 209 + height: 1, 210 + marginVertical: 4, 211 + }, 212 + ellipsis: { 213 + padding: isWeb ? 0 : 10, 214 + }, 215 + content: { 216 + backgroundColor: '#f0f0f0', 217 + borderRadius: 8, 218 + paddingVertical: 4, 219 + paddingHorizontal: 4, 220 + marginTop: 6, 221 + ...Platform.select({ 222 + web: { 223 + animationDuration: '400ms', 224 + animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', 225 + willChange: 'transform, opacity', 226 + animationKeyframes: { 227 + '0%': {opacity: 0, transform: [{scale: 0.5}]}, 228 + '100%': {opacity: 1, transform: [{scale: 1}]}, 229 + }, 230 + boxShadow: 231 + '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)', 232 + transformOrigin: 'var(--radix-dropdown-menu-content-transform-origin)', 233 + }, 234 + }), 235 + }, 236 + item: { 237 + flexDirection: 'row', 238 + justifyContent: 'space-between', 239 + alignItems: 'center', 240 + columnGap: 20, 241 + // @ts-ignore -web 242 + cursor: 'pointer', 243 + paddingVertical: 8, 244 + paddingHorizontal: 12, 245 + borderRadius: 8, 246 + }, 247 + itemTitle: { 248 + fontSize: 18, 249 + }, 250 + })
+148
src/view/com/util/forms/PostDropdownBtn.tsx
··· 1 + import React from 'react' 2 + import {toShareUrl} from 'lib/strings/url-helpers' 3 + import {useStores} from 'state/index' 4 + import {shareUrl} from 'lib/sharing' 5 + import { 6 + NativeDropdown, 7 + DropdownItem as NativeDropdownItem, 8 + } from './NativeDropdown' 9 + import {Pressable} from 'react-native' 10 + 11 + export function PostDropdownBtn({ 12 + testID, 13 + itemUri, 14 + itemCid, 15 + itemHref, 16 + isAuthor, 17 + isThreadMuted, 18 + onCopyPostText, 19 + onOpenTranslate, 20 + onToggleThreadMute, 21 + onDeletePost, 22 + }: { 23 + testID: string 24 + itemUri: string 25 + itemCid: string 26 + itemHref: string 27 + itemTitle: string 28 + isAuthor: boolean 29 + isThreadMuted: boolean 30 + onCopyPostText: () => void 31 + onOpenTranslate: () => void 32 + onToggleThreadMute: () => void 33 + onDeletePost: () => void 34 + }) { 35 + const store = useStores() 36 + 37 + const dropdownItems: NativeDropdownItem[] = [ 38 + { 39 + label: 'Translate', 40 + onPress() { 41 + onOpenTranslate() 42 + }, 43 + testID: 'postDropdownTranslateBtn', 44 + icon: { 45 + ios: { 46 + name: 'character.book.closed', 47 + }, 48 + android: 'ic_menu_sort_alphabetically', 49 + web: 'language', 50 + }, 51 + }, 52 + { 53 + label: 'Copy post text', 54 + onPress() { 55 + onCopyPostText() 56 + }, 57 + testID: 'postDropdownCopyTextBtn', 58 + icon: { 59 + ios: { 60 + name: 'doc.on.doc', 61 + }, 62 + android: 'ic_menu_edit', 63 + web: ['far', 'paste'], 64 + }, 65 + }, 66 + { 67 + label: 'Share', 68 + onPress() { 69 + const url = toShareUrl(itemHref) 70 + shareUrl(url) 71 + }, 72 + testID: 'postDropdownShareBtn', 73 + icon: { 74 + ios: { 75 + name: 'square.and.arrow.up', 76 + }, 77 + android: 'ic_menu_share', 78 + web: 'share', 79 + }, 80 + }, 81 + { 82 + label: 'separator', 83 + }, 84 + { 85 + label: isThreadMuted ? 'Unmute thread' : 'Mute thread', 86 + onPress() { 87 + onToggleThreadMute() 88 + }, 89 + testID: 'postDropdownMuteThreadBtn', 90 + icon: { 91 + ios: { 92 + name: 'speaker.slash', 93 + }, 94 + android: 'ic_lock_silent_mode', 95 + web: 'comment-slash', 96 + }, 97 + }, 98 + { 99 + label: 'separator', 100 + }, 101 + { 102 + label: 'Report post', 103 + onPress() { 104 + store.shell.openModal({ 105 + name: 'report-post', 106 + postUri: itemUri, 107 + postCid: itemCid, 108 + }) 109 + }, 110 + testID: 'postDropdownReportBtn', 111 + icon: { 112 + ios: { 113 + name: 'exclamationmark.triangle', 114 + }, 115 + android: 'ic_menu_report_image', 116 + web: 'circle-exclamation', 117 + }, 118 + }, 119 + isAuthor && { 120 + label: 'separator', 121 + }, 122 + isAuthor && { 123 + label: 'Delete post', 124 + onPress() { 125 + store.shell.openModal({ 126 + name: 'confirm', 127 + title: 'Delete this post?', 128 + message: 'Are you sure? This can not be undone.', 129 + onPressConfirm: onDeletePost, 130 + }) 131 + }, 132 + testID: 'postDropdownDeleteBtn', 133 + icon: { 134 + ios: { 135 + name: 'trash', 136 + }, 137 + android: 'ic_menu_delete', 138 + web: ['far', 'trash-can'], 139 + }, 140 + }, 141 + ].filter(Boolean) as NativeDropdownItem[] 142 + 143 + return ( 144 + <Pressable testID={testID} accessibilityRole="button"> 145 + <NativeDropdown items={dropdownItems} /> 146 + </Pressable> 147 + ) 148 + }
+3 -4
src/view/com/util/load-latest/LoadLatestBtn.web.tsx
··· 5 5 import {usePalette} from 'lib/hooks/usePalette' 6 6 import {LoadLatestBtn as LoadLatestBtnMobile} from './LoadLatestBtnMobile' 7 7 import {isMobileWeb} from 'platform/detection' 8 - 9 - const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} 8 + import {HITSLOP_20} from 'lib/constants' 10 9 11 10 export const LoadLatestBtn = ({ 12 11 onPress, ··· 40 39 minimalShellMode && styles.loadLatestCenteredMinimal, 41 40 ]} 42 41 onPress={onPress} 43 - hitSlop={HITSLOP} 42 + hitSlop={HITSLOP_20} 44 43 accessibilityRole="button" 45 44 accessibilityLabel={label} 46 45 accessibilityHint=""> ··· 52 51 <TouchableOpacity 53 52 style={[pal.view, pal.borderDark, styles.loadLatest]} 54 53 onPress={onPress} 55 - hitSlop={HITSLOP} 54 + hitSlop={HITSLOP_20} 56 55 accessibilityRole="button" 57 56 accessibilityLabel={label} 58 57 accessibilityHint="">
+2 -3
src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
··· 7 7 import {useStores} from 'state/index' 8 8 import {usePalette} from 'lib/hooks/usePalette' 9 9 import {colors} from 'lib/styles' 10 - 11 - const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} 10 + import {HITSLOP_20} from 'lib/constants' 12 11 13 12 export const LoadLatestBtn = observer( 14 13 ({ ··· 35 34 }, 36 35 ]} 37 36 onPress={onPress} 38 - hitSlop={HITSLOP} 37 + hitSlop={HITSLOP_20} 39 38 accessibilityRole="button" 40 39 accessibilityLabel={label} 41 40 accessibilityHint="">
+18 -36
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 6 6 View, 7 7 ViewStyle, 8 8 } from 'react-native' 9 - import { 10 - FontAwesomeIcon, 11 - FontAwesomeIconStyle, 12 - } from '@fortawesome/react-native-fontawesome' 13 9 // DISABLED see #135 14 10 // import { 15 11 // TriggerableAnimated, 16 12 // TriggerableAnimatedRef, 17 13 // } from './anim/TriggerableAnimated' 18 14 import {Text} from '../text/Text' 19 - import {PostDropdownBtn} from '../forms/DropdownButton' 15 + import {PostDropdownBtn} from '../forms/PostDropdownBtn' 20 16 import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons' 21 17 import {s, colors} from 'lib/styles' 22 18 import {pluralize} from 'lib/strings/helpers' ··· 24 20 import {useStores} from 'state/index' 25 21 import {RepostButton} from './RepostButton' 26 22 import {Haptics} from 'lib/haptics' 23 + import {createHitslop} from 'lib/constants' 27 24 28 25 interface PostCtrlsOpts { 29 26 itemUri: string ··· 56 53 onDeletePost: () => void 57 54 } 58 55 59 - const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} 56 + const HITSLOP = createHitslop(5) 60 57 61 58 // DISABLED see #135 62 59 /* ··· 222 219 </Text> 223 220 ) : undefined} 224 221 </TouchableOpacity> 225 - <View> 226 - {opts.big ? undefined : ( 227 - <PostDropdownBtn 228 - testID="postDropdownBtn" 229 - style={styles.ctrl} 230 - itemUri={opts.itemUri} 231 - itemCid={opts.itemCid} 232 - itemHref={opts.itemHref} 233 - itemTitle={opts.itemTitle} 234 - isAuthor={opts.isAuthor} 235 - isThreadMuted={opts.isThreadMuted} 236 - onCopyPostText={opts.onCopyPostText} 237 - onOpenTranslate={opts.onOpenTranslate} 238 - onToggleThreadMute={opts.onToggleThreadMute} 239 - onDeletePost={opts.onDeletePost}> 240 - <FontAwesomeIcon 241 - icon="ellipsis-h" 242 - size={18} 243 - style={[ 244 - s.mt2, 245 - s.mr5, 246 - { 247 - color: 248 - theme.colorScheme === 'light' ? colors.gray4 : colors.gray5, 249 - } as FontAwesomeIconStyle, 250 - ]} 251 - /> 252 - </PostDropdownBtn> 253 - )} 254 - </View> 222 + {opts.big ? undefined : ( 223 + <PostDropdownBtn 224 + testID="postDropdownBtn" 225 + itemUri={opts.itemUri} 226 + itemCid={opts.itemCid} 227 + itemHref={opts.itemHref} 228 + itemTitle={opts.itemTitle} 229 + isAuthor={opts.isAuthor} 230 + isThreadMuted={opts.isThreadMuted} 231 + onCopyPostText={opts.onCopyPostText} 232 + onOpenTranslate={opts.onOpenTranslate} 233 + onToggleThreadMute={opts.onToggleThreadMute} 234 + onDeletePost={opts.onDeletePost} 235 + /> 236 + )} 255 237 {/* used for adding pad to the right side */} 256 238 <View /> 257 239 </View>
+2 -1
src/view/com/util/post-ctrls/RepostButton.tsx
··· 6 6 import {Text} from '../text/Text' 7 7 import {pluralize} from 'lib/strings/helpers' 8 8 import {useStores} from 'state/index' 9 + import {createHitslop} from 'lib/constants' 9 10 10 - const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} 11 + const HITSLOP = createHitslop(5) 11 12 12 13 interface Props { 13 14 isReposted: boolean
+17 -10
src/view/screens/CustomFeed.tsx
··· 29 29 import {ComposeIcon2} from 'lib/icons' 30 30 import {FAB} from '../com/util/fab/FAB' 31 31 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 32 - import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton' 33 32 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' 34 33 import {EmptyState} from 'view/com/util/EmptyState' 35 34 import {useAnalytics} from 'lib/analytics/analytics' 35 + import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' 36 36 import {makeProfileLink} from 'lib/routes/links' 37 37 38 38 type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'> ··· 121 121 testID: 'feedHeaderDropdownRemoveBtn', 122 122 label: 'Remove from my feeds', 123 123 onPress: onToggleSaved, 124 + icon: { 125 + ios: { 126 + name: 'trash', 127 + }, 128 + android: 'ic_delete', 129 + web: 'trash', 130 + }, 124 131 }, 125 132 { 126 133 testID: 'feedHeaderDropdownShareBtn', 127 134 label: 'Share link', 128 135 onPress: onPressShare, 136 + icon: { 137 + ios: { 138 + name: 'square.and.arrow.up', 139 + }, 140 + android: 'ic_menu_share', 141 + web: 'share', 142 + }, 129 143 }, 130 144 ] 131 145 return items ··· 163 177 </Button> 164 178 ) : undefined} 165 179 {currentFeed?.isSaved ? ( 166 - <DropdownButton 180 + <NativeDropdown 167 181 testID="feedHeaderDropdownBtn" 168 - type="default-light" 169 182 items={dropdownItems} 170 - menuWidth={250}> 171 - <FontAwesomeIcon 172 - icon="ellipsis" 173 - color={pal.colors.textLight} 174 - size={18} 175 - /> 176 - </DropdownButton> 183 + /> 177 184 ) : ( 178 185 <Button 179 186 type="default-light"
+14 -6
src/view/screens/Settings.tsx
··· 3 3 ActivityIndicator, 4 4 Linking, 5 5 Platform, 6 + Pressable, 6 7 StyleSheet, 7 8 TextStyle, 8 9 TouchableOpacity, ··· 30 31 import {Text} from '../com/util/text/Text' 31 32 import * as Toast from '../com/util/Toast' 32 33 import {UserAvatar} from '../com/util/UserAvatar' 33 - import {DropdownButton} from 'view/com/util/forms/DropdownButton' 34 34 import {ToggleButton} from 'view/com/util/forms/ToggleButton' 35 35 import {SelectableBtn} from 'view/com/util/forms/SelectableBtn' 36 36 import {usePalette} from 'lib/hooks/usePalette' ··· 50 50 // -prf 51 51 import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header' 52 52 import {STATUS_PAGE_URL} from 'lib/constants' 53 + import {DropdownItem, NativeDropdown} from 'view/com/util/forms/NativeDropdown' 53 54 54 55 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> 55 56 export const SettingsScreen = withAuthRequired( ··· 565 566 function AccountDropdownBtn({handle}: {handle: string}) { 566 567 const store = useStores() 567 568 const pal = usePalette('default') 568 - const items = [ 569 + const items: DropdownItem[] = [ 569 570 { 570 571 label: 'Remove account', 571 572 onPress: () => { 572 573 store.session.removeAccount(handle) 573 574 Toast.show('Account removed from quick access') 575 + }, 576 + icon: { 577 + ios: { 578 + name: 'trash', 579 + }, 580 + android: 'ic_delete', 581 + web: 'trash', 574 582 }, 575 583 }, 576 584 ] 577 585 return ( 578 - <View style={s.pl10}> 579 - <DropdownButton type="bare" items={items}> 586 + <Pressable accessibilityRole="button" style={s.pl10}> 587 + <NativeDropdown testID="accountSettingsDropdownBtn" items={items}> 580 588 <FontAwesomeIcon 581 589 icon="ellipsis-h" 582 590 style={pal.textLight as FontAwesomeIconStyle} 583 591 /> 584 - </DropdownButton> 585 - </View> 592 + </NativeDropdown> 593 + </Pressable> 586 594 ) 587 595 } 588 596
+373 -1
yarn.lock
··· 2586 2586 dependencies: 2587 2587 regenerator-runtime "^0.13.11" 2588 2588 2589 + "@babel/runtime@^7.13.10": 2590 + version "7.22.6" 2591 + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" 2592 + integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== 2593 + dependencies: 2594 + regenerator-runtime "^0.13.11" 2595 + 2589 2596 "@babel/template@^7.0.0", "@babel/template@^7.22.5", "@babel/template@^7.3.3": 2590 2597 version "7.22.5" 2591 2598 resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" ··· 2832 2839 version "0.5.7" 2833 2840 resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" 2834 2841 integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== 2842 + 2843 + "@dominicstop/ts-event-emitter@^1.1.0": 2844 + version "1.1.0" 2845 + resolved "https://registry.yarnpkg.com/@dominicstop/ts-event-emitter/-/ts-event-emitter-1.1.0.tgz#1f3d3fa878a1ccab686931280757954719cf88e4" 2846 + integrity sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw== 2835 2847 2836 2848 "@egjs/hammerjs@^2.0.17": 2837 2849 version "2.0.17" ··· 3240 3252 chalk "^4.1.0" 3241 3253 find-up "^5.0.0" 3242 3254 js-yaml "^4.1.0" 3255 + 3256 + "@floating-ui/core@^1.3.1": 3257 + version "1.3.1" 3258 + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.1.tgz#4d795b649cc3b1cbb760d191c80dcb4353c9a366" 3259 + integrity sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g== 3260 + 3261 + "@floating-ui/dom@^1.3.0": 3262 + version "1.4.5" 3263 + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.4.5.tgz#336dfb9870c98b471ff5802002982e489b8bd1c5" 3264 + integrity sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw== 3265 + dependencies: 3266 + "@floating-ui/core" "^1.3.1" 3267 + 3268 + "@floating-ui/react-dom@^2.0.0": 3269 + version "2.0.1" 3270 + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.1.tgz#7972a4fc488a8c746cded3cfe603b6057c308a91" 3271 + integrity sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA== 3272 + dependencies: 3273 + "@floating-ui/dom" "^1.3.0" 3243 3274 3244 3275 "@fortawesome/fontawesome-common-types@6.4.0": 3245 3276 version "6.4.0" ··· 3992 4023 resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" 3993 4024 integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== 3994 4025 4026 + "@radix-ui/primitive@1.0.1": 4027 + version "1.0.1" 4028 + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd" 4029 + integrity sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw== 4030 + dependencies: 4031 + "@babel/runtime" "^7.13.10" 4032 + 4033 + "@radix-ui/react-arrow@1.0.3": 4034 + version "1.0.3" 4035 + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" 4036 + integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA== 4037 + dependencies: 4038 + "@babel/runtime" "^7.13.10" 4039 + "@radix-ui/react-primitive" "1.0.3" 4040 + 4041 + "@radix-ui/react-collection@1.0.3": 4042 + version "1.0.3" 4043 + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159" 4044 + integrity sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA== 4045 + dependencies: 4046 + "@babel/runtime" "^7.13.10" 4047 + "@radix-ui/react-compose-refs" "1.0.1" 4048 + "@radix-ui/react-context" "1.0.1" 4049 + "@radix-ui/react-primitive" "1.0.3" 4050 + "@radix-ui/react-slot" "1.0.2" 4051 + 4052 + "@radix-ui/react-compose-refs@1.0.1": 4053 + version "1.0.1" 4054 + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989" 4055 + integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw== 4056 + dependencies: 4057 + "@babel/runtime" "^7.13.10" 4058 + 4059 + "@radix-ui/react-context-menu@^2.0.1": 4060 + version "2.1.4" 4061 + resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.1.4.tgz#250420d259d3cebe026b7658414c516a1204de3f" 4062 + integrity sha512-HVHLUtZOBiR2Fh5l07qQ9y0IgX4dGZF0S9Gwdk4CVA+DL9afSphvFNa4nRiw6RNgb6quwLV4dLPF/gFDvNaOcQ== 4063 + dependencies: 4064 + "@babel/runtime" "^7.13.10" 4065 + "@radix-ui/primitive" "1.0.1" 4066 + "@radix-ui/react-context" "1.0.1" 4067 + "@radix-ui/react-menu" "2.0.5" 4068 + "@radix-ui/react-primitive" "1.0.3" 4069 + "@radix-ui/react-use-callback-ref" "1.0.1" 4070 + "@radix-ui/react-use-controllable-state" "1.0.1" 4071 + 4072 + "@radix-ui/react-context@1.0.1": 4073 + version "1.0.1" 4074 + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c" 4075 + integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg== 4076 + dependencies: 4077 + "@babel/runtime" "^7.13.10" 4078 + 4079 + "@radix-ui/react-direction@1.0.1": 4080 + version "1.0.1" 4081 + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" 4082 + integrity sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA== 4083 + dependencies: 4084 + "@babel/runtime" "^7.13.10" 4085 + 4086 + "@radix-ui/react-dismissable-layer@1.0.4": 4087 + version "1.0.4" 4088 + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz#883a48f5f938fa679427aa17fcba70c5494c6978" 4089 + integrity sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg== 4090 + dependencies: 4091 + "@babel/runtime" "^7.13.10" 4092 + "@radix-ui/primitive" "1.0.1" 4093 + "@radix-ui/react-compose-refs" "1.0.1" 4094 + "@radix-ui/react-primitive" "1.0.3" 4095 + "@radix-ui/react-use-callback-ref" "1.0.1" 4096 + "@radix-ui/react-use-escape-keydown" "1.0.3" 4097 + 4098 + "@radix-ui/react-dropdown-menu@^2.0.1": 4099 + version "2.0.5" 4100 + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz#19bf4de8ffa348b4eb6a86842f14eff93d741170" 4101 + integrity sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw== 4102 + dependencies: 4103 + "@babel/runtime" "^7.13.10" 4104 + "@radix-ui/primitive" "1.0.1" 4105 + "@radix-ui/react-compose-refs" "1.0.1" 4106 + "@radix-ui/react-context" "1.0.1" 4107 + "@radix-ui/react-id" "1.0.1" 4108 + "@radix-ui/react-menu" "2.0.5" 4109 + "@radix-ui/react-primitive" "1.0.3" 4110 + "@radix-ui/react-use-controllable-state" "1.0.1" 4111 + 4112 + "@radix-ui/react-focus-guards@1.0.1": 4113 + version "1.0.1" 4114 + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" 4115 + integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA== 4116 + dependencies: 4117 + "@babel/runtime" "^7.13.10" 4118 + 4119 + "@radix-ui/react-focus-scope@1.0.3": 4120 + version "1.0.3" 4121 + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz#9c2e8d4ed1189a1d419ee61edd5c1828726472f9" 4122 + integrity sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ== 4123 + dependencies: 4124 + "@babel/runtime" "^7.13.10" 4125 + "@radix-ui/react-compose-refs" "1.0.1" 4126 + "@radix-ui/react-primitive" "1.0.3" 4127 + "@radix-ui/react-use-callback-ref" "1.0.1" 4128 + 4129 + "@radix-ui/react-id@1.0.1": 4130 + version "1.0.1" 4131 + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" 4132 + integrity sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ== 4133 + dependencies: 4134 + "@babel/runtime" "^7.13.10" 4135 + "@radix-ui/react-use-layout-effect" "1.0.1" 4136 + 4137 + "@radix-ui/react-menu@2.0.5": 4138 + version "2.0.5" 4139 + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.5.tgz#a7d78b0808c4d38269240bf5d5c7ffea3e225e16" 4140 + integrity sha512-Gw4f9pwdH+w5w+49k0gLjN0PfRDHvxmAgG16AbyJZ7zhwZ6PBHKtWohvnSwfusfnK3L68dpBREHpVkj8wEM7ZA== 4141 + dependencies: 4142 + "@babel/runtime" "^7.13.10" 4143 + "@radix-ui/primitive" "1.0.1" 4144 + "@radix-ui/react-collection" "1.0.3" 4145 + "@radix-ui/react-compose-refs" "1.0.1" 4146 + "@radix-ui/react-context" "1.0.1" 4147 + "@radix-ui/react-direction" "1.0.1" 4148 + "@radix-ui/react-dismissable-layer" "1.0.4" 4149 + "@radix-ui/react-focus-guards" "1.0.1" 4150 + "@radix-ui/react-focus-scope" "1.0.3" 4151 + "@radix-ui/react-id" "1.0.1" 4152 + "@radix-ui/react-popper" "1.1.2" 4153 + "@radix-ui/react-portal" "1.0.3" 4154 + "@radix-ui/react-presence" "1.0.1" 4155 + "@radix-ui/react-primitive" "1.0.3" 4156 + "@radix-ui/react-roving-focus" "1.0.4" 4157 + "@radix-ui/react-slot" "1.0.2" 4158 + "@radix-ui/react-use-callback-ref" "1.0.1" 4159 + aria-hidden "^1.1.1" 4160 + react-remove-scroll "2.5.5" 4161 + 4162 + "@radix-ui/react-popper@1.1.2": 4163 + version "1.1.2" 4164 + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9" 4165 + integrity sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg== 4166 + dependencies: 4167 + "@babel/runtime" "^7.13.10" 4168 + "@floating-ui/react-dom" "^2.0.0" 4169 + "@radix-ui/react-arrow" "1.0.3" 4170 + "@radix-ui/react-compose-refs" "1.0.1" 4171 + "@radix-ui/react-context" "1.0.1" 4172 + "@radix-ui/react-primitive" "1.0.3" 4173 + "@radix-ui/react-use-callback-ref" "1.0.1" 4174 + "@radix-ui/react-use-layout-effect" "1.0.1" 4175 + "@radix-ui/react-use-rect" "1.0.1" 4176 + "@radix-ui/react-use-size" "1.0.1" 4177 + "@radix-ui/rect" "1.0.1" 4178 + 4179 + "@radix-ui/react-portal@1.0.3": 4180 + version "1.0.3" 4181 + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1" 4182 + integrity sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA== 4183 + dependencies: 4184 + "@babel/runtime" "^7.13.10" 4185 + "@radix-ui/react-primitive" "1.0.3" 4186 + 4187 + "@radix-ui/react-presence@1.0.1": 4188 + version "1.0.1" 4189 + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" 4190 + integrity sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg== 4191 + dependencies: 4192 + "@babel/runtime" "^7.13.10" 4193 + "@radix-ui/react-compose-refs" "1.0.1" 4194 + "@radix-ui/react-use-layout-effect" "1.0.1" 4195 + 4196 + "@radix-ui/react-primitive@1.0.3": 4197 + version "1.0.3" 4198 + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0" 4199 + integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g== 4200 + dependencies: 4201 + "@babel/runtime" "^7.13.10" 4202 + "@radix-ui/react-slot" "1.0.2" 4203 + 4204 + "@radix-ui/react-roving-focus@1.0.4": 4205 + version "1.0.4" 4206 + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" 4207 + integrity sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ== 4208 + dependencies: 4209 + "@babel/runtime" "^7.13.10" 4210 + "@radix-ui/primitive" "1.0.1" 4211 + "@radix-ui/react-collection" "1.0.3" 4212 + "@radix-ui/react-compose-refs" "1.0.1" 4213 + "@radix-ui/react-context" "1.0.1" 4214 + "@radix-ui/react-direction" "1.0.1" 4215 + "@radix-ui/react-id" "1.0.1" 4216 + "@radix-ui/react-primitive" "1.0.3" 4217 + "@radix-ui/react-use-callback-ref" "1.0.1" 4218 + "@radix-ui/react-use-controllable-state" "1.0.1" 4219 + 4220 + "@radix-ui/react-slot@1.0.2": 4221 + version "1.0.2" 4222 + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" 4223 + integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== 4224 + dependencies: 4225 + "@babel/runtime" "^7.13.10" 4226 + "@radix-ui/react-compose-refs" "1.0.1" 4227 + 4228 + "@radix-ui/react-use-callback-ref@1.0.1": 4229 + version "1.0.1" 4230 + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a" 4231 + integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ== 4232 + dependencies: 4233 + "@babel/runtime" "^7.13.10" 4234 + 4235 + "@radix-ui/react-use-controllable-state@1.0.1": 4236 + version "1.0.1" 4237 + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286" 4238 + integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA== 4239 + dependencies: 4240 + "@babel/runtime" "^7.13.10" 4241 + "@radix-ui/react-use-callback-ref" "1.0.1" 4242 + 4243 + "@radix-ui/react-use-escape-keydown@1.0.3": 4244 + version "1.0.3" 4245 + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" 4246 + integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== 4247 + dependencies: 4248 + "@babel/runtime" "^7.13.10" 4249 + "@radix-ui/react-use-callback-ref" "1.0.1" 4250 + 4251 + "@radix-ui/react-use-layout-effect@1.0.1": 4252 + version "1.0.1" 4253 + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" 4254 + integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ== 4255 + dependencies: 4256 + "@babel/runtime" "^7.13.10" 4257 + 4258 + "@radix-ui/react-use-rect@1.0.1": 4259 + version "1.0.1" 4260 + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2" 4261 + integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw== 4262 + dependencies: 4263 + "@babel/runtime" "^7.13.10" 4264 + "@radix-ui/rect" "1.0.1" 4265 + 4266 + "@radix-ui/react-use-size@1.0.1": 4267 + version "1.0.1" 4268 + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" 4269 + integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g== 4270 + dependencies: 4271 + "@babel/runtime" "^7.13.10" 4272 + "@radix-ui/react-use-layout-effect" "1.0.1" 4273 + 4274 + "@radix-ui/rect@1.0.1": 4275 + version "1.0.1" 4276 + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f" 4277 + integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ== 4278 + dependencies: 4279 + "@babel/runtime" "^7.13.10" 4280 + 3995 4281 "@react-native-async-storage/async-storage@^1.15.15", "@react-native-async-storage/async-storage@^1.17.6": 3996 4282 version "1.18.2" 3997 4283 resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.18.2.tgz#ec8fd487a0b6c9500b43ece4b8779d1561f12e91" ··· 4218 4504 version "1.3.0" 4219 4505 resolved "https://registry.yarnpkg.com/@react-native-community/eslint-plugin/-/eslint-plugin-1.3.0.tgz#9e558170c106bbafaa1ef502bd8e6d4651012bf9" 4220 4506 integrity sha512-+zDZ20NUnSWghj7Ku5aFphMzuM9JulqCW+aPXT6IfIXFbb8tzYTTOSeRFOtuekJ99ibW2fUCSsjuKNlwDIbHFg== 4507 + 4508 + "@react-native-menu/menu@^0.8.0": 4509 + version "0.8.0" 4510 + resolved "https://registry.yarnpkg.com/@react-native-menu/menu/-/menu-0.8.0.tgz#dbf227c2081e5ffd3d2073ee68ecc84cf8639727" 4511 + integrity sha512-kxiT6ySZsDbBvNWovrKVAfs4AQvAytKIf0f8KQLkVO6eNYMUmONBQPzi6onTTbVujXtZHambo7qr/PcedaR8Tg== 4221 4512 4222 4513 "@react-native/assets@1.0.0": 4223 4514 version "1.0.0" ··· 6850 7141 resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 6851 7142 integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 6852 7143 7144 + aria-hidden@^1.1.1: 7145 + version "1.2.3" 7146 + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" 7147 + integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== 7148 + dependencies: 7149 + tslib "^2.0.0" 7150 + 6853 7151 aria-query@^5.1.3: 6854 7152 version "5.3.0" 6855 7153 resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" ··· 8978 9276 resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" 8979 9277 integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== 8980 9278 9279 + detect-node-es@^1.1.0: 9280 + version "1.1.0" 9281 + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" 9282 + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== 9283 + 8981 9284 detect-node@^2.0.4: 8982 9285 version "2.1.0" 8983 9286 resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" ··· 10819 11122 has-proto "^1.0.1" 10820 11123 has-symbols "^1.0.3" 10821 11124 11125 + get-nonce@^1.0.0: 11126 + version "1.0.1" 11127 + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" 11128 + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== 11129 + 10822 11130 get-own-enumerable-property-symbols@^3.0.0: 10823 11131 version "3.0.2" 10824 11132 resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" ··· 16750 17058 invariant "^2.2.4" 16751 17059 opencollective-postinstall "^2.0.3" 16752 17060 17061 + react-native-ios-context-menu@^1.15.3: 17062 + version "1.15.3" 17063 + resolved "https://registry.yarnpkg.com/react-native-ios-context-menu/-/react-native-ios-context-menu-1.15.3.tgz#c02e6a7af2df8c08d0b3e1c8f3395484b3c9c760" 17064 + integrity sha512-UNkVl7ocvSpNaEpvBvE1aHOfDy/DFdZ5I+ElfnTXFsRxrVZmxLtST0b1q2wSWGWDmd2Ig2AYd7GRbYtcY222Ag== 17065 + dependencies: 17066 + "@dominicstop/ts-event-emitter" "^1.1.0" 17067 + 16753 17068 react-native-linear-gradient@^2.6.2: 16754 17069 version "2.7.3" 16755 17070 resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.7.3.tgz#f77b71ed7c955e033f9cba5fc8478df57953eb27" ··· 16892 17207 resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.4.3.tgz#966f1750c191672e76e16c2efa569150cc73ab53" 16893 17208 integrity sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA== 16894 17209 17210 + react-remove-scroll-bar@^2.3.3: 17211 + version "2.3.4" 17212 + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9" 17213 + integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A== 17214 + dependencies: 17215 + react-style-singleton "^2.2.1" 17216 + tslib "^2.0.0" 17217 + 17218 + react-remove-scroll@2.5.5: 17219 + version "2.5.5" 17220 + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" 17221 + integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== 17222 + dependencies: 17223 + react-remove-scroll-bar "^2.3.3" 17224 + react-style-singleton "^2.2.1" 17225 + tslib "^2.1.0" 17226 + use-callback-ref "^1.3.0" 17227 + use-sidecar "^1.1.2" 17228 + 16895 17229 react-responsive@^9.0.2: 16896 17230 version "9.0.2" 16897 17231 resolved "https://registry.yarnpkg.com/react-responsive/-/react-responsive-9.0.2.tgz#34531ca77a61e7a8775714016d21241df7e4205c" ··· 16964 17298 dependencies: 16965 17299 object-assign "^4.1.1" 16966 17300 react-is "^16.12.0 || ^17.0.0 || ^18.0.0" 17301 + 17302 + react-style-singleton@^2.2.1: 17303 + version "2.2.1" 17304 + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" 17305 + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== 17306 + dependencies: 17307 + get-nonce "^1.0.0" 17308 + invariant "^2.2.4" 17309 + tslib "^2.0.0" 16967 17310 16968 17311 react-test-renderer@18.2.0: 16969 17312 version "18.2.0" ··· 17757 18100 version "1.2.0" 17758 18101 resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 17759 18102 integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 18103 + 18104 + sf-symbols-typescript@^1.0.0: 18105 + version "1.0.0" 18106 + resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-1.0.0.tgz#94e9210bf27e7583f9749a0d07bd4f4937ea488f" 18107 + integrity sha512-DkS7q3nN68dEMb4E18HFPDAvyrjDZK9YAQQF2QxeFu9gp2xRDXFMF8qLJ1EmQ/qeEGQmop4lmMM1WtYJTIcCMw== 17760 18108 17761 18109 shallow-clone@^3.0.0: 17762 18110 version "3.0.1" ··· 18966 19314 resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" 18967 19315 integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 18968 19316 18969 - 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: 19317 + 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: 18970 19318 version "2.6.0" 18971 19319 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" 18972 19320 integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== ··· 19289 19637 querystringify "^2.1.1" 19290 19638 requires-port "^1.0.0" 19291 19639 19640 + use-callback-ref@^1.3.0: 19641 + version "1.3.0" 19642 + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" 19643 + integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== 19644 + dependencies: 19645 + tslib "^2.0.0" 19646 + 19292 19647 use-latest-callback@^0.1.5: 19293 19648 version "0.1.6" 19294 19649 resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.6.tgz#3fa6e7babbb5f9bfa24b5094b22939e1e92ebcf6" 19295 19650 integrity sha512-VO/P91A/PmKH9bcN9a7O3duSuxe6M14ZoYXgA6a8dab8doWNdhiIHzEkX/jFeTTRBsX0Ubk6nG4q2NIjNsj+bg== 19651 + 19652 + use-sidecar@^1.1.2: 19653 + version "1.1.2" 19654 + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" 19655 + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== 19656 + dependencies: 19657 + detect-node-es "^1.1.0" 19658 + tslib "^2.0.0" 19296 19659 19297 19660 use-sync-external-store@^1.0.0: 19298 19661 version "1.2.0" ··· 20201 20564 version "0.1.0" 20202 20565 resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 20203 20566 integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 20567 + 20568 + zeego@^1.6.2: 20569 + version "1.6.2" 20570 + resolved "https://registry.yarnpkg.com/zeego/-/zeego-1.6.2.tgz#6051ecc99cd82ced2f49ab14b167398323f8618c" 20571 + integrity sha512-6SSKzW69Z0Px2v3kF5lsoVZeBOLf22Xl38XLsvIrdQS2Mq9WZOrEvLU4JA6pxagKw54f6LLHyqtiWAAM9U/9pg== 20572 + dependencies: 20573 + "@radix-ui/react-context-menu" "^2.0.1" 20574 + "@radix-ui/react-dropdown-menu" "^2.0.1" 20575 + sf-symbols-typescript "^1.0.0" 20204 20576 20205 20577 zod@^3.14.2, zod@^3.20.2, zod@^3.21.4: 20206 20578 version "3.21.4"