Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'custom-algos' into main

+4590 -1358
+5 -1
.eslintrc.js
··· 1 1 module.exports = { 2 2 root: true, 3 - extends: ['@react-native-community', 'plugin:react-native-a11y/ios'], 3 + extends: [ 4 + '@react-native-community', 5 + 'plugin:react-native-a11y/ios', 6 + 'prettier', 7 + ], 4 8 parser: '@typescript-eslint/parser', 5 9 plugins: ['@typescript-eslint', 'detox'], 6 10 ignorePatterns: [
+2 -2
app.json
··· 4 4 "slug": "bluesky", 5 5 "scheme": "bluesky", 6 6 "owner": "blueskysocial", 7 - "version": "1.28.0", 7 + "version": "1.29.0", 8 8 "orientation": "portrait", 9 9 "icon": "./assets/icon.png", 10 10 "userInterfaceStyle": "light", ··· 40 40 "backgroundColor": "#ffffff" 41 41 }, 42 42 "android": { 43 - "versionCode": 14, 43 + "versionCode": 15, 44 44 "adaptiveIcon": { 45 45 "foregroundImage": "./assets/adaptive-icon.png", 46 46 "backgroundColor": "#ffffff"
+5
bskyweb/cmd/bskyweb/server.go
··· 107 107 108 108 // generic routes 109 109 e.GET("/search", server.WebGeneric) 110 + e.GET("/search/feeds", server.WebGeneric) 111 + e.GET("/feeds", server.WebGeneric) 110 112 e.GET("/notifications", server.WebGeneric) 111 113 e.GET("/moderation", server.WebGeneric) 112 114 e.GET("/moderation/mute-lists", server.WebGeneric) ··· 114 116 e.GET("/moderation/blocked-accounts", server.WebGeneric) 115 117 e.GET("/settings", server.WebGeneric) 116 118 e.GET("/settings/app-passwords", server.WebGeneric) 119 + e.GET("/settings/saved-feeds", server.WebGeneric) 117 120 e.GET("/sys/debug", server.WebGeneric) 118 121 e.GET("/sys/log", server.WebGeneric) 119 122 e.GET("/support", server.WebGeneric) ··· 127 130 e.GET("/profile/:handle/follows", server.WebGeneric) 128 131 e.GET("/profile/:handle/followers", server.WebGeneric) 129 132 e.GET("/profile/:handle/lists/:rkey", server.WebGeneric) 133 + e.GET("/profile/:handle/feed/:rkey", server.WebGeneric) 134 + e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric) 130 135 131 136 // post endpoints; only first populates info 132 137 e.GET("/profile/:handle/post/:rkey", server.WebPost)
+3
jest/test-pds.ts
··· 13 13 const SECOND = 1000 14 14 const MINUTE = SECOND * 60 15 15 const HOUR = MINUTE * 60 16 + const DAY = HOUR * 24 16 17 17 18 export interface TestUser { 18 19 email: string ··· 66 67 adminPassword: ADMIN_PASSWORD, 67 68 inviteRequired, 68 69 didPlcUrl: plcUrl, 70 + didCacheMaxTTL: DAY, 71 + didCacheStaleTTL: HOUR, 69 72 jwtSecret: 'jwt-secret', 70 73 availableUserDomains: ['.test'], 71 74 appUrlPasswordReset: 'app://forgot-password',
+4 -3
package.json
··· 1 1 { 2 2 "name": "bsky.app", 3 - "version": "1.28.0", 3 + "version": "1.29.0", 4 4 "private": true, 5 5 "scripts": { 6 6 "postinstall": "patch-package", ··· 22 22 "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" 23 23 }, 24 24 "dependencies": { 25 - "@atproto/api": "0.3.3", 25 + "@atproto/api": "0.3.8", 26 26 "@bam.tech/react-native-image-resizer": "^3.0.4", 27 27 "@braintree/sanitize-url": "^6.0.2", 28 28 "@expo/webpack-config": "^18.0.1", ··· 111 111 "react-native": "0.71.7", 112 112 "react-native-appstate-hook": "^1.0.6", 113 113 "react-native-background-fetch": "^4.1.8", 114 + "react-native-draggable-flatlist": "^4.0.1", 114 115 "react-native-drawer-layout": "^3.2.0", 115 116 "react-native-fs": "^2.20.0", 116 117 "react-native-gesture-handler": "~2.9.0", ··· 140 141 "zod": "^3.20.2" 141 142 }, 142 143 "devDependencies": { 143 - "@atproto/pds": "^0.1.8", 144 + "@atproto/pds": "^0.1.10", 144 145 "@babel/core": "^7.20.0", 145 146 "@babel/preset-env": "^7.20.0", 146 147 "@babel/runtime": "^7.20.0",
+51 -1
src/Navigation.tsx
··· 14 14 import { 15 15 HomeTabNavigatorParams, 16 16 SearchTabNavigatorParams, 17 + FeedsTabNavigatorParams, 17 18 NotificationsTabNavigatorParams, 18 19 FlatNavigatorParams, 19 20 AllNavigatorParams, ··· 32 33 33 34 import {HomeScreen} from './view/screens/Home' 34 35 import {SearchScreen} from './view/screens/Search' 36 + import {FeedsScreen} from './view/screens/Feeds' 35 37 import {NotificationsScreen} from './view/screens/Notifications' 36 38 import {ModerationScreen} from './view/screens/Moderation' 37 39 import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists' 40 + import {DiscoverFeedsScreen} from 'view/screens/DiscoverFeeds' 38 41 import {NotFoundScreen} from './view/screens/NotFound' 39 42 import {SettingsScreen} from './view/screens/Settings' 40 43 import {ProfileScreen} from './view/screens/Profile' 41 44 import {ProfileFollowersScreen} from './view/screens/ProfileFollowers' 42 45 import {ProfileFollowsScreen} from './view/screens/ProfileFollows' 46 + import {CustomFeedScreen} from './view/screens/CustomFeed' 47 + import {CustomFeedLikedByScreen} from './view/screens/CustomFeedLikedBy' 43 48 import {ProfileListScreen} from './view/screens/ProfileList' 44 49 import {PostThreadScreen} from './view/screens/PostThread' 45 50 import {PostLikedByScreen} from './view/screens/PostLikedBy' ··· 54 59 import {AppPasswords} from 'view/screens/AppPasswords' 55 60 import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' 56 61 import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' 62 + import {SavedFeeds} from 'view/screens/SavedFeeds' 57 63 import {getRoutingInstrumentation} from 'lib/sentry' 58 64 import {bskyTitle} from 'lib/strings/headings' 59 65 ··· 61 67 62 68 const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>() 63 69 const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>() 70 + const FeedsTab = createNativeStackNavigator<FeedsTabNavigatorParams>() 64 71 const NotificationsTab = 65 72 createNativeStackNavigator<NotificationsTabNavigatorParams>() 66 73 const MyProfileTab = createNativeStackNavigator<MyProfileTabNavigatorParams>() ··· 101 108 options={{title: title('Blocked Accounts')}} 102 109 /> 103 110 <Stack.Screen 111 + name="DiscoverFeeds" 112 + component={DiscoverFeedsScreen} 113 + options={{title: title('Discover Feeds')}} 114 + /> 115 + <Stack.Screen 104 116 name="Settings" 105 117 component={SettingsScreen} 106 118 options={{title: title('Settings')}} ··· 145 157 options={({route}) => ({title: title(`Post by @${route.params.name}`)})} 146 158 /> 147 159 <Stack.Screen 160 + name="CustomFeed" 161 + component={CustomFeedScreen} 162 + options={{title: title('Feed')}} 163 + /> 164 + <Stack.Screen 165 + name="CustomFeedLikedBy" 166 + component={CustomFeedLikedByScreen} 167 + options={{title: title('Liked by')}} 168 + /> 169 + <Stack.Screen 148 170 name="Debug" 149 171 component={DebugScreen} 150 172 options={{title: title('Debug')}} ··· 184 206 component={AppPasswords} 185 207 options={{title: title('App Passwords')}} 186 208 /> 209 + <Stack.Screen 210 + name="SavedFeeds" 211 + component={SavedFeeds} 212 + options={{title: title('Edit My Feeds')}} 213 + /> 187 214 </> 188 215 ) 189 216 } ··· 201 228 screenOptions={{headerShown: false}} 202 229 tabBar={tabBar}> 203 230 <Tab.Screen name="HomeTab" component={HomeTabNavigator} /> 231 + <Tab.Screen name="SearchTab" component={SearchTabNavigator} /> 232 + <Tab.Screen name="FeedsTab" component={FeedsTabNavigator} /> 204 233 <Tab.Screen 205 234 name="NotificationsTab" 206 235 component={NotificationsTabNavigator} 207 236 /> 208 - <Tab.Screen name="SearchTab" component={SearchTabNavigator} /> 209 237 <Tab.Screen name="MyProfileTab" component={MyProfileTabNavigator} /> 210 238 </Tab.Navigator> 211 239 ) ··· 245 273 ) 246 274 } 247 275 276 + function FeedsTabNavigator() { 277 + const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) 278 + return ( 279 + <FeedsTab.Navigator 280 + screenOptions={{ 281 + gestureEnabled: true, 282 + fullScreenGestureEnabled: true, 283 + headerShown: false, 284 + animationDuration: 250, 285 + contentStyle, 286 + }}> 287 + <FeedsTab.Screen name="Feeds" component={FeedsScreen} /> 288 + {commonScreens(FeedsTab as typeof HomeTab)} 289 + </FeedsTab.Navigator> 290 + ) 291 + } 292 + 248 293 function NotificationsTabNavigator() { 249 294 const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) 250 295 return ( ··· 317 362 name="Search" 318 363 component={SearchScreen} 319 364 options={{title: title('Search')}} 365 + /> 366 + <Flat.Screen 367 + name="Feeds" 368 + component={FeedsScreen} 369 + options={{title: title('Feeds')}} 320 370 /> 321 371 <Flat.Screen 322 372 name="Notifications"
-3
src/lib/api/feed-manip.ts
··· 143 143 } 144 144 } 145 145 146 - // sort by slice roots' timestamps 147 - slices.sort((a, b) => b.ts.localeCompare(a.ts)) 148 - 149 146 for (const slice of slices) { 150 147 for (const item of slice.items) { 151 148 this.seenUris.add(item.post.uri)
+49 -44
src/lib/api/index.ts
··· 18 18 uri: string 19 19 isLoading: boolean 20 20 meta?: LinkMeta 21 + embed?: AppBskyEmbedRecord.Main 21 22 localThumb?: ImageModel 22 23 } 23 24 ··· 135 136 } 136 137 137 138 if (opts.extLink && !opts.images?.length) { 138 - let thumb 139 - if (opts.extLink.localThumb) { 140 - opts.onStateChange?.('Uploading link thumbnail...') 141 - let encoding 142 - if (opts.extLink.localThumb.mime) { 143 - encoding = opts.extLink.localThumb.mime 144 - } else if (opts.extLink.localThumb.path.endsWith('.png')) { 145 - encoding = 'image/png' 146 - } else if ( 147 - opts.extLink.localThumb.path.endsWith('.jpeg') || 148 - opts.extLink.localThumb.path.endsWith('.jpg') 149 - ) { 150 - encoding = 'image/jpeg' 151 - } else { 152 - store.log.warn( 153 - 'Unexpected image format for thumbnail, skipping', 154 - opts.extLink.localThumb.path, 155 - ) 156 - } 157 - if (encoding) { 158 - const thumbUploadRes = await uploadBlob( 159 - store, 160 - opts.extLink.localThumb.path, 161 - encoding, 162 - ) 163 - thumb = thumbUploadRes.data.blob 139 + if (opts.extLink.embed) { 140 + embed = opts.extLink.embed 141 + } else { 142 + let thumb 143 + if (opts.extLink.localThumb) { 144 + opts.onStateChange?.('Uploading link thumbnail...') 145 + let encoding 146 + if (opts.extLink.localThumb.mime) { 147 + encoding = opts.extLink.localThumb.mime 148 + } else if (opts.extLink.localThumb.path.endsWith('.png')) { 149 + encoding = 'image/png' 150 + } else if ( 151 + opts.extLink.localThumb.path.endsWith('.jpeg') || 152 + opts.extLink.localThumb.path.endsWith('.jpg') 153 + ) { 154 + encoding = 'image/jpeg' 155 + } else { 156 + store.log.warn( 157 + 'Unexpected image format for thumbnail, skipping', 158 + opts.extLink.localThumb.path, 159 + ) 160 + } 161 + if (encoding) { 162 + const thumbUploadRes = await uploadBlob( 163 + store, 164 + opts.extLink.localThumb.path, 165 + encoding, 166 + ) 167 + thumb = thumbUploadRes.data.blob 168 + } 164 169 } 165 - } 166 170 167 - if (opts.quote) { 168 - embed = { 169 - $type: 'app.bsky.embed.recordWithMedia', 170 - record: embed, 171 - media: { 171 + if (opts.quote) { 172 + embed = { 173 + $type: 'app.bsky.embed.recordWithMedia', 174 + record: embed, 175 + media: { 176 + $type: 'app.bsky.embed.external', 177 + external: { 178 + uri: opts.extLink.uri, 179 + title: opts.extLink.meta?.title || '', 180 + description: opts.extLink.meta?.description || '', 181 + thumb, 182 + }, 183 + } as AppBskyEmbedExternal.Main, 184 + } as AppBskyEmbedRecordWithMedia.Main 185 + } else { 186 + embed = { 172 187 $type: 'app.bsky.embed.external', 173 188 external: { 174 189 uri: opts.extLink.uri, ··· 176 191 description: opts.extLink.meta?.description || '', 177 192 thumb, 178 193 }, 179 - } as AppBskyEmbedExternal.Main, 180 - } as AppBskyEmbedRecordWithMedia.Main 181 - } else { 182 - embed = { 183 - $type: 'app.bsky.embed.external', 184 - external: { 185 - uri: opts.extLink.uri, 186 - title: opts.extLink.meta?.title || '', 187 - description: opts.extLink.meta?.description || '', 188 - thumb, 189 - }, 190 - } as AppBskyEmbedExternal.Main 194 + } as AppBskyEmbedExternal.Main 195 + } 191 196 } 192 197 } 193 198
+16
src/lib/async/revertible.ts
··· 4 4 5 5 const ongoingActions = new Set<any>() 6 6 7 + /** 8 + * This is a TypeScript function that optimistically updates data on the client-side before sending a 9 + * request to the server and rolling back changes if the request fails. 10 + * @param {T} model - The object or record that needs to be updated optimistically. 11 + * @param preUpdate - `preUpdate` is a function that is called before the server update is executed. It 12 + * can be used to perform any necessary actions or updates on the model or UI before the server update 13 + * is initiated. 14 + * @param serverUpdate - `serverUpdate` is a function that returns a Promise representing the server 15 + * update operation. This function is called after the previous state of the model has been recorded 16 + * and the `preUpdate` function has been executed. If the server update is successful, the `postUpdate` 17 + * function is called with the result 18 + * @param [postUpdate] - `postUpdate` is an optional callback function that will be called after the 19 + * server update is successful. It takes in the response from the server update as its parameter. If 20 + * this parameter is not provided, nothing will happen after the server update. 21 + * @returns A Promise that resolves to `void`. 22 + */ 7 23 export const updateDataOptimistically = async < 8 24 T extends Record<string, any>, 9 25 U,
+43
src/lib/constants.ts
··· 94 94 } 95 95 } 96 96 97 + export const STAGING_DEFAULT_FEED = (rkey: string) => 98 + `at://did:plc:wqzurwm3kmaig6e6hnc2gqwo/app.bsky.feed.generator/${rkey}` 99 + export const PROD_DEFAULT_FEED = (rkey: string) => 100 + `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}` 101 + export async function DEFAULT_FEEDS( 102 + serviceUrl: string, 103 + resolveHandle: (name: string) => Promise<string>, 104 + ) { 105 + if (serviceUrl.includes('localhost')) { 106 + // local dev 107 + const aliceDid = await resolveHandle('alice.test') 108 + return { 109 + pinned: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], 110 + saved: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`], 111 + } 112 + } else if (serviceUrl.includes('staging')) { 113 + // staging 114 + return { 115 + pinned: [STAGING_DEFAULT_FEED('whats-hot')], 116 + saved: [ 117 + STAGING_DEFAULT_FEED('bsky-team'), 118 + STAGING_DEFAULT_FEED('with-friends'), 119 + STAGING_DEFAULT_FEED('whats-hot'), 120 + STAGING_DEFAULT_FEED('hot-classic'), 121 + ], 122 + } 123 + } else { 124 + // production 125 + return { 126 + pinned: [ 127 + PROD_DEFAULT_FEED('whats-hot'), 128 + PROD_DEFAULT_FEED('with-friends'), 129 + ], 130 + saved: [ 131 + PROD_DEFAULT_FEED('bsky-team'), 132 + PROD_DEFAULT_FEED('with-friends'), 133 + PROD_DEFAULT_FEED('whats-hot'), 134 + PROD_DEFAULT_FEED('hot-classic'), 135 + ], 136 + } 137 + } 138 + } 139 + 97 140 export const POST_IMG_MAX = { 98 141 width: 2000, 99 142 height: 2000,
+40
src/lib/haptics.ts
··· 1 + import {isIOS, isWeb} from 'platform/detection' 2 + import ReactNativeHapticFeedback, { 3 + HapticFeedbackTypes, 4 + } from 'react-native-haptic-feedback' 5 + 6 + const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537s 7 + 8 + export class Haptics { 9 + static default() { 10 + if (isWeb) { 11 + return 12 + } 13 + ReactNativeHapticFeedback.trigger(hapticImpact) 14 + } 15 + static impact(type: HapticFeedbackTypes = hapticImpact) { 16 + if (isWeb) { 17 + return 18 + } 19 + ReactNativeHapticFeedback.trigger(type) 20 + } 21 + static selection() { 22 + if (isWeb) { 23 + return 24 + } 25 + ReactNativeHapticFeedback.trigger('selection') 26 + } 27 + static notification = (type: 'success' | 'warning' | 'error') => { 28 + if (isWeb) { 29 + return 30 + } 31 + switch (type) { 32 + case 'success': 33 + return ReactNativeHapticFeedback.trigger('notificationSuccess') 34 + case 'warning': 35 + return ReactNativeHapticFeedback.trigger('notificationWarning') 36 + case 'error': 37 + return ReactNativeHapticFeedback.trigger('notificationError') 38 + } 39 + } 40 + }
+27
src/lib/hooks/useCustomFeed.ts
··· 1 + import {useEffect, useState} from 'react' 2 + import {useStores} from 'state/index' 3 + import {CustomFeedModel} from 'state/models/feeds/custom-feed' 4 + 5 + export function useCustomFeed(uri: string): CustomFeedModel | undefined { 6 + const store = useStores() 7 + const [item, setItem] = useState<CustomFeedModel | undefined>() 8 + useEffect(() => { 9 + async function fetchView() { 10 + const res = await store.agent.app.bsky.feed.getFeedGenerator({ 11 + feed: uri, 12 + }) 13 + const view = res.data.view 14 + return view 15 + } 16 + async function buildFeedItem() { 17 + const view = await fetchView() 18 + if (view) { 19 + const temp = new CustomFeedModel(store, view) 20 + setItem(temp) 21 + } 22 + } 23 + buildFeedItem() 24 + }, [store, uri]) 25 + 26 + return item 27 + }
+84
src/lib/hooks/useDraggableScrollView.ts
··· 1 + import {useEffect, useRef, useMemo, ForwardedRef} from 'react' 2 + import {Platform, findNodeHandle} from 'react-native' 3 + import type {ScrollView} from 'react-native' 4 + import {mergeRefs} from 'lib/merge-refs' 5 + 6 + type Props<Scrollable extends ScrollView = ScrollView> = { 7 + cursor?: string 8 + outerRef?: ForwardedRef<Scrollable> 9 + } 10 + 11 + export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({ 12 + outerRef, 13 + cursor = 'grab', 14 + }: Props<Scrollable> = {}) { 15 + const ref = useRef<Scrollable>(null) 16 + 17 + useEffect(() => { 18 + if (Platform.OS !== 'web' || !ref.current) { 19 + return 20 + } 21 + const slider = findNodeHandle(ref.current) as unknown as HTMLDivElement 22 + if (!slider) { 23 + return 24 + } 25 + let isDragging = false 26 + let isMouseDown = false 27 + let startX = 0 28 + let scrollLeft = 0 29 + 30 + const mouseDown = (e: MouseEvent) => { 31 + isMouseDown = true 32 + startX = e.pageX - slider.offsetLeft 33 + scrollLeft = slider.scrollLeft 34 + 35 + slider.style.cursor = cursor 36 + } 37 + 38 + const mouseUp = () => { 39 + if (isDragging) { 40 + slider.addEventListener('click', e => e.stopPropagation(), {once: true}) 41 + } 42 + 43 + isMouseDown = false 44 + isDragging = false 45 + slider.style.cursor = 'default' 46 + } 47 + 48 + const mouseMove = (e: MouseEvent) => { 49 + if (!isMouseDown) { 50 + return 51 + } 52 + 53 + // Require n pixels momement before start of drag (3 in this case ) 54 + const x = e.pageX - slider.offsetLeft 55 + if (Math.abs(x - startX) < 3) { 56 + return 57 + } 58 + 59 + isDragging = true 60 + e.preventDefault() 61 + const walk = x - startX 62 + slider.scrollLeft = scrollLeft - walk 63 + } 64 + 65 + slider.addEventListener('mousedown', mouseDown) 66 + window.addEventListener('mouseup', mouseUp) 67 + window.addEventListener('mousemove', mouseMove) 68 + 69 + return () => { 70 + slider.removeEventListener('mousedown', mouseDown) 71 + window.removeEventListener('mouseup', mouseUp) 72 + window.removeEventListener('mousemove', mouseMove) 73 + } 74 + }, [cursor]) 75 + 76 + const refs = useMemo( 77 + () => mergeRefs(outerRef ? [ref, outerRef] : [ref]), 78 + [ref, outerRef], 79 + ) 80 + 81 + return { 82 + refs, 83 + } 84 + }
+3 -1
src/lib/hooks/useNavigationTabState.ts
··· 6 6 const res = { 7 7 isAtHome: getTabState(state, 'Home') !== TabState.Outside, 8 8 isAtSearch: getTabState(state, 'Search') !== TabState.Outside, 9 + isAtFeeds: getTabState(state, 'Feeds') !== TabState.Outside, 9 10 isAtNotifications: 10 11 getTabState(state, 'Notifications') !== TabState.Outside, 11 12 isAtMyProfile: getTabState(state, 'MyProfile') !== TabState.Outside, 12 13 } 13 14 if ( 14 15 !res.isAtHome && 16 + !res.isAtSearch && 17 + !res.isAtFeeds && 15 18 !res.isAtNotifications && 16 - !res.isAtSearch && 17 19 !res.isAtMyProfile 18 20 ) { 19 21 // HACK for some reason useNavigationState will give us pre-hydration results
+46 -15
src/lib/hooks/useOnMainScroll.ts
··· 1 - import {useState} from 'react' 1 + import {useState, useCallback, useRef} from 'react' 2 2 import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native' 3 3 import {RootStoreModel} from 'state/index' 4 + import {s} from 'lib/styles' 5 + import {isDesktopWeb} from 'platform/detection' 6 + 7 + const DY_LIMIT = isDesktopWeb ? 30 : 10 4 8 5 9 export type OnScrollCb = ( 6 10 event: NativeSyntheticEvent<NativeScrollEvent>, 7 11 ) => void 12 + export type ResetCb = () => void 8 13 9 - export function useOnMainScroll(store: RootStoreModel) { 10 - let [lastY, setLastY] = useState(0) 11 - let isMinimal = store.shell.minimalShellMode 12 - return function onMainScroll(event: NativeSyntheticEvent<NativeScrollEvent>) { 13 - const y = event.nativeEvent.contentOffset.y 14 - const dy = y - (lastY || 0) 15 - setLastY(y) 14 + export function useOnMainScroll( 15 + store: RootStoreModel, 16 + ): [OnScrollCb, boolean, ResetCb] { 17 + let lastY = useRef(0) 18 + let [isScrolledDown, setIsScrolledDown] = useState(false) 19 + return [ 20 + useCallback( 21 + (event: NativeSyntheticEvent<NativeScrollEvent>) => { 22 + const y = event.nativeEvent.contentOffset.y 23 + const dy = y - (lastY.current || 0) 24 + lastY.current = y 25 + 26 + if (!store.shell.minimalShellMode && y > 10 && dy > DY_LIMIT) { 27 + store.shell.setMinimalShellMode(true) 28 + } else if ( 29 + store.shell.minimalShellMode && 30 + (y <= 10 || dy < DY_LIMIT * -1) 31 + ) { 32 + store.shell.setMinimalShellMode(false) 33 + } 16 34 17 - if (!isMinimal && y > 10 && dy > 10) { 18 - store.shell.setMinimalShellMode(true) 19 - isMinimal = true 20 - } else if (isMinimal && (y <= 10 || dy < -10)) { 35 + if ( 36 + !isScrolledDown && 37 + event.nativeEvent.contentOffset.y > s.window.height 38 + ) { 39 + setIsScrolledDown(true) 40 + } else if ( 41 + isScrolledDown && 42 + event.nativeEvent.contentOffset.y < s.window.height 43 + ) { 44 + setIsScrolledDown(false) 45 + } 46 + }, 47 + [store, isScrolledDown], 48 + ), 49 + isScrolledDown, 50 + useCallback(() => { 51 + setIsScrolledDown(false) 21 52 store.shell.setMinimalShellMode(false) 22 - isMinimal = false 23 - } 24 - } 53 + lastY.current = 1e8 // NOTE we set this very high so that the onScroll logic works right -prf 54 + }, [store, setIsScrolledDown]), 55 + ] 25 56 }
+77 -3
src/lib/icons.tsx
··· 1 1 import React from 'react' 2 2 import {StyleProp, TextStyle, ViewStyle} from 'react-native' 3 - import Svg, {Path, Rect, Line, Ellipse} from 'react-native-svg' 3 + import Svg, {Path, Rect, Line, Ellipse, Circle} from 'react-native-svg' 4 4 5 5 export function GridIcon({ 6 6 style, ··· 472 472 size = 24, 473 473 strokeWidth = 1.5, 474 474 }: { 475 - style?: StyleProp<ViewStyle> 475 + style?: StyleProp<TextStyle> 476 476 size?: string | number 477 477 strokeWidth: number 478 478 }) { ··· 493 493 style, 494 494 size = 24, 495 495 }: { 496 - style?: StyleProp<ViewStyle> 496 + style?: StyleProp<TextStyle> 497 497 size?: string | number 498 498 }) { 499 499 return ( ··· 883 883 </Svg> 884 884 ) 885 885 } 886 + 887 + export function SatelliteDishIconSolid({ 888 + style, 889 + size, 890 + strokeWidth = 1.5, 891 + }: { 892 + style?: StyleProp<ViewStyle> 893 + size?: string | number 894 + strokeWidth?: number 895 + }) { 896 + return ( 897 + <Svg 898 + width={size || 24} 899 + height={size || 24} 900 + viewBox="0 0 22 22" 901 + style={style} 902 + fill="none" 903 + stroke="none"> 904 + <Path 905 + d="M16 19.6622C14.5291 20.513 12.8214 21 11 21C5.47715 21 1 16.5229 1 11C1 9.17858 1.48697 7.47088 2.33782 6.00002C3.18867 4.52915 6 7.66219 6 7.66219L14.5 16.1622C14.5 16.1622 17.4709 18.8113 16 19.6622Z" 906 + fill="currentColor" 907 + /> 908 + <Path 909 + d="M8 1.62961C9.04899 1.22255 10.1847 1 11.3704 1C16.6887 1 21 5.47715 21 11C21 12.0452 20.8456 13.053 20.5592 14" 910 + stroke="currentColor" 911 + strokeWidth={strokeWidth} 912 + strokeLinecap="round" 913 + /> 914 + <Path 915 + d="M9 5.38745C9.64553 5.13695 10.3444 5 11.0741 5C14.3469 5 17 7.75517 17 11.1538C17 11.797 16.905 12.4172 16.7287 13" 916 + stroke="currentColor" 917 + strokeWidth={strokeWidth} 918 + strokeLinecap="round" 919 + /> 920 + <Circle cx="10" cy="12" r="2" fill="currentColor" /> 921 + </Svg> 922 + ) 923 + } 924 + 925 + export function SatelliteDishIcon({ 926 + style, 927 + size, 928 + strokeWidth = 1.5, 929 + }: { 930 + style?: StyleProp<TextStyle> 931 + size?: string | number 932 + strokeWidth?: number 933 + }) { 934 + return ( 935 + <Svg 936 + fill="none" 937 + viewBox="0 0 22 22" 938 + strokeWidth={strokeWidth} 939 + stroke="currentColor" 940 + width={size} 941 + height={size} 942 + style={style}> 943 + <Path d="M5.25593 8.3303L5.25609 8.33047L5.25616 8.33056L5.25621 8.33061L5.27377 8.35018L5.29289 8.3693L13.7929 16.8693L13.8131 16.8895L13.8338 16.908L13.834 16.9081L13.8342 16.9083L13.8342 16.9083L13.8345 16.9086L13.8381 16.9118L13.8574 16.9294C13.8752 16.9458 13.9026 16.9711 13.9377 17.0043C14.0081 17.0708 14.1088 17.1683 14.2258 17.2881C14.4635 17.5315 14.7526 17.8509 14.9928 18.1812C15.2067 18.4755 15.3299 18.7087 15.3817 18.8634C14.0859 19.5872 12.5926 20 11 20C6.02944 20 2 15.9706 2 11C2 9.4151 2.40883 7.9285 3.12619 6.63699C3.304 6.69748 3.56745 6.84213 3.89275 7.08309C4.24679 7.34534 4.58866 7.65673 4.84827 7.9106C4.97633 8.03583 5.08062 8.14337 5.152 8.21863C5.18763 8.25619 5.21487 8.28551 5.23257 8.30473L5.25178 8.32572L5.25571 8.33006L5.25593 8.3303ZM3.00217 6.60712C3.00217 6.6071 3.00267 6.6071 3.00372 6.60715C3.00271 6.60716 3.00218 6.60714 3.00217 6.60712Z" /> 944 + <Path 945 + d="M8 1.62961C9.04899 1.22255 10.1847 1 11.3704 1C16.6887 1 21 5.47715 21 11C21 12.0452 20.8456 13.053 20.5592 14" 946 + stroke-linecap="round" 947 + /> 948 + <Path 949 + d="M9 5.38745C9.64553 5.13695 10.3444 5 11.0741 5C14.3469 5 17 7.75517 17 11.1538C17 11.797 16.905 12.4172 16.7287 13" 950 + stroke-linecap="round" 951 + /> 952 + <Path 953 + d="M12 12C12 12.7403 11.5978 13.3866 11 13.7324L8.26756 11C8.61337 10.4022 9.25972 10 10 10C11.1046 10 12 10.8954 12 12Z" 954 + fill="currentColor" 955 + stroke="none" 956 + /> 957 + </Svg> 958 + ) 959 + }
+27
src/lib/merge-refs.ts
··· 1 + /** 2 + * This TypeScript function merges multiple React refs into a single ref callback. 3 + * When developing low level UI components, it is common to have to use a local ref 4 + * but also support an external one using React.forwardRef. 5 + * Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility. 6 + * Today a ref can be a function or an object, tomorrow it could be another thing, who knows. 7 + * This utility handles compatibility for you. 8 + * This function is inspired by https://github.com/gregberge/react-merge-refs 9 + * @param refs - An array of React refs, which can be either `React.MutableRefObject<T>` or 10 + * `React.LegacyRef<T>`. These refs are used to store references to DOM elements or React components. 11 + * The `mergeRefs` function takes in an array of these refs and returns a callback function that 12 + * @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and 13 + * returns a ref callback function that can be used to merge multiple refs into a single ref. 14 + */ 15 + export function mergeRefs<T = any>( 16 + refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>, 17 + ): React.RefCallback<T> { 18 + return value => { 19 + refs.forEach(ref => { 20 + if (typeof ref === 'function') { 21 + ref(value) 22 + } else if (ref != null) { 23 + ;(ref as React.MutableRefObject<T | null>).current = value 24 + } 25 + }) 26 + } 27 + }
+12
src/lib/routes/types.ts
··· 9 9 ModerationMuteLists: undefined 10 10 ModerationMutedAccounts: undefined 11 11 ModerationBlockedAccounts: undefined 12 + DiscoverFeeds: undefined 12 13 Settings: undefined 13 14 Profile: {name: string; hideBackButton?: boolean} 14 15 ProfileFollowers: {name: string} ··· 17 18 PostThread: {name: string; rkey: string} 18 19 PostLikedBy: {name: string; rkey: string} 19 20 PostRepostedBy: {name: string; rkey: string} 21 + CustomFeed: {name: string; rkey: string} 22 + CustomFeedLikedBy: {name: string; rkey: string} 20 23 Debug: undefined 21 24 Log: undefined 22 25 Support: undefined ··· 25 28 CommunityGuidelines: undefined 26 29 CopyrightPolicy: undefined 27 30 AppPasswords: undefined 31 + SavedFeeds: undefined 28 32 } 29 33 30 34 export type BottomTabNavigatorParams = CommonNavigatorParams & { 31 35 HomeTab: undefined 32 36 SearchTab: undefined 37 + FeedsTab: undefined 33 38 NotificationsTab: undefined 34 39 MyProfileTab: undefined 35 40 } ··· 42 47 Search: {q?: string} 43 48 } 44 49 50 + export type FeedsTabNavigatorParams = CommonNavigatorParams & { 51 + Feeds: undefined 52 + } 53 + 45 54 export type NotificationsTabNavigatorParams = CommonNavigatorParams & { 46 55 Notifications: undefined 47 56 } ··· 53 62 export type FlatNavigatorParams = CommonNavigatorParams & { 54 63 Home: undefined 55 64 Search: {q?: string} 65 + Feeds: undefined 56 66 Notifications: undefined 57 67 } 58 68 ··· 61 71 Home: undefined 62 72 SearchTab: undefined 63 73 Search: {q?: string} 74 + FeedsTab: undefined 75 + Feeds: undefined 64 76 NotificationsTab: undefined 65 77 Notifications: undefined 66 78 MyProfileTab: undefined
+12
src/lib/strings/url-helpers.ts
··· 82 82 return false 83 83 } 84 84 85 + export function isBskyCustomFeedUrl(url: string): boolean { 86 + if (isBskyAppUrl(url)) { 87 + try { 88 + const urlp = new URL(url) 89 + return /profile\/(?<name>[^/]+)\/feed\/(?<rkey>[^/]+)/i.test( 90 + urlp.pathname, 91 + ) 92 + } catch {} 93 + } 94 + return false 95 + } 96 + 85 97 export function convertBskyAppUrlIfNeeded(url: string): string { 86 98 if (isBskyAppUrl(url)) { 87 99 try {
+8 -1
src/lib/styles.ts
··· 1 - import {StyleProp, StyleSheet, TextStyle} from 'react-native' 1 + import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native' 2 2 import {Theme, TypographyVariant} from './ThemeContext' 3 3 import {isMobileWeb} from 'platform/detection' 4 4 ··· 52 52 green5: '#082b03', 53 53 54 54 unreadNotifBg: '#ebf6ff', 55 + brandBlue: '#0066FF', 55 56 } 56 57 57 58 export const gradients = { ··· 169 170 w100pct: {width: '100%'}, 170 171 h100pct: {height: '100%'}, 171 172 hContentRegion: isMobileWeb ? {flex: 1} : {height: '100%'}, 173 + window: { 174 + width: Dimensions.get('window').width, 175 + height: Dimensions.get('window').height, 176 + }, 172 177 173 178 // text align 174 179 textLeft: {textAlign: 'left'}, ··· 214 219 green3: {color: colors.green3}, 215 220 green4: {color: colors.green4}, 216 221 green5: {color: colors.green5}, 222 + 223 + brandBlue: {color: colors.brandBlue}, 217 224 }) 218 225 219 226 export function lh(
+5
src/routes.ts
··· 3 3 export const router = new Router({ 4 4 Home: '/', 5 5 Search: '/search', 6 + Feeds: '/feeds', 7 + DiscoverFeeds: '/search/feeds', 6 8 Notifications: '/notifications', 7 9 Settings: '/settings', 8 10 Moderation: '/moderation', ··· 16 18 PostThread: '/profile/:name/post/:rkey', 17 19 PostLikedBy: '/profile/:name/post/:rkey/liked-by', 18 20 PostRepostedBy: '/profile/:name/post/:rkey/reposted-by', 21 + CustomFeed: '/profile/:name/feed/:rkey', 22 + CustomFeedLikedBy: '/profile/:name/feed/:rkey/liked-by', 19 23 Debug: '/sys/debug', 20 24 Log: '/sys/log', 21 25 AppPasswords: '/settings/app-passwords', 26 + SavedFeeds: '/settings/saved-feeds', 22 27 Support: '/support', 23 28 PrivacyPolicy: '/support/privacy', 24 29 TermsOfService: '/support/tos',
+97
src/state/models/discovery/feeds.ts
··· 1 + import {makeAutoObservable} from 'mobx' 2 + import {AppBskyUnspeccedGetPopularFeedGenerators} from '@atproto/api' 3 + import {RootStoreModel} from '../root-store' 4 + import {bundleAsync} from 'lib/async/bundle' 5 + import {cleanError} from 'lib/strings/errors' 6 + import {CustomFeedModel} from '../feeds/custom-feed' 7 + 8 + export class FeedsDiscoveryModel { 9 + // state 10 + isLoading = false 11 + isRefreshing = false 12 + hasLoaded = false 13 + error = '' 14 + 15 + // data 16 + feeds: CustomFeedModel[] = [] 17 + 18 + constructor(public rootStore: RootStoreModel) { 19 + makeAutoObservable( 20 + this, 21 + { 22 + rootStore: false, 23 + }, 24 + {autoBind: true}, 25 + ) 26 + } 27 + 28 + get hasMore() { 29 + return false 30 + } 31 + 32 + get hasContent() { 33 + return this.feeds.length > 0 34 + } 35 + 36 + get hasError() { 37 + return this.error !== '' 38 + } 39 + 40 + get isEmpty() { 41 + return this.hasLoaded && !this.hasContent 42 + } 43 + 44 + // public api 45 + // = 46 + 47 + refresh = bundleAsync(async () => { 48 + this._xLoading() 49 + try { 50 + const res = 51 + await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators( 52 + {}, 53 + ) 54 + this._replaceAll(res) 55 + this._xIdle() 56 + } catch (e: any) { 57 + this._xIdle(e) 58 + } 59 + }) 60 + 61 + clear() { 62 + this.isLoading = false 63 + this.isRefreshing = false 64 + this.hasLoaded = false 65 + this.error = '' 66 + this.feeds = [] 67 + } 68 + 69 + // state transitions 70 + // = 71 + 72 + _xLoading() { 73 + this.isLoading = true 74 + this.isRefreshing = true 75 + this.error = '' 76 + } 77 + 78 + _xIdle(err?: any) { 79 + this.isLoading = false 80 + this.isRefreshing = false 81 + this.hasLoaded = true 82 + this.error = cleanError(err) 83 + if (err) { 84 + this.rootStore.log.error('Failed to fetch popular feeds', err) 85 + } 86 + } 87 + 88 + // helper functions 89 + // = 90 + 91 + _replaceAll(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) { 92 + this.feeds = [] 93 + for (const f of res.data.feeds) { 94 + this.feeds.push(new CustomFeedModel(this.rootStore, f)) 95 + } 96 + } 97 + }
+120
src/state/models/feeds/custom-feed.ts
··· 1 + import {AppBskyFeedDefs} from '@atproto/api' 2 + import {makeAutoObservable, runInAction} from 'mobx' 3 + import {RootStoreModel} from 'state/models/root-store' 4 + import {sanitizeDisplayName} from 'lib/strings/display-names' 5 + import {updateDataOptimistically} from 'lib/async/revertible' 6 + 7 + export class CustomFeedModel { 8 + // data 9 + _reactKey: string 10 + data: AppBskyFeedDefs.GeneratorView 11 + isOnline: boolean 12 + isValid: boolean 13 + 14 + constructor( 15 + public rootStore: RootStoreModel, 16 + view: AppBskyFeedDefs.GeneratorView, 17 + isOnline?: boolean, 18 + isValid?: boolean, 19 + ) { 20 + this._reactKey = view.uri 21 + this.data = view 22 + this.isOnline = isOnline ?? true 23 + this.isValid = isValid ?? true 24 + makeAutoObservable( 25 + this, 26 + { 27 + rootStore: false, 28 + }, 29 + {autoBind: true}, 30 + ) 31 + } 32 + 33 + // local actions 34 + // = 35 + 36 + get uri() { 37 + return this.data.uri 38 + } 39 + 40 + get displayName() { 41 + if (this.data.displayName) { 42 + return sanitizeDisplayName(this.data.displayName) 43 + } 44 + return `Feed by @${this.data.creator.handle}` 45 + } 46 + 47 + get isSaved() { 48 + return this.rootStore.preferences.savedFeeds.includes(this.uri) 49 + } 50 + 51 + get isLiked() { 52 + return this.data.viewer?.like 53 + } 54 + 55 + // public apis 56 + // = 57 + 58 + async save() { 59 + await this.rootStore.preferences.addSavedFeed(this.uri) 60 + } 61 + 62 + async unsave() { 63 + await this.rootStore.preferences.removeSavedFeed(this.uri) 64 + } 65 + 66 + async like() { 67 + try { 68 + await updateDataOptimistically( 69 + this.data, 70 + () => { 71 + this.data.viewer = this.data.viewer || {} 72 + this.data.viewer.like = 'pending' 73 + this.data.likeCount = (this.data.likeCount || 0) + 1 74 + }, 75 + () => this.rootStore.agent.like(this.data.uri, this.data.cid), 76 + res => { 77 + this.data.viewer = this.data.viewer || {} 78 + this.data.viewer.like = res.uri 79 + }, 80 + ) 81 + } catch (e: any) { 82 + this.rootStore.log.error('Failed to like feed', e) 83 + } 84 + } 85 + 86 + async unlike() { 87 + if (!this.data.viewer?.like) { 88 + return 89 + } 90 + try { 91 + const likeUri = this.data.viewer.like 92 + await updateDataOptimistically( 93 + this.data, 94 + () => { 95 + this.data.viewer = this.data.viewer || {} 96 + this.data.viewer.like = undefined 97 + this.data.likeCount = (this.data.likeCount || 1) - 1 98 + }, 99 + () => this.rootStore.agent.deleteLike(likeUri), 100 + ) 101 + } catch (e: any) { 102 + this.rootStore.log.error('Failed to unlike feed', e) 103 + } 104 + } 105 + 106 + async reload() { 107 + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ 108 + feed: this.data.uri, 109 + }) 110 + runInAction(() => { 111 + this.data = res.data.view 112 + this.isOnline = res.data.isOnline 113 + this.isValid = res.data.isValid 114 + }) 115 + } 116 + 117 + serialize() { 118 + return JSON.stringify(this.data) 119 + } 120 + }
+216
src/state/models/feeds/multi-feed.ts
··· 1 + import {makeAutoObservable, runInAction} from 'mobx' 2 + import {AtUri} from '@atproto/api' 3 + import {bundleAsync} from 'lib/async/bundle' 4 + import {RootStoreModel} from '../root-store' 5 + import {CustomFeedModel} from './custom-feed' 6 + import {PostsFeedModel} from './posts' 7 + import {PostsFeedSliceModel} from './post' 8 + 9 + const FEED_PAGE_SIZE = 5 10 + const FEEDS_PAGE_SIZE = 3 11 + 12 + export type MultiFeedItem = 13 + | { 14 + _reactKey: string 15 + type: 'header' 16 + } 17 + | { 18 + _reactKey: string 19 + type: 'feed-header' 20 + avatar: string | undefined 21 + title: string 22 + } 23 + | { 24 + _reactKey: string 25 + type: 'feed-slice' 26 + slice: PostsFeedSliceModel 27 + } 28 + | { 29 + _reactKey: string 30 + type: 'feed-loading' 31 + } 32 + | { 33 + _reactKey: string 34 + type: 'feed-error' 35 + error: string 36 + } 37 + | { 38 + _reactKey: string 39 + type: 'feed-footer' 40 + title: string 41 + uri: string 42 + } 43 + | { 44 + _reactKey: string 45 + type: 'footer' 46 + } 47 + 48 + export class PostsMultiFeedModel { 49 + // state 50 + isLoading = false 51 + isRefreshing = false 52 + hasLoaded = false 53 + hasMore = true 54 + 55 + // data 56 + feedInfos: CustomFeedModel[] = [] 57 + feeds: PostsFeedModel[] = [] 58 + 59 + constructor(public rootStore: RootStoreModel) { 60 + makeAutoObservable(this, {rootStore: false}, {autoBind: true}) 61 + } 62 + 63 + get hasContent() { 64 + return this.feeds.length !== 0 65 + } 66 + 67 + get isEmpty() { 68 + return this.hasLoaded && !this.hasContent 69 + } 70 + 71 + get items() { 72 + const items: MultiFeedItem[] = [{_reactKey: '__header__', type: 'header'}] 73 + for (let i = 0; i < this.feedInfos.length; i++) { 74 + if (!this.feeds[i]) { 75 + break 76 + } 77 + const feed = this.feeds[i] 78 + const feedInfo = this.feedInfos[i] 79 + const urip = new AtUri(feedInfo.uri) 80 + items.push({ 81 + _reactKey: `__feed_header_${i}__`, 82 + type: 'feed-header', 83 + avatar: feedInfo.data.avatar, 84 + title: feedInfo.displayName, 85 + }) 86 + if (feed.isLoading) { 87 + items.push({ 88 + _reactKey: `__feed_loading_${i}__`, 89 + type: 'feed-loading', 90 + }) 91 + } else if (feed.hasError) { 92 + items.push({ 93 + _reactKey: `__feed_error_${i}__`, 94 + type: 'feed-error', 95 + error: feed.error, 96 + }) 97 + } else { 98 + for (let j = 0; j < feed.slices.length; j++) { 99 + items.push({ 100 + _reactKey: `__feed_slice_${i}_${j}__`, 101 + type: 'feed-slice', 102 + slice: feed.slices[j], 103 + }) 104 + } 105 + } 106 + items.push({ 107 + _reactKey: `__feed_footer_${i}__`, 108 + type: 'feed-footer', 109 + title: feedInfo.displayName, 110 + uri: `/profile/${feedInfo.data.creator.did}/feed/${urip.rkey}`, 111 + }) 112 + } 113 + if (!this.hasMore) { 114 + items.push({_reactKey: '__footer__', type: 'footer'}) 115 + } 116 + return items 117 + } 118 + 119 + // public api 120 + // = 121 + 122 + /** 123 + * Nuke all data 124 + */ 125 + clear() { 126 + this.rootStore.log.debug('MultiFeedModel:clear') 127 + this.isLoading = false 128 + this.isRefreshing = false 129 + this.hasLoaded = false 130 + this.hasMore = true 131 + this.feeds = [] 132 + } 133 + 134 + /** 135 + * Register any event listeners. Returns a cleanup function. 136 + */ 137 + registerListeners() { 138 + const sub = this.rootStore.onPostDeleted(this.onPostDeleted.bind(this)) 139 + return () => sub.remove() 140 + } 141 + 142 + /** 143 + * Reset and load 144 + */ 145 + async refresh() { 146 + this.feedInfos = this.rootStore.me.savedFeeds.all.slice() // capture current feeds 147 + await this.loadMore(true) 148 + } 149 + 150 + /** 151 + * Load more posts to the end of the feed 152 + */ 153 + loadMore = bundleAsync(async (isRefreshing: boolean = false) => { 154 + if (!isRefreshing && !this.hasMore) { 155 + return 156 + } 157 + if (isRefreshing) { 158 + this.isRefreshing = true // set optimistically for UI 159 + this.feeds = [] 160 + } 161 + this._xLoading(isRefreshing) 162 + const start = this.feeds.length 163 + const newFeeds: PostsFeedModel[] = [] 164 + for ( 165 + let i = start; 166 + i < start + FEEDS_PAGE_SIZE && i < this.feedInfos.length; 167 + i++ 168 + ) { 169 + const feed = new PostsFeedModel(this.rootStore, 'custom', { 170 + feed: this.feedInfos[i].uri, 171 + }) 172 + feed.pageSize = FEED_PAGE_SIZE 173 + await feed.setup() 174 + newFeeds.push(feed) 175 + } 176 + runInAction(() => { 177 + this.feeds = this.feeds.concat(newFeeds) 178 + this.hasMore = this.feeds.length < this.feedInfos.length 179 + }) 180 + this._xIdle() 181 + }) 182 + 183 + /** 184 + * Attempt to load more again after a failure 185 + */ 186 + async retryLoadMore() { 187 + this.hasMore = true 188 + return this.loadMore() 189 + } 190 + 191 + /** 192 + * Removes posts from the feed upon deletion. 193 + */ 194 + onPostDeleted(uri: string) { 195 + for (const f of this.feeds) { 196 + f.onPostDeleted(uri) 197 + } 198 + } 199 + 200 + // state transitions 201 + // = 202 + 203 + _xLoading(isRefreshing = false) { 204 + this.isLoading = true 205 + this.isRefreshing = isRefreshing 206 + } 207 + 208 + _xIdle() { 209 + this.isLoading = false 210 + this.isRefreshing = false 211 + this.hasLoaded = true 212 + } 213 + 214 + // helper functions 215 + // = 216 + }
+3 -1
src/state/models/feeds/notifications.ts
··· 290 290 } 291 291 292 292 get hasNewLatest() { 293 - return this.queuedNotifications && this.queuedNotifications?.length > 0 293 + return Boolean( 294 + this.queuedNotifications && this.queuedNotifications?.length > 0, 295 + ) 294 296 } 295 297 296 298 get unreadCountLabel(): string {
+265
src/state/models/feeds/post.ts
··· 1 + import {makeAutoObservable} from 'mobx' 2 + import {AppBskyFeedDefs, AppBskyFeedPost, RichText} from '@atproto/api' 3 + import {RootStoreModel} from '../root-store' 4 + import {updateDataOptimistically} from 'lib/async/revertible' 5 + import {PostLabelInfo, PostModeration} from 'lib/labeling/types' 6 + import {FeedViewPostsSlice} from 'lib/api/feed-manip' 7 + import { 8 + getEmbedLabels, 9 + getEmbedMuted, 10 + getEmbedMutedByList, 11 + getEmbedBlocking, 12 + getEmbedBlockedBy, 13 + getPostModeration, 14 + filterAccountLabels, 15 + filterProfileLabels, 16 + mergePostModerations, 17 + } from 'lib/labeling/helpers' 18 + 19 + type FeedViewPost = AppBskyFeedDefs.FeedViewPost 20 + type ReasonRepost = AppBskyFeedDefs.ReasonRepost 21 + type PostView = AppBskyFeedDefs.PostView 22 + 23 + let _idCounter = 0 24 + 25 + export class PostsFeedItemModel { 26 + // ui state 27 + _reactKey: string = '' 28 + 29 + // data 30 + post: PostView 31 + postRecord?: AppBskyFeedPost.Record 32 + reply?: FeedViewPost['reply'] 33 + reason?: FeedViewPost['reason'] 34 + richText?: RichText 35 + 36 + constructor( 37 + public rootStore: RootStoreModel, 38 + reactKey: string, 39 + v: FeedViewPost, 40 + ) { 41 + this._reactKey = reactKey 42 + this.post = v.post 43 + if (AppBskyFeedPost.isRecord(this.post.record)) { 44 + const valid = AppBskyFeedPost.validateRecord(this.post.record) 45 + if (valid.success) { 46 + this.postRecord = this.post.record 47 + this.richText = new RichText(this.postRecord, {cleanNewlines: true}) 48 + } else { 49 + this.postRecord = undefined 50 + this.richText = undefined 51 + rootStore.log.warn( 52 + 'Received an invalid app.bsky.feed.post record', 53 + valid.error, 54 + ) 55 + } 56 + } else { 57 + this.postRecord = undefined 58 + this.richText = undefined 59 + rootStore.log.warn( 60 + 'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type', 61 + this.post.record, 62 + ) 63 + } 64 + this.reply = v.reply 65 + this.reason = v.reason 66 + makeAutoObservable(this, {rootStore: false}) 67 + } 68 + 69 + get rootUri(): string { 70 + if (this.reply?.root.uri) { 71 + return this.reply.root.uri 72 + } 73 + return this.post.uri 74 + } 75 + 76 + get isThreadMuted() { 77 + return this.rootStore.mutedThreads.uris.has(this.rootUri) 78 + } 79 + 80 + get labelInfo(): PostLabelInfo { 81 + return { 82 + postLabels: (this.post.labels || []).concat( 83 + getEmbedLabels(this.post.embed), 84 + ), 85 + accountLabels: filterAccountLabels(this.post.author.labels), 86 + profileLabels: filterProfileLabels(this.post.author.labels), 87 + isMuted: 88 + this.post.author.viewer?.muted || 89 + getEmbedMuted(this.post.embed) || 90 + false, 91 + mutedByList: 92 + this.post.author.viewer?.mutedByList || 93 + getEmbedMutedByList(this.post.embed), 94 + isBlocking: 95 + !!this.post.author.viewer?.blocking || 96 + getEmbedBlocking(this.post.embed) || 97 + false, 98 + isBlockedBy: 99 + !!this.post.author.viewer?.blockedBy || 100 + getEmbedBlockedBy(this.post.embed) || 101 + false, 102 + } 103 + } 104 + 105 + get moderation(): PostModeration { 106 + return getPostModeration(this.rootStore, this.labelInfo) 107 + } 108 + 109 + copy(v: FeedViewPost) { 110 + this.post = v.post 111 + this.reply = v.reply 112 + this.reason = v.reason 113 + } 114 + 115 + copyMetrics(v: FeedViewPost) { 116 + this.post.replyCount = v.post.replyCount 117 + this.post.repostCount = v.post.repostCount 118 + this.post.likeCount = v.post.likeCount 119 + this.post.viewer = v.post.viewer 120 + } 121 + 122 + get reasonRepost(): ReasonRepost | undefined { 123 + if (this.reason?.$type === 'app.bsky.feed.defs#reasonRepost') { 124 + return this.reason as ReasonRepost 125 + } 126 + } 127 + 128 + async toggleLike() { 129 + this.post.viewer = this.post.viewer || {} 130 + if (this.post.viewer.like) { 131 + const url = this.post.viewer.like 132 + await updateDataOptimistically( 133 + this.post, 134 + () => { 135 + this.post.likeCount = (this.post.likeCount || 0) - 1 136 + this.post.viewer!.like = undefined 137 + }, 138 + () => this.rootStore.agent.deleteLike(url), 139 + ) 140 + } else { 141 + await updateDataOptimistically( 142 + this.post, 143 + () => { 144 + this.post.likeCount = (this.post.likeCount || 0) + 1 145 + this.post.viewer!.like = 'pending' 146 + }, 147 + () => this.rootStore.agent.like(this.post.uri, this.post.cid), 148 + res => { 149 + this.post.viewer!.like = res.uri 150 + }, 151 + ) 152 + } 153 + } 154 + 155 + async toggleRepost() { 156 + this.post.viewer = this.post.viewer || {} 157 + if (this.post.viewer?.repost) { 158 + const url = this.post.viewer.repost 159 + await updateDataOptimistically( 160 + this.post, 161 + () => { 162 + this.post.repostCount = (this.post.repostCount || 0) - 1 163 + this.post.viewer!.repost = undefined 164 + }, 165 + () => this.rootStore.agent.deleteRepost(url), 166 + ) 167 + } else { 168 + await updateDataOptimistically( 169 + this.post, 170 + () => { 171 + this.post.repostCount = (this.post.repostCount || 0) + 1 172 + this.post.viewer!.repost = 'pending' 173 + }, 174 + () => this.rootStore.agent.repost(this.post.uri, this.post.cid), 175 + res => { 176 + this.post.viewer!.repost = res.uri 177 + }, 178 + ) 179 + } 180 + } 181 + 182 + async toggleThreadMute() { 183 + if (this.isThreadMuted) { 184 + this.rootStore.mutedThreads.uris.delete(this.rootUri) 185 + } else { 186 + this.rootStore.mutedThreads.uris.add(this.rootUri) 187 + } 188 + } 189 + 190 + async delete() { 191 + await this.rootStore.agent.deletePost(this.post.uri) 192 + this.rootStore.emitPostDeleted(this.post.uri) 193 + } 194 + } 195 + 196 + export class PostsFeedSliceModel { 197 + // ui state 198 + _reactKey: string = '' 199 + 200 + // data 201 + items: PostsFeedItemModel[] = [] 202 + 203 + constructor( 204 + public rootStore: RootStoreModel, 205 + reactKey: string, 206 + slice: FeedViewPostsSlice, 207 + ) { 208 + this._reactKey = reactKey 209 + for (const item of slice.items) { 210 + this.items.push( 211 + new PostsFeedItemModel(rootStore, `slice-${_idCounter++}`, item), 212 + ) 213 + } 214 + makeAutoObservable(this, {rootStore: false}) 215 + } 216 + 217 + get uri() { 218 + if (this.isReply) { 219 + return this.items[1].post.uri 220 + } 221 + return this.items[0].post.uri 222 + } 223 + 224 + get isThread() { 225 + return ( 226 + this.items.length > 1 && 227 + this.items.every( 228 + item => item.post.author.did === this.items[0].post.author.did, 229 + ) 230 + ) 231 + } 232 + 233 + get isReply() { 234 + return this.items.length > 1 && !this.isThread 235 + } 236 + 237 + get rootItem() { 238 + if (this.isReply) { 239 + return this.items[1] 240 + } 241 + return this.items[0] 242 + } 243 + 244 + get moderation() { 245 + return mergePostModerations(this.items.map(item => item.moderation)) 246 + } 247 + 248 + containsUri(uri: string) { 249 + return !!this.items.find(item => item.post.uri === uri) 250 + } 251 + 252 + isThreadParentAt(i: number) { 253 + if (this.items.length === 1) { 254 + return false 255 + } 256 + return i < this.items.length - 1 257 + } 258 + 259 + isThreadChildAt(i: number) { 260 + if (this.items.length === 1) { 261 + return false 262 + } 263 + return i > 0 264 + } 265 + }
+42 -321
src/state/models/feeds/posts.ts
··· 1 1 import {makeAutoObservable, runInAction} from 'mobx' 2 2 import { 3 3 AppBskyFeedGetTimeline as GetTimeline, 4 - AppBskyFeedDefs, 5 - AppBskyFeedPost, 6 4 AppBskyFeedGetAuthorFeed as GetAuthorFeed, 7 - RichText, 8 - jsonToLex, 5 + AppBskyFeedGetFeed as GetCustomFeed, 9 6 } from '@atproto/api' 10 7 import AwaitLock from 'await-lock' 11 8 import {bundleAsync} from 'lib/async/bundle' ··· 19 16 mergePosts, 20 17 } from 'lib/api/build-suggested-posts' 21 18 import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip' 22 - import {updateDataOptimistically} from 'lib/async/revertible' 23 - import {PostLabelInfo, PostModeration} from 'lib/labeling/types' 24 - import { 25 - getEmbedLabels, 26 - getEmbedMuted, 27 - getEmbedMutedByList, 28 - getEmbedBlocking, 29 - getEmbedBlockedBy, 30 - getPostModeration, 31 - mergePostModerations, 32 - filterAccountLabels, 33 - filterProfileLabels, 34 - } from 'lib/labeling/helpers' 35 - 36 - type FeedViewPost = AppBskyFeedDefs.FeedViewPost 37 - type ReasonRepost = AppBskyFeedDefs.ReasonRepost 38 - type PostView = AppBskyFeedDefs.PostView 19 + import {PostsFeedSliceModel} from './post' 39 20 40 21 const PAGE_SIZE = 30 41 22 let _idCounter = 0 42 23 43 - export class PostsFeedItemModel { 44 - // ui state 45 - _reactKey: string = '' 46 - 47 - // data 48 - post: PostView 49 - postRecord?: AppBskyFeedPost.Record 50 - reply?: FeedViewPost['reply'] 51 - reason?: FeedViewPost['reason'] 52 - richText?: RichText 53 - 54 - constructor( 55 - public rootStore: RootStoreModel, 56 - reactKey: string, 57 - v: FeedViewPost, 58 - ) { 59 - this._reactKey = reactKey 60 - this.post = v.post 61 - if (AppBskyFeedPost.isRecord(this.post.record)) { 62 - const valid = AppBskyFeedPost.validateRecord(this.post.record) 63 - if (valid.success) { 64 - this.postRecord = this.post.record 65 - this.richText = new RichText(this.postRecord, {cleanNewlines: true}) 66 - } else { 67 - this.postRecord = undefined 68 - this.richText = undefined 69 - rootStore.log.warn( 70 - 'Received an invalid app.bsky.feed.post record', 71 - valid.error, 72 - ) 73 - } 74 - } else { 75 - this.postRecord = undefined 76 - this.richText = undefined 77 - rootStore.log.warn( 78 - 'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type', 79 - this.post.record, 80 - ) 81 - } 82 - this.reply = v.reply 83 - this.reason = v.reason 84 - makeAutoObservable(this, {rootStore: false}) 85 - } 86 - 87 - get rootUri(): string { 88 - if (this.reply?.root.uri) { 89 - return this.reply.root.uri 90 - } 91 - return this.post.uri 92 - } 93 - 94 - get isThreadMuted() { 95 - return this.rootStore.mutedThreads.uris.has(this.rootUri) 96 - } 97 - 98 - get labelInfo(): PostLabelInfo { 99 - return { 100 - postLabels: (this.post.labels || []).concat( 101 - getEmbedLabels(this.post.embed), 102 - ), 103 - accountLabels: filterAccountLabels(this.post.author.labels), 104 - profileLabels: filterProfileLabels(this.post.author.labels), 105 - isMuted: 106 - this.post.author.viewer?.muted || 107 - getEmbedMuted(this.post.embed) || 108 - false, 109 - mutedByList: 110 - this.post.author.viewer?.mutedByList || 111 - getEmbedMutedByList(this.post.embed), 112 - isBlocking: 113 - !!this.post.author.viewer?.blocking || 114 - getEmbedBlocking(this.post.embed) || 115 - false, 116 - isBlockedBy: 117 - !!this.post.author.viewer?.blockedBy || 118 - getEmbedBlockedBy(this.post.embed) || 119 - false, 120 - } 121 - } 122 - 123 - get moderation(): PostModeration { 124 - return getPostModeration(this.rootStore, this.labelInfo) 125 - } 126 - 127 - copy(v: FeedViewPost) { 128 - this.post = v.post 129 - this.reply = v.reply 130 - this.reason = v.reason 131 - } 132 - 133 - copyMetrics(v: FeedViewPost) { 134 - this.post.replyCount = v.post.replyCount 135 - this.post.repostCount = v.post.repostCount 136 - this.post.likeCount = v.post.likeCount 137 - this.post.viewer = v.post.viewer 138 - } 139 - 140 - get reasonRepost(): ReasonRepost | undefined { 141 - if (this.reason?.$type === 'app.bsky.feed.defs#reasonRepost') { 142 - return this.reason as ReasonRepost 143 - } 144 - } 145 - 146 - async toggleLike() { 147 - this.post.viewer = this.post.viewer || {} 148 - if (this.post.viewer.like) { 149 - const url = this.post.viewer.like 150 - await updateDataOptimistically( 151 - this.post, 152 - () => { 153 - this.post.likeCount = (this.post.likeCount || 0) - 1 154 - this.post.viewer!.like = undefined 155 - }, 156 - () => this.rootStore.agent.deleteLike(url), 157 - ) 158 - } else { 159 - await updateDataOptimistically( 160 - this.post, 161 - () => { 162 - this.post.likeCount = (this.post.likeCount || 0) + 1 163 - this.post.viewer!.like = 'pending' 164 - }, 165 - () => this.rootStore.agent.like(this.post.uri, this.post.cid), 166 - res => { 167 - this.post.viewer!.like = res.uri 168 - }, 169 - ) 170 - } 171 - } 172 - 173 - async toggleRepost() { 174 - this.post.viewer = this.post.viewer || {} 175 - if (this.post.viewer?.repost) { 176 - const url = this.post.viewer.repost 177 - await updateDataOptimistically( 178 - this.post, 179 - () => { 180 - this.post.repostCount = (this.post.repostCount || 0) - 1 181 - this.post.viewer!.repost = undefined 182 - }, 183 - () => this.rootStore.agent.deleteRepost(url), 184 - ) 185 - } else { 186 - await updateDataOptimistically( 187 - this.post, 188 - () => { 189 - this.post.repostCount = (this.post.repostCount || 0) + 1 190 - this.post.viewer!.repost = 'pending' 191 - }, 192 - () => this.rootStore.agent.repost(this.post.uri, this.post.cid), 193 - res => { 194 - this.post.viewer!.repost = res.uri 195 - }, 196 - ) 197 - } 198 - } 199 - 200 - async toggleThreadMute() { 201 - if (this.isThreadMuted) { 202 - this.rootStore.mutedThreads.uris.delete(this.rootUri) 203 - } else { 204 - this.rootStore.mutedThreads.uris.add(this.rootUri) 205 - } 206 - } 207 - 208 - async delete() { 209 - await this.rootStore.agent.deletePost(this.post.uri) 210 - this.rootStore.emitPostDeleted(this.post.uri) 211 - } 212 - } 213 - 214 - export class PostsFeedSliceModel { 215 - // ui state 216 - _reactKey: string = '' 217 - 218 - // data 219 - items: PostsFeedItemModel[] = [] 220 - 221 - constructor( 222 - public rootStore: RootStoreModel, 223 - reactKey: string, 224 - slice: FeedViewPostsSlice, 225 - ) { 226 - this._reactKey = reactKey 227 - for (const item of slice.items) { 228 - this.items.push( 229 - new PostsFeedItemModel(rootStore, `item-${_idCounter++}`, item), 230 - ) 231 - } 232 - makeAutoObservable(this, {rootStore: false}) 233 - } 234 - 235 - get uri() { 236 - if (this.isReply) { 237 - return this.items[1].post.uri 238 - } 239 - return this.items[0].post.uri 240 - } 241 - 242 - get isThread() { 243 - return ( 244 - this.items.length > 1 && 245 - this.items.every( 246 - item => item.post.author.did === this.items[0].post.author.did, 247 - ) 248 - ) 249 - } 250 - 251 - get isReply() { 252 - return this.items.length > 1 && !this.isThread 253 - } 254 - 255 - get rootItem() { 256 - if (this.isReply) { 257 - return this.items[1] 258 - } 259 - return this.items[0] 260 - } 261 - 262 - get moderation() { 263 - return mergePostModerations(this.items.map(item => item.moderation)) 264 - } 265 - 266 - containsUri(uri: string) { 267 - return !!this.items.find(item => item.post.uri === uri) 268 - } 269 - 270 - isThreadParentAt(i: number) { 271 - if (this.items.length === 1) { 272 - return false 273 - } 274 - return i < this.items.length - 1 275 - } 276 - 277 - isThreadChildAt(i: number) { 278 - if (this.items.length === 1) { 279 - return false 280 - } 281 - return i > 0 282 - } 283 - } 284 - 285 24 export class PostsFeedModel { 286 25 // state 287 26 isLoading = false ··· 297 36 loadMoreCursor: string | undefined 298 37 pollCursor: string | undefined 299 38 tuner = new FeedTuner() 39 + pageSize = PAGE_SIZE 300 40 301 41 // used to linearize async modifications to state 302 42 lock = new AwaitLock() ··· 309 49 310 50 constructor( 311 51 public rootStore: RootStoreModel, 312 - public feedType: 'home' | 'author' | 'suggested' | 'goodstuff', 313 - params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams, 52 + public feedType: 'home' | 'author' | 'suggested' | 'custom', 53 + params: 54 + | GetTimeline.QueryParams 55 + | GetAuthorFeed.QueryParams 56 + | GetCustomFeed.QueryParams, 314 57 ) { 315 58 makeAutoObservable( 316 59 this, ··· 387 130 } 388 131 389 132 get feedTuners() { 390 - if (this.feedType === 'goodstuff') { 133 + if (this.feedType === 'custom') { 391 134 return [ 392 135 FeedTuner.dedupReposts, 393 - FeedTuner.likedRepliesOnly, 394 136 FeedTuner.preferredLangOnly( 395 137 this.rootStore.preferences.contentLanguages, 396 138 ), ··· 416 158 this.tuner.reset() 417 159 this._xLoading(isRefreshing) 418 160 try { 419 - const res = await this._getFeed({limit: PAGE_SIZE}) 161 + const res = await this._getFeed({limit: this.pageSize}) 420 162 await this._replaceAll(res) 421 163 this._xIdle() 422 164 } catch (e: any) { ··· 455 197 try { 456 198 const res = await this._getFeed({ 457 199 cursor: this.loadMoreCursor, 458 - limit: PAGE_SIZE, 200 + limit: this.pageSize, 459 201 }) 460 202 await this._appendAll(res) 461 203 this._xIdle() ··· 524 266 if (this.hasNewLatest || this.feedType === 'suggested') { 525 267 return 526 268 } 527 - const res = await this._getFeed({limit: PAGE_SIZE}) 269 + const res = await this._getFeed({limit: this.pageSize}) 528 270 const tuner = new FeedTuner() 529 271 const slices = tuner.tune(res.data.feed, this.feedTuners) 530 272 this.setHasNewLatest(slices[0]?.uri !== this.slices[0]?.uri) ··· 599 341 // helper functions 600 342 // = 601 343 602 - async _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) { 344 + async _replaceAll( 345 + res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response, 346 + ) { 603 347 this.pollCursor = res.data.feed[0]?.post.uri 604 348 return this._appendAll(res, true) 605 349 } 606 350 607 351 async _appendAll( 608 - res: GetTimeline.Response | GetAuthorFeed.Response, 352 + res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response, 609 353 replace = false, 610 354 ) { 611 355 this.loadMoreCursor = res.data.cursor ··· 644 388 }) 645 389 } 646 390 647 - _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) { 391 + _updateAll( 392 + res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response, 393 + ) { 648 394 for (const item of res.data.feed) { 649 395 const existingSlice = this.slices.find(slice => 650 396 slice.containsUri(item.post.uri), ··· 661 407 } 662 408 663 409 protected async _getFeed( 664 - params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams = {}, 665 - ): Promise<GetTimeline.Response | GetAuthorFeed.Response> { 410 + params: 411 + | GetTimeline.QueryParams 412 + | GetAuthorFeed.QueryParams 413 + | GetCustomFeed.QueryParams, 414 + ): Promise< 415 + GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response 416 + > { 666 417 params = Object.assign({}, this.params, params) 667 418 if (this.feedType === 'suggested') { 668 419 const responses = await getMultipleAuthorsPosts( ··· 684 435 } 685 436 } else if (this.feedType === 'home') { 686 437 return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams) 687 - } else if (this.feedType === 'goodstuff') { 688 - const res = await getGoodStuff( 689 - this.rootStore.session.currentSession?.accessJwt || '', 690 - params as GetTimeline.QueryParams, 438 + } else if (this.feedType === 'custom') { 439 + this.checkIfCustomFeedIsOnlineAndValid( 440 + params as GetCustomFeed.QueryParams, 691 441 ) 692 - res.data.feed = (res.data.feed || []).filter( 693 - item => !item.post.author.viewer?.muted, 442 + return this.rootStore.agent.app.bsky.feed.getFeed( 443 + params as GetCustomFeed.QueryParams, 694 444 ) 695 - return res 696 445 } else { 697 446 return this.rootStore.agent.getAuthorFeed( 698 447 params as GetAuthorFeed.QueryParams, 699 448 ) 700 449 } 701 450 } 702 - } 703 451 704 - // HACK 705 - // temporary off-spec route to get the good stuff 706 - // -prf 707 - async function getGoodStuff( 708 - accessJwt: string, 709 - params: GetTimeline.QueryParams, 710 - ): Promise<GetTimeline.Response> { 711 - const controller = new AbortController() 712 - const to = setTimeout(() => controller.abort(), 15e3) 713 - 714 - const uri = new URL('https://bsky.social/xrpc/app.bsky.unspecced.getPopular') 715 - let k: keyof GetTimeline.QueryParams 716 - for (k in params) { 717 - if (typeof params[k] !== 'undefined') { 718 - uri.searchParams.set(k, String(params[k])) 452 + private async checkIfCustomFeedIsOnlineAndValid( 453 + params: GetCustomFeed.QueryParams, 454 + ) { 455 + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({ 456 + feed: params.feed, 457 + }) 458 + if (!res.data.isOnline || !res.data.isValid) { 459 + runInAction(() => { 460 + this.error = 461 + 'This custom feed is not online or may be experiencing issues.' 462 + }) 719 463 } 720 - } 721 - 722 - const res = await fetch(String(uri), { 723 - method: 'get', 724 - headers: { 725 - accept: 'application/json', 726 - authorization: `Bearer ${accessJwt}`, 727 - }, 728 - signal: controller.signal, 729 - }) 730 - 731 - const resHeaders: Record<string, string> = {} 732 - res.headers.forEach((value: string, key: string) => { 733 - resHeaders[key] = value 734 - }) 735 - let resBody = await res.json() 736 - 737 - clearTimeout(to) 738 - 739 - return { 740 - success: res.status === 200, 741 - headers: resHeaders, 742 - data: jsonToLex(resBody), 743 464 } 744 465 }
+120
src/state/models/lists/actor-feeds.ts
··· 1 + import {makeAutoObservable} from 'mobx' 2 + import {AppBskyFeedGetActorFeeds as GetActorFeeds} from '@atproto/api' 3 + import {RootStoreModel} from '../root-store' 4 + import {bundleAsync} from 'lib/async/bundle' 5 + import {cleanError} from 'lib/strings/errors' 6 + import {CustomFeedModel} from '../feeds/custom-feed' 7 + 8 + const PAGE_SIZE = 30 9 + 10 + export class ActorFeedsModel { 11 + // state 12 + isLoading = false 13 + isRefreshing = false 14 + hasLoaded = false 15 + error = '' 16 + hasMore = true 17 + loadMoreCursor?: string 18 + 19 + // data 20 + feeds: CustomFeedModel[] = [] 21 + 22 + constructor( 23 + public rootStore: RootStoreModel, 24 + public params: GetActorFeeds.QueryParams, 25 + ) { 26 + makeAutoObservable( 27 + this, 28 + { 29 + rootStore: false, 30 + }, 31 + {autoBind: true}, 32 + ) 33 + } 34 + 35 + get hasContent() { 36 + return this.feeds.length > 0 37 + } 38 + 39 + get hasError() { 40 + return this.error !== '' 41 + } 42 + 43 + get isEmpty() { 44 + return this.hasLoaded && !this.hasContent 45 + } 46 + 47 + // public api 48 + // = 49 + 50 + async refresh() { 51 + return this.loadMore(true) 52 + } 53 + 54 + clear() { 55 + this.isLoading = false 56 + this.isRefreshing = false 57 + this.hasLoaded = false 58 + this.error = '' 59 + this.hasMore = true 60 + this.loadMoreCursor = undefined 61 + this.feeds = [] 62 + } 63 + 64 + loadMore = bundleAsync(async (replace: boolean = false) => { 65 + if (!replace && !this.hasMore) { 66 + return 67 + } 68 + this._xLoading(replace) 69 + try { 70 + const res = await this.rootStore.agent.app.bsky.feed.getActorFeeds({ 71 + actor: this.params.actor, 72 + limit: PAGE_SIZE, 73 + cursor: replace ? undefined : this.loadMoreCursor, 74 + }) 75 + if (replace) { 76 + this._replaceAll(res) 77 + } else { 78 + this._appendAll(res) 79 + } 80 + this._xIdle() 81 + } catch (e: any) { 82 + this._xIdle(e) 83 + } 84 + }) 85 + 86 + // state transitions 87 + // = 88 + 89 + _xLoading(isRefreshing = false) { 90 + this.isLoading = true 91 + this.isRefreshing = isRefreshing 92 + this.error = '' 93 + } 94 + 95 + _xIdle(err?: any) { 96 + this.isLoading = false 97 + this.isRefreshing = false 98 + this.hasLoaded = true 99 + this.error = cleanError(err) 100 + if (err) { 101 + this.rootStore.log.error('Failed to fetch user followers', err) 102 + } 103 + } 104 + 105 + // helper functions 106 + // = 107 + 108 + _replaceAll(res: GetActorFeeds.Response) { 109 + this.feeds = [] 110 + this._appendAll(res) 111 + } 112 + 113 + _appendAll(res: GetActorFeeds.Response) { 114 + this.loadMoreCursor = res.data.cursor 115 + this.hasMore = !!this.loadMoreCursor 116 + for (const f of res.data.feeds) { 117 + this.feeds.push(new CustomFeedModel(this.rootStore, f)) 118 + } 119 + } 120 + }
+16
src/state/models/log.ts
··· 27 27 28 28 export class LogModel { 29 29 entries: LogEntry[] = [] 30 + timers = new Map<string, number>() 30 31 31 32 constructor() { 32 33 makeAutoObservable(this) ··· 73 74 details, 74 75 ts: Date.now(), 75 76 }) 77 + } 78 + 79 + time = (label = 'default') => { 80 + this.timers.set(label, performance.now()) 81 + } 82 + 83 + timeEnd = (label = 'default', warn = false) => { 84 + const endTime = performance.now() 85 + if (this.timers.has(label)) { 86 + const elapsedTime = endTime - this.timers.get(label)! 87 + console.log(`${label}: ${elapsedTime.toFixed(3)}ms`) 88 + this.timers.delete(label) 89 + } else { 90 + warn && console.warn(`Timer with label '${label}' does not exist.`) 91 + } 76 92 } 77 93 } 78 94
+6
src/state/models/me.ts
··· 8 8 import {NotificationsFeedModel} from './feeds/notifications' 9 9 import {MyFollowsCache} from './cache/my-follows' 10 10 import {isObj, hasProp} from 'lib/type-guards' 11 + import {SavedFeedsModel} from './ui/saved-feeds' 11 12 12 13 const PROFILE_UPDATE_INTERVAL = 10 * 60 * 1e3 // 10min 13 14 const NOTIFS_UPDATE_INTERVAL = 30 * 1e3 // 30sec ··· 21 22 followsCount: number | undefined 22 23 followersCount: number | undefined 23 24 mainFeed: PostsFeedModel 25 + savedFeeds: SavedFeedsModel 24 26 notifications: NotificationsFeedModel 25 27 follows: MyFollowsCache 26 28 invites: ComAtprotoServerDefs.InviteCode[] = [] ··· 43 45 }) 44 46 this.notifications = new NotificationsFeedModel(this.rootStore) 45 47 this.follows = new MyFollowsCache(this.rootStore) 48 + this.savedFeeds = new SavedFeedsModel(this.rootStore) 46 49 } 47 50 48 51 clear() { 49 52 this.mainFeed.clear() 50 53 this.notifications.clear() 51 54 this.follows.clear() 55 + this.savedFeeds.clear() 52 56 this.did = '' 53 57 this.handle = '' 54 58 this.displayName = '' ··· 110 114 /* dont await */ this.notifications.setup().catch(e => { 111 115 this.rootStore.log.error('Failed to setup notifications model', e) 112 116 }) 117 + /* dont await */ this.savedFeeds.refresh(true) 113 118 this.rootStore.emitSessionLoaded() 114 119 await this.fetchInviteCodes() 115 120 await this.fetchAppPasswords() ··· 119 124 } 120 125 121 126 async updateIfNeeded() { 127 + /* dont await */ this.savedFeeds.refresh(true) 122 128 if (Date.now() - this.lastProfileStateUpdate > PROFILE_UPDATE_INTERVAL) { 123 129 this.rootStore.log.debug('Updating me profile information') 124 130 this.lastProfileStateUpdate = Date.now()
+1 -1
src/state/models/media/image.ts
··· 135 135 // Only for mobile 136 136 async crop() { 137 137 try { 138 - const cropped = await openCropper({ 138 + const cropped = await openCropper(this.rootStore, { 139 139 mediaType: 'photo', 140 140 path: this.path, 141 141 freeStyleCropEnabled: true,
+153 -4
src/state/models/ui/preferences.ts
··· 11 11 ALWAYS_FILTER_LABEL_GROUP, 12 12 ALWAYS_WARN_LABEL_GROUP, 13 13 } from 'lib/labeling/const' 14 + import {DEFAULT_FEEDS} from 'lib/constants' 14 15 import {isIOS} from 'platform/detection' 15 16 16 17 const deviceLocales = getLocales() ··· 25 26 'spam', 26 27 'impersonation', 27 28 ] 29 + const VISIBILITY_VALUES = ['show', 'warn', 'hide'] 28 30 29 31 export class LabelPreferencesModel { 30 32 nsfw: LabelPreference = 'hide' ··· 45 47 contentLanguages: string[] = 46 48 deviceLocales?.map?.(locale => locale.languageCode) || [] 47 49 contentLabels = new LabelPreferencesModel() 50 + savedFeeds: string[] = [] 51 + pinnedFeeds: string[] = [] 48 52 49 53 constructor(public rootStore: RootStoreModel) { 50 54 makeAutoObservable(this, {}, {autoBind: true}) ··· 54 58 return { 55 59 contentLanguages: this.contentLanguages, 56 60 contentLabels: this.contentLabels, 61 + savedFeeds: this.savedFeeds, 62 + pinnedFeeds: this.pinnedFeeds, 57 63 } 58 64 } 59 65 66 + /** 67 + * The function hydrates an object with properties related to content languages, labels, saved feeds, 68 + * and pinned feeds that it gets from the parameter `v` (probably local storage) 69 + * @param {unknown} v - the data object to hydrate from 70 + */ 60 71 hydrate(v: unknown) { 61 72 if (isObj(v)) { 62 73 if ( ··· 72 83 // default to the device languages 73 84 this.contentLanguages = deviceLocales.map(locale => locale.languageCode) 74 85 } 86 + if ( 87 + hasProp(v, 'savedFeeds') && 88 + Array.isArray(v.savedFeeds) && 89 + typeof v.savedFeeds.every(item => typeof item === 'string') 90 + ) { 91 + this.savedFeeds = v.savedFeeds 92 + } 93 + if ( 94 + hasProp(v, 'pinnedFeeds') && 95 + Array.isArray(v.pinnedFeeds) && 96 + typeof v.pinnedFeeds.every(item => typeof item === 'string') 97 + ) { 98 + this.pinnedFeeds = v.pinnedFeeds 99 + } 75 100 } 76 101 } 77 102 103 + /** 104 + * This function fetches preferences and sets defaults for missing items. 105 + */ 78 106 async sync() { 107 + // fetch preferences 108 + let hasSavedFeedsPref = false 79 109 const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) 80 110 runInAction(() => { 81 111 for (const pref of res.data.preferences) { ··· 88 118 AppBskyActorDefs.isContentLabelPref(pref) && 89 119 AppBskyActorDefs.validateAdultContentPref(pref).success 90 120 ) { 91 - if (LABEL_GROUPS.includes(pref.label)) { 92 - this.contentLabels[pref.label] = pref.visibility 121 + if ( 122 + LABEL_GROUPS.includes(pref.label) && 123 + VISIBILITY_VALUES.includes(pref.visibility) 124 + ) { 125 + this.contentLabels[pref.label as keyof LabelPreferencesModel] = 126 + pref.visibility as LabelPreference 93 127 } 128 + } else if ( 129 + AppBskyActorDefs.isSavedFeedsPref(pref) && 130 + AppBskyActorDefs.validateSavedFeedsPref(pref).success 131 + ) { 132 + this.savedFeeds = pref.saved 133 + this.pinnedFeeds = pref.pinned 134 + hasSavedFeedsPref = true 94 135 } 95 136 } 96 137 }) 138 + 139 + // set defaults on missing items 140 + if (!hasSavedFeedsPref) { 141 + const {saved, pinned} = await DEFAULT_FEEDS( 142 + this.rootStore.agent.service.toString(), 143 + (handle: string) => 144 + this.rootStore.agent 145 + .resolveHandle({handle}) 146 + .then(({data}) => data.did), 147 + ) 148 + runInAction(() => { 149 + this.savedFeeds = saved 150 + this.pinnedFeeds = pinned 151 + }) 152 + res.data.preferences.push({ 153 + $type: 'app.bsky.actor.defs#savedFeedsPref', 154 + saved, 155 + pinned, 156 + }) 157 + await this.rootStore.agent.app.bsky.actor.putPreferences({ 158 + preferences: res.data.preferences, 159 + }) 160 + /* dont await */ this.rootStore.me.savedFeeds.refresh() 161 + } 97 162 } 98 163 99 - async update(cb: (prefs: AppBskyActorDefs.Preferences) => void) { 164 + /** 165 + * This function updates the preferences of a user and allows for a callback function to be executed 166 + * before the update. 167 + * @param cb - cb is a callback function that takes in a single parameter of type 168 + * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to 169 + * update the preferences of the user. The function is called with the current preferences as an 170 + * argument and if the callback returns false, the preferences are not updated. 171 + * @returns void 172 + */ 173 + async update(cb: (prefs: AppBskyActorDefs.Preferences) => boolean | void) { 100 174 const res = await this.rootStore.agent.app.bsky.actor.getPreferences({}) 101 - cb(res.data.preferences) 175 + if (cb(res.data.preferences) === false) { 176 + return 177 + } 102 178 await this.rootStore.agent.app.bsky.actor.putPreferences({ 103 179 preferences: res.data.preferences, 180 + }) 181 + } 182 + 183 + /** 184 + * This function resets the preferences to an empty array of no preferences. 185 + */ 186 + async reset() { 187 + runInAction(() => { 188 + this.contentLabels = new LabelPreferencesModel() 189 + this.contentLanguages = deviceLocales.map(locale => locale.languageCode) 190 + this.savedFeeds = [] 191 + this.pinnedFeeds = [] 192 + }) 193 + await this.rootStore.agent.app.bsky.actor.putPreferences({ 194 + preferences: [], 104 195 }) 105 196 } 106 197 ··· 199 290 res.pref = 'hide' 200 291 } 201 292 return res 293 + } 294 + 295 + setFeeds(saved: string[], pinned: string[]) { 296 + this.savedFeeds = saved 297 + this.pinnedFeeds = pinned 298 + } 299 + 300 + async setSavedFeeds(saved: string[], pinned: string[]) { 301 + const oldSaved = this.savedFeeds 302 + const oldPinned = this.pinnedFeeds 303 + this.setFeeds(saved, pinned) 304 + try { 305 + await this.update((prefs: AppBskyActorDefs.Preferences) => { 306 + const existing = prefs.find( 307 + pref => 308 + AppBskyActorDefs.isSavedFeedsPref(pref) && 309 + AppBskyActorDefs.validateSavedFeedsPref(pref).success, 310 + ) 311 + if (existing) { 312 + existing.saved = saved 313 + existing.pinned = pinned 314 + } else { 315 + prefs.push({ 316 + $type: 'app.bsky.actor.defs#savedFeedsPref', 317 + saved, 318 + pinned, 319 + }) 320 + } 321 + }) 322 + } catch (e) { 323 + runInAction(() => { 324 + this.savedFeeds = oldSaved 325 + this.pinnedFeeds = oldPinned 326 + }) 327 + throw e 328 + } 329 + } 330 + 331 + async addSavedFeed(v: string) { 332 + return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds) 333 + } 334 + 335 + async removeSavedFeed(v: string) { 336 + return this.setSavedFeeds( 337 + this.savedFeeds.filter(uri => uri !== v), 338 + this.pinnedFeeds.filter(uri => uri !== v), 339 + ) 340 + } 341 + 342 + async addPinnedFeed(v: string) { 343 + return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v]) 344 + } 345 + 346 + async removePinnedFeed(v: string) { 347 + return this.setSavedFeeds( 348 + this.savedFeeds, 349 + this.pinnedFeeds.filter(uri => uri !== v), 350 + ) 202 351 } 203 352 }
+26 -10
src/state/models/ui/profile.ts
··· 2 2 import {RootStoreModel} from '../root-store' 3 3 import {ProfileModel} from '../content/profile' 4 4 import {PostsFeedModel} from '../feeds/posts' 5 + import {ActorFeedsModel} from '../lists/actor-feeds' 5 6 import {ListsListModel} from '../lists/lists-list' 6 7 7 8 export enum Sections { 8 9 Posts = 'Posts', 9 10 PostsWithReplies = 'Posts & replies', 11 + CustomAlgorithms = 'Feeds', 10 12 Lists = 'Lists', 11 13 } 12 - 13 - const USER_SELECTOR_ITEMS = [ 14 - Sections.Posts, 15 - Sections.PostsWithReplies, 16 - Sections.Lists, 17 - ] 18 14 19 15 export interface ProfileUiParams { 20 16 user: string ··· 28 24 // data 29 25 profile: ProfileModel 30 26 feed: PostsFeedModel 27 + algos: ActorFeedsModel 31 28 lists: ListsListModel 32 29 33 30 // ui state ··· 50 47 actor: params.user, 51 48 limit: 10, 52 49 }) 50 + this.algos = new ActorFeedsModel(rootStore, {actor: params.user}) 53 51 this.lists = new ListsListModel(rootStore, params.user) 54 52 } 55 53 56 - get currentView(): PostsFeedModel | ListsListModel { 54 + get currentView(): PostsFeedModel | ActorFeedsModel | ListsListModel { 57 55 if ( 58 56 this.selectedView === Sections.Posts || 59 57 this.selectedView === Sections.PostsWithReplies ··· 62 60 } else if (this.selectedView === Sections.Lists) { 63 61 return this.lists 64 62 } 63 + if (this.selectedView === Sections.CustomAlgorithms) { 64 + return this.algos 65 + } 65 66 throw new Error(`Invalid selector value: ${this.selectedViewIndex}`) 66 67 } 67 68 ··· 75 76 } 76 77 77 78 get selectorItems() { 78 - return USER_SELECTOR_ITEMS 79 + const items = [Sections.Posts, Sections.PostsWithReplies] 80 + if (this.algos.hasLoaded && !this.algos.isEmpty) { 81 + items.push(Sections.CustomAlgorithms) 82 + } 83 + if (this.lists.hasLoaded && !this.lists.isEmpty) { 84 + items.push(Sections.Lists) 85 + } 86 + return items 79 87 } 80 88 81 89 get selectedView() { ··· 84 92 85 93 get uiItems() { 86 94 let arr: any[] = [] 95 + // if loading, return loading item to show loading spinner 87 96 if (this.isInitialLoading) { 88 97 arr = arr.concat([ProfileUiModel.LOADING_ITEM]) 89 98 } else if (this.currentView.hasError) { 99 + // if error, return error item to show error message 90 100 arr = arr.concat([ 91 101 { 92 102 _reactKey: '__error__', ··· 94 104 }, 95 105 ]) 96 106 } else { 107 + // not loading, no error, show content 97 108 if ( 98 109 this.selectedView === Sections.Posts || 99 - this.selectedView === Sections.PostsWithReplies 110 + this.selectedView === Sections.PostsWithReplies || 111 + this.selectedView === Sections.CustomAlgorithms 100 112 ) { 101 113 if (this.feed.hasContent) { 102 - if (this.selectedView === Sections.Posts) { 114 + if (this.selectedView === Sections.CustomAlgorithms) { 115 + arr = this.algos.feeds 116 + } else if (this.selectedView === Sections.Posts) { 103 117 arr = this.feed.nonReplyFeed 104 118 } else { 105 119 arr = this.feed.slices.slice() ··· 117 131 arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) 118 132 } 119 133 } else { 134 + // fallback, add empty item, to show empty message 120 135 arr = arr.concat([ProfileUiModel.EMPTY_ITEM]) 121 136 } 122 137 } ··· 151 166 .setup() 152 167 .catch(err => this.rootStore.log.error('Failed to fetch feed', err)), 153 168 ]) 169 + this.algos.refresh() 154 170 // HACK: need to use the DID as a param, not the username -prf 155 171 this.lists.source = this.profile.did 156 172 this.lists
+185
src/state/models/ui/saved-feeds.ts
··· 1 + import {makeAutoObservable, runInAction} from 'mobx' 2 + import {AppBskyFeedDefs} from '@atproto/api' 3 + import {RootStoreModel} from '../root-store' 4 + import {bundleAsync} from 'lib/async/bundle' 5 + import {cleanError} from 'lib/strings/errors' 6 + import {CustomFeedModel} from '../feeds/custom-feed' 7 + 8 + export class SavedFeedsModel { 9 + // state 10 + isLoading = false 11 + isRefreshing = false 12 + hasLoaded = false 13 + error = '' 14 + 15 + // data 16 + feeds: CustomFeedModel[] = [] 17 + 18 + constructor(public rootStore: RootStoreModel) { 19 + makeAutoObservable( 20 + this, 21 + { 22 + rootStore: false, 23 + }, 24 + {autoBind: true}, 25 + ) 26 + } 27 + 28 + get hasContent() { 29 + return this.feeds.length > 0 30 + } 31 + 32 + get hasError() { 33 + return this.error !== '' 34 + } 35 + 36 + get isEmpty() { 37 + return this.hasLoaded && !this.hasContent 38 + } 39 + 40 + get pinned() { 41 + return this.rootStore.preferences.pinnedFeeds 42 + .map(uri => this.feeds.find(f => f.uri === uri) as CustomFeedModel) 43 + .filter(Boolean) 44 + } 45 + 46 + get unpinned() { 47 + return this.feeds.filter(f => !this.isPinned(f)) 48 + } 49 + 50 + get all() { 51 + return this.pinned.concat(this.unpinned) 52 + } 53 + 54 + get pinnedFeedNames() { 55 + return this.pinned.map(f => f.displayName) 56 + } 57 + 58 + // public api 59 + // = 60 + 61 + clear() { 62 + this.isLoading = false 63 + this.isRefreshing = false 64 + this.hasLoaded = false 65 + this.error = '' 66 + this.feeds = [] 67 + } 68 + 69 + refresh = bundleAsync(async (quietRefresh = false) => { 70 + this._xLoading(!quietRefresh) 71 + try { 72 + let feeds: AppBskyFeedDefs.GeneratorView[] = [] 73 + for ( 74 + let i = 0; 75 + i < this.rootStore.preferences.savedFeeds.length; 76 + i += 25 77 + ) { 78 + const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerators({ 79 + feeds: this.rootStore.preferences.savedFeeds.slice(i, 25), 80 + }) 81 + feeds = feeds.concat(res.data.feeds) 82 + } 83 + runInAction(() => { 84 + this.feeds = feeds.map(f => new CustomFeedModel(this.rootStore, f)) 85 + }) 86 + this._xIdle() 87 + } catch (e: any) { 88 + this._xIdle(e) 89 + } 90 + }) 91 + 92 + async save(feed: CustomFeedModel) { 93 + try { 94 + await feed.save() 95 + runInAction(() => { 96 + this.feeds = [ 97 + ...this.feeds, 98 + new CustomFeedModel(this.rootStore, feed.data), 99 + ] 100 + }) 101 + } catch (e: any) { 102 + this.rootStore.log.error('Failed to save feed', e) 103 + } 104 + } 105 + 106 + async unsave(feed: CustomFeedModel) { 107 + const uri = feed.uri 108 + try { 109 + if (this.isPinned(feed)) { 110 + await this.rootStore.preferences.removePinnedFeed(uri) 111 + } 112 + await feed.unsave() 113 + runInAction(() => { 114 + this.feeds = this.feeds.filter(f => f.data.uri !== uri) 115 + }) 116 + } catch (e: any) { 117 + this.rootStore.log.error('Failed to unsave feed', e) 118 + } 119 + } 120 + 121 + async togglePinnedFeed(feed: CustomFeedModel) { 122 + if (!this.isPinned(feed)) { 123 + return this.rootStore.preferences.addPinnedFeed(feed.uri) 124 + } else { 125 + return this.rootStore.preferences.removePinnedFeed(feed.uri) 126 + } 127 + } 128 + 129 + async reorderPinnedFeeds(feeds: CustomFeedModel[]) { 130 + return this.rootStore.preferences.setSavedFeeds( 131 + this.rootStore.preferences.savedFeeds, 132 + feeds.filter(feed => this.isPinned(feed)).map(feed => feed.uri), 133 + ) 134 + } 135 + 136 + isPinned(feedOrUri: CustomFeedModel | string) { 137 + let uri: string 138 + if (typeof feedOrUri === 'string') { 139 + uri = feedOrUri 140 + } else { 141 + uri = feedOrUri.uri 142 + } 143 + return this.rootStore.preferences.pinnedFeeds.includes(uri) 144 + } 145 + 146 + async movePinnedFeed(item: CustomFeedModel, direction: 'up' | 'down') { 147 + const pinned = this.rootStore.preferences.pinnedFeeds.slice() 148 + const index = pinned.indexOf(item.uri) 149 + if (index === -1) { 150 + return 151 + } 152 + if (direction === 'up' && index !== 0) { 153 + const temp = pinned[index] 154 + pinned[index] = pinned[index - 1] 155 + pinned[index - 1] = temp 156 + } else if (direction === 'down' && index < pinned.length - 1) { 157 + const temp = pinned[index] 158 + pinned[index] = pinned[index + 1] 159 + pinned[index + 1] = temp 160 + } 161 + await this.rootStore.preferences.setSavedFeeds( 162 + this.rootStore.preferences.savedFeeds, 163 + pinned, 164 + ) 165 + } 166 + 167 + // state transitions 168 + // = 169 + 170 + _xLoading(isRefreshing = false) { 171 + this.isLoading = true 172 + this.isRefreshing = isRefreshing 173 + this.error = '' 174 + } 175 + 176 + _xIdle(err?: any) { 177 + this.isLoading = false 178 + this.isRefreshing = false 179 + this.hasLoaded = true 180 + this.error = cleanError(err) 181 + if (err) { 182 + this.rootStore.log.error('Failed to fetch user feeds', err) 183 + } 184 + } 185 + }
+1 -1
src/state/models/ui/shell.ts
··· 119 119 // Moderation 120 120 | ReportAccountModal 121 121 | ReportPostModal 122 - | CreateMuteListModal 122 + | CreateOrEditMuteListModal 123 123 | ListAddRemoveUserModal 124 124 125 125 // Posts
+20 -2
src/view/com/composer/useExternalLinkFetch.ts
··· 2 2 import {useStores} from 'state/index' 3 3 import * as apilib from 'lib/api/index' 4 4 import {getLinkMeta} from 'lib/link-meta/link-meta' 5 - import {getPostAsQuote} from 'lib/link-meta/bsky' 5 + import {getPostAsQuote, getFeedAsEmbed} from 'lib/link-meta/bsky' 6 6 import {downloadAndResize} from 'lib/media/manip' 7 - import {isBskyPostUrl} from 'lib/strings/url-helpers' 7 + import {isBskyPostUrl, isBskyCustomFeedUrl} from 'lib/strings/url-helpers' 8 8 import {ComposerOpts} from 'state/models/ui/shell' 9 9 import {POST_IMG_MAX} from 'lib/constants' 10 10 ··· 38 38 }, 39 39 err => { 40 40 store.log.error('Failed to fetch post for quote embedding', {err}) 41 + setExtLink(undefined) 42 + }, 43 + ) 44 + } else if (isBskyCustomFeedUrl(extLink.uri)) { 45 + getFeedAsEmbed(store, extLink.uri).then( 46 + ({embed, meta}) => { 47 + if (aborted) { 48 + return 49 + } 50 + setExtLink({ 51 + uri: extLink.uri, 52 + isLoading: false, 53 + meta, 54 + embed, 55 + }) 56 + }, 57 + err => { 58 + store.log.error('Failed to fetch feed for embedding', {err}) 41 59 setExtLink(undefined) 42 60 }, 43 61 )
+162
src/view/com/feeds/CustomFeed.tsx
··· 1 + import React from 'react' 2 + import { 3 + Pressable, 4 + StyleProp, 5 + StyleSheet, 6 + View, 7 + ViewStyle, 8 + TouchableOpacity, 9 + } from 'react-native' 10 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 11 + import {Text} from '../util/text/Text' 12 + import {usePalette} from 'lib/hooks/usePalette' 13 + import {s} from 'lib/styles' 14 + import {UserAvatar} from '../util/UserAvatar' 15 + import {observer} from 'mobx-react-lite' 16 + import {CustomFeedModel} from 'state/models/feeds/custom-feed' 17 + import {useNavigation} from '@react-navigation/native' 18 + import {NavigationProp} from 'lib/routes/types' 19 + import {useStores} from 'state/index' 20 + import {pluralize} from 'lib/strings/helpers' 21 + import {AtUri} from '@atproto/api' 22 + import * as Toast from 'view/com/util/Toast' 23 + 24 + export const CustomFeed = observer( 25 + ({ 26 + item, 27 + style, 28 + showSaveBtn = false, 29 + showDescription = false, 30 + showLikes = false, 31 + }: { 32 + item: CustomFeedModel 33 + style?: StyleProp<ViewStyle> 34 + showSaveBtn?: boolean 35 + showDescription?: boolean 36 + showLikes?: boolean 37 + }) => { 38 + const store = useStores() 39 + const pal = usePalette('default') 40 + const navigation = useNavigation<NavigationProp>() 41 + 42 + const onToggleSaved = React.useCallback(async () => { 43 + if (item.isSaved) { 44 + store.shell.openModal({ 45 + name: 'confirm', 46 + title: 'Remove from my feeds', 47 + message: `Remove ${item.displayName} from my feeds?`, 48 + onPressConfirm: async () => { 49 + try { 50 + await store.me.savedFeeds.unsave(item) 51 + Toast.show('Removed from my feeds') 52 + } catch (e) { 53 + Toast.show('There was an issue contacting your server') 54 + store.log.error('Failed to unsave feed', {e}) 55 + } 56 + }, 57 + }) 58 + } else { 59 + try { 60 + await store.me.savedFeeds.save(item) 61 + Toast.show('Added to my feeds') 62 + } catch (e) { 63 + Toast.show('There was an issue contacting your server') 64 + store.log.error('Failed to save feed', {e}) 65 + } 66 + } 67 + }, [store, item]) 68 + 69 + return ( 70 + <TouchableOpacity 71 + accessibilityRole="button" 72 + style={[styles.container, pal.border, style]} 73 + onPress={() => { 74 + navigation.navigate('CustomFeed', { 75 + name: item.data.creator.did, 76 + rkey: new AtUri(item.data.uri).rkey, 77 + }) 78 + }} 79 + key={item.data.uri}> 80 + <View style={[styles.headerContainer]}> 81 + <View style={[s.mr10]}> 82 + <UserAvatar type="algo" size={36} avatar={item.data.avatar} /> 83 + </View> 84 + <View style={[styles.headerTextContainer]}> 85 + <Text style={[pal.text, s.bold]} numberOfLines={3}> 86 + {item.displayName} 87 + </Text> 88 + <Text style={[pal.textLight]} numberOfLines={3}> 89 + by @{item.data.creator.handle} 90 + </Text> 91 + </View> 92 + {showSaveBtn && ( 93 + <View> 94 + <Pressable 95 + accessibilityRole="button" 96 + accessibilityLabel={ 97 + item.isSaved ? 'Remove from my feeds' : 'Add to my feeds' 98 + } 99 + accessibilityHint="" 100 + onPress={onToggleSaved} 101 + hitSlop={15} 102 + style={styles.btn}> 103 + {item.isSaved ? ( 104 + <FontAwesomeIcon 105 + icon={['far', 'trash-can']} 106 + size={19} 107 + color={pal.colors.icon} 108 + /> 109 + ) : ( 110 + <FontAwesomeIcon 111 + icon="plus" 112 + size={18} 113 + color={pal.colors.link} 114 + /> 115 + )} 116 + </Pressable> 117 + </View> 118 + )} 119 + </View> 120 + 121 + {showDescription && item.data.description ? ( 122 + <Text style={[pal.textLight, styles.description]} numberOfLines={3}> 123 + {item.data.description} 124 + </Text> 125 + ) : null} 126 + 127 + {showLikes ? ( 128 + <Text type="sm-medium" style={[pal.text, pal.textLight]}> 129 + Liked by {item.data.likeCount || 0}{' '} 130 + {pluralize(item.data.likeCount || 0, 'user')} 131 + </Text> 132 + ) : null} 133 + </TouchableOpacity> 134 + ) 135 + }, 136 + ) 137 + 138 + const styles = StyleSheet.create({ 139 + container: { 140 + paddingHorizontal: 18, 141 + paddingVertical: 20, 142 + flexDirection: 'column', 143 + flex: 1, 144 + borderTopWidth: 1, 145 + gap: 14, 146 + }, 147 + headerContainer: { 148 + flexDirection: 'row', 149 + }, 150 + headerTextContainer: { 151 + flexDirection: 'column', 152 + columnGap: 4, 153 + flex: 1, 154 + }, 155 + description: { 156 + flex: 1, 157 + flexWrap: 'wrap', 158 + }, 159 + btn: { 160 + paddingVertical: 6, 161 + }, 162 + })
+1 -1
src/view/com/lists/ListCard.tsx
··· 60 60 anchorNoUnderline> 61 61 <View style={styles.layout}> 62 62 <View style={styles.layoutAvi}> 63 - <UserAvatar size={40} avatar={list.avatar} /> 63 + <UserAvatar type="list" size={40} avatar={list.avatar} /> 64 64 </View> 65 65 <View style={styles.layoutContent}> 66 66 <Text
+1 -1
src/view/com/lists/ListItems.tsx
··· 341 341 )} 342 342 </View> 343 343 <View> 344 - <UserAvatar avatar={list.avatar} size={64} /> 344 + <UserAvatar type="list" avatar={list.avatar} size={64} /> 345 345 </View> 346 346 </View> 347 347 <View style={[styles.fakeSelector, pal.border]}>
+2 -2
src/view/com/modals/ContentLanguagesSettings.tsx
··· 41 41 <View testID="contentLanguagesModal" style={[pal.view, styles.container]}> 42 42 <Text style={[pal.text, styles.title]}>Content Languages</Text> 43 43 <Text style={[pal.text, styles.description]}> 44 - Which languages would you like to see in the What's Hot feed? (Leave 45 - them all unchecked to see any language.) 44 + Which languages would you like to see in the your feed? (Leave them all 45 + unchecked to see any language.) 46 46 </Text> 47 47 <ScrollView style={styles.scrollContainer}> 48 48 {languages.map(lang => (
+1
src/view/com/modals/CreateOrEditMuteList.tsx
··· 143 143 <Text style={[styles.label, pal.text]}>List Avatar</Text> 144 144 <View style={[styles.avi, {borderColor: pal.colors.background}]}> 145 145 <UserAvatar 146 + type="list" 146 147 size={80} 147 148 avatar={avatar} 148 149 onSelectNewAvatar={onSelectNewAvatar}
+1
src/view/com/notifications/Feed.tsx
··· 154 154 onEndReached={onEndReached} 155 155 onEndReachedThreshold={0.6} 156 156 onScroll={onScroll} 157 + scrollEventThrottle={100} 157 158 contentContainerStyle={s.contentContainer} 158 159 /> 159 160 ) : null}
+15
src/view/com/pager/DraggableScrollView.tsx
··· 1 + import {useDraggableScroll} from 'lib/hooks/useDraggableScrollView' 2 + import React, {ComponentProps} from 'react' 3 + import {ScrollView} from 'react-native' 4 + 5 + export const DraggableScrollView = React.forwardRef< 6 + ScrollView, 7 + ComponentProps<typeof ScrollView> 8 + >(function DraggableScrollView(props, ref) { 9 + const {refs} = useDraggableScroll<ScrollView>({ 10 + outerRef: ref, 11 + cursor: 'grab', // optional, default 12 + }) 13 + 14 + return <ScrollView ref={refs} horizontal {...props} /> 15 + })
+8 -3
src/view/com/pager/FeedsTabBar.web.tsx
··· 1 - import React from 'react' 1 + import React, {useMemo} from 'react' 2 2 import {Animated, StyleSheet} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 4 import {TabBar} from 'view/com/pager/TabBar' ··· 27 27 props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 28 28 ) => { 29 29 const store = useStores() 30 + const items = useMemo( 31 + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames], 32 + [store.me.savedFeeds.pinnedFeedNames], 33 + ) 30 34 const pal = usePalette('default') 31 35 const interp = useAnimatedValue(0) 32 36 ··· 44 48 {translateY: Animated.multiply(interp, -100)}, 45 49 ], 46 50 } 51 + 47 52 return ( 48 53 // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf 49 54 <Animated.View style={[pal.view, styles.tabBar, transform]}> 50 55 <TabBar 56 + key={items.join(',')} 51 57 {...props} 52 - items={['Following', "What's hot"]} 53 - indicatorPosition="bottom" 58 + items={items} 54 59 indicatorColor={pal.colors.link} 55 60 /> 56 61 </Animated.View>
+56 -18
src/view/com/pager/FeedsTabBarMobile.tsx
··· 1 - import React from 'react' 2 - import {Animated, StyleSheet, TouchableOpacity} from 'react-native' 1 + import React, {useMemo} from 'react' 2 + import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 4 import {TabBar} from 'view/com/pager/TabBar' 5 5 import {RenderTabBarFnProps} from 'view/com/pager/Pager' 6 - import {UserAvatar} from '../util/UserAvatar' 7 6 import {useStores} from 'state/index' 8 7 import {usePalette} from 'lib/hooks/usePalette' 9 8 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 9 + import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' 10 + import {Link} from '../util/Link' 11 + import {Text} from '../util/text/Text' 12 + import {CogIcon} from 'lib/icons' 13 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 14 + import {s} from 'lib/styles' 10 15 11 16 export const FeedsTabBar = observer( 12 17 ( ··· 28 33 transform: [{translateY: Animated.multiply(interp, -100)}], 29 34 } 30 35 36 + const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3) 37 + 31 38 const onPressAvi = React.useCallback(() => { 32 39 store.shell.openDrawer() 33 40 }, [store]) 34 41 42 + const items = useMemo( 43 + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames], 44 + [store.me.savedFeeds.pinnedFeedNames], 45 + ) 46 + 35 47 return ( 36 48 <Animated.View style={[pal.view, pal.border, styles.tabBar, transform]}> 37 - <TouchableOpacity 38 - testID="viewHeaderDrawerBtn" 39 - style={styles.tabBarAvi} 40 - onPress={onPressAvi} 41 - accessibilityRole="button" 42 - accessibilityLabel="Menu" 43 - accessibilityHint="Access navigation links and settings"> 44 - <UserAvatar avatar={store.me.avatar} size={30} /> 45 - </TouchableOpacity> 49 + <View style={[pal.view, styles.topBar]}> 50 + <View style={[pal.view]}> 51 + <TouchableOpacity 52 + testID="viewHeaderDrawerBtn" 53 + onPress={onPressAvi} 54 + accessibilityRole="button" 55 + accessibilityLabel="Open navigation" 56 + accessibilityHint="Access profile and other navigation links" 57 + hitSlop={10}> 58 + <FontAwesomeIcon 59 + icon="bars" 60 + size={18} 61 + color={pal.colors.textLight} 62 + /> 63 + </TouchableOpacity> 64 + </View> 65 + <Text style={[brandBlue, s.bold, styles.title]}>Bluesky</Text> 66 + <View style={[pal.view]}> 67 + <Link 68 + href="/settings/saved-feeds" 69 + hitSlop={10} 70 + accessibilityRole="button" 71 + accessibilityLabel="Edit Saved Feeds" 72 + accessibilityHint="Opens screen to edit Saved Feeds"> 73 + <CogIcon size={21} strokeWidth={2} style={pal.textLight} /> 74 + </Link> 75 + </View> 76 + </View> 46 77 <TabBar 78 + key={items.join(',')} 47 79 {...props} 48 - items={['Following', "What's hot"]} 49 - indicatorPosition="bottom" 80 + items={items} 50 81 indicatorColor={pal.colors.link} 51 82 /> 52 83 </Animated.View> ··· 61 92 left: 0, 62 93 right: 0, 63 94 top: 0, 95 + flexDirection: 'column', 96 + alignItems: 'center', 97 + borderBottomWidth: 1, 98 + }, 99 + topBar: { 64 100 flexDirection: 'row', 101 + justifyContent: 'space-between', 65 102 alignItems: 'center', 66 103 paddingHorizontal: 18, 67 - borderBottomWidth: 1, 104 + paddingTop: 8, 105 + paddingBottom: 2, 106 + width: '100%', 68 107 }, 69 - tabBarAvi: { 70 - marginTop: 1, 71 - marginRight: 18, 108 + title: { 109 + fontSize: 21, 72 110 }, 73 111 })
+59 -66
src/view/com/pager/Pager.tsx
··· 1 - import React from 'react' 1 + import React, {forwardRef} from 'react' 2 2 import {Animated, View} from 'react-native' 3 3 import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' 4 - import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 5 4 import {s} from 'lib/styles' 6 5 7 6 export type PageSelectedEvent = PagerViewOnPageSelectedEvent 8 7 const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) 9 8 9 + export interface PagerRef { 10 + setPage: (index: number) => void 11 + } 12 + 10 13 export interface RenderTabBarFnProps { 11 14 selectedPage: number 12 - position: Animated.Value 13 - offset: Animated.Value 14 15 onSelect?: (index: number) => void 15 16 } 16 17 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element ··· 22 23 onPageSelected?: (index: number) => void 23 24 testID?: string 24 25 } 25 - export const Pager = ({ 26 - children, 27 - tabBarPosition = 'top', 28 - initialPage = 0, 29 - renderTabBar, 30 - onPageSelected, 31 - testID, 32 - }: React.PropsWithChildren<Props>) => { 33 - const [selectedPage, setSelectedPage] = React.useState(0) 34 - const position = useAnimatedValue(0) 35 - const offset = useAnimatedValue(0) 36 - const pagerView = React.useRef<PagerView>() 26 + export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( 27 + ( 28 + { 29 + children, 30 + tabBarPosition = 'top', 31 + initialPage = 0, 32 + renderTabBar, 33 + onPageSelected, 34 + testID, 35 + }: React.PropsWithChildren<Props>, 36 + ref, 37 + ) => { 38 + const [selectedPage, setSelectedPage] = React.useState(0) 39 + const pagerView = React.useRef<PagerView>() 40 + 41 + React.useImperativeHandle(ref, () => ({ 42 + setPage: (index: number) => pagerView.current?.setPage(index), 43 + })) 37 44 38 - const onPageSelectedInner = React.useCallback( 39 - (e: PageSelectedEvent) => { 40 - setSelectedPage(e.nativeEvent.position) 41 - onPageSelected?.(e.nativeEvent.position) 42 - }, 43 - [setSelectedPage, onPageSelected], 44 - ) 45 + const onPageSelectedInner = React.useCallback( 46 + (e: PageSelectedEvent) => { 47 + setSelectedPage(e.nativeEvent.position) 48 + onPageSelected?.(e.nativeEvent.position) 49 + }, 50 + [setSelectedPage, onPageSelected], 51 + ) 45 52 46 - const onTabBarSelect = React.useCallback( 47 - (index: number) => { 48 - pagerView.current?.setPage(index) 49 - }, 50 - [pagerView], 51 - ) 53 + const onTabBarSelect = React.useCallback( 54 + (index: number) => { 55 + pagerView.current?.setPage(index) 56 + }, 57 + [pagerView], 58 + ) 52 59 53 - return ( 54 - <View testID={testID}> 55 - {tabBarPosition === 'top' && 56 - renderTabBar({ 57 - selectedPage, 58 - position, 59 - offset, 60 - onSelect: onTabBarSelect, 61 - })} 62 - <AnimatedPagerView 63 - ref={pagerView} 64 - style={s.h100pct} 65 - initialPage={initialPage} 66 - onPageSelected={onPageSelectedInner} 67 - onPageScroll={Animated.event( 68 - [ 69 - { 70 - nativeEvent: { 71 - position: position, 72 - offset: offset, 73 - }, 74 - }, 75 - ], 76 - {useNativeDriver: true}, 77 - )}> 78 - {children} 79 - </AnimatedPagerView> 80 - {tabBarPosition === 'bottom' && 81 - renderTabBar({ 82 - selectedPage, 83 - position, 84 - offset, 85 - onSelect: onTabBarSelect, 86 - })} 87 - </View> 88 - ) 89 - } 60 + return ( 61 + <View testID={testID}> 62 + {tabBarPosition === 'top' && 63 + renderTabBar({ 64 + selectedPage, 65 + onSelect: onTabBarSelect, 66 + })} 67 + <AnimatedPagerView 68 + ref={pagerView} 69 + style={s.h100pct} 70 + initialPage={initialPage} 71 + onPageSelected={onPageSelectedInner}> 72 + {children} 73 + </AnimatedPagerView> 74 + {tabBarPosition === 'bottom' && 75 + renderTabBar({ 76 + selectedPage, 77 + onSelect: onTabBarSelect, 78 + })} 79 + </View> 80 + ) 81 + }, 82 + )
+47 -52
src/view/com/pager/Pager.web.tsx
··· 1 1 import React from 'react' 2 - import {Animated, View} from 'react-native' 3 - import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 2 + import {View} from 'react-native' 4 3 import {s} from 'lib/styles' 5 4 6 5 export interface RenderTabBarFnProps { 7 6 selectedPage: number 8 - position: Animated.Value 9 - offset: Animated.Value 10 7 onSelect?: (index: number) => void 11 8 } 12 9 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element ··· 17 14 renderTabBar: RenderTabBarFn 18 15 onPageSelected?: (index: number) => void 19 16 } 20 - export const Pager = ({ 21 - children, 22 - tabBarPosition = 'top', 23 - initialPage = 0, 24 - renderTabBar, 25 - onPageSelected, 26 - }: React.PropsWithChildren<Props>) => { 27 - const [selectedPage, setSelectedPage] = React.useState(initialPage) 28 - const position = useAnimatedValue(0) 29 - const offset = useAnimatedValue(0) 17 + export const Pager = React.forwardRef( 18 + ( 19 + { 20 + children, 21 + tabBarPosition = 'top', 22 + initialPage = 0, 23 + renderTabBar, 24 + onPageSelected, 25 + }: React.PropsWithChildren<Props>, 26 + ref, 27 + ) => { 28 + const [selectedPage, setSelectedPage] = React.useState(initialPage) 30 29 31 - const onTabBarSelect = React.useCallback( 32 - (index: number) => { 33 - setSelectedPage(index) 34 - onPageSelected?.(index) 35 - Animated.timing(position, { 36 - toValue: index, 37 - duration: 200, 38 - useNativeDriver: true, 39 - }).start() 40 - }, 41 - [setSelectedPage, onPageSelected, position], 42 - ) 30 + React.useImperativeHandle(ref, () => ({ 31 + setPage: (index: number) => setSelectedPage(index), 32 + })) 33 + 34 + const onTabBarSelect = React.useCallback( 35 + (index: number) => { 36 + setSelectedPage(index) 37 + onPageSelected?.(index) 38 + }, 39 + [setSelectedPage, onPageSelected], 40 + ) 43 41 44 - return ( 45 - <View> 46 - {tabBarPosition === 'top' && 47 - renderTabBar({ 48 - selectedPage, 49 - position, 50 - offset, 51 - onSelect: onTabBarSelect, 52 - })} 53 - {React.Children.map(children, (child, i) => ( 54 - <View 55 - style={selectedPage === i ? undefined : s.hidden} 56 - key={`page-${i}`}> 57 - {child} 58 - </View> 59 - ))} 60 - {tabBarPosition === 'bottom' && 61 - renderTabBar({ 62 - selectedPage, 63 - position, 64 - offset, 65 - onSelect: onTabBarSelect, 66 - })} 67 - </View> 68 - ) 69 - } 42 + return ( 43 + <View> 44 + {tabBarPosition === 'top' && 45 + renderTabBar({ 46 + selectedPage, 47 + onSelect: onTabBarSelect, 48 + })} 49 + {React.Children.map(children, (child, i) => ( 50 + <View 51 + style={selectedPage === i ? undefined : s.hidden} 52 + key={`page-${i}`}> 53 + {child} 54 + </View> 55 + ))} 56 + {tabBarPosition === 'bottom' && 57 + renderTabBar({ 58 + selectedPage, 59 + onSelect: onTabBarSelect, 60 + })} 61 + </View> 62 + ) 63 + }, 64 + )
+86 -116
src/view/com/pager/TabBar.tsx
··· 1 - import React, {createRef, useState, useMemo, useRef} from 'react' 2 - import {Animated, StyleSheet, View} from 'react-native' 1 + import React, { 2 + useRef, 3 + createRef, 4 + useMemo, 5 + useEffect, 6 + useState, 7 + useCallback, 8 + } from 'react' 9 + import {StyleSheet, View, ScrollView} from 'react-native' 3 10 import {Text} from '../util/text/Text' 4 11 import {PressableWithHover} from '../util/PressableWithHover' 5 12 import {usePalette} from 'lib/hooks/usePalette' 6 - import {isDesktopWeb} from 'platform/detection' 7 - 8 - interface Layout { 9 - x: number 10 - width: number 11 - } 13 + import {isDesktopWeb, isMobileWeb} from 'platform/detection' 14 + import {DraggableScrollView} from './DraggableScrollView' 12 15 13 16 export interface TabBarProps { 14 17 testID?: string 15 18 selectedPage: number 16 19 items: string[] 17 - position: Animated.Value 18 - offset: Animated.Value 19 - indicatorPosition?: 'top' | 'bottom' 20 20 indicatorColor?: string 21 21 onSelect?: (index: number) => void 22 22 onPressSelected?: () => void ··· 26 26 testID, 27 27 selectedPage, 28 28 items, 29 - position, 30 - offset, 31 - indicatorPosition = 'bottom', 32 29 indicatorColor, 33 30 onSelect, 34 31 onPressSelected, 35 32 }: TabBarProps) { 36 33 const pal = usePalette('default') 37 - const [itemLayouts, setItemLayouts] = useState<Layout[]>( 38 - items.map(() => ({x: 0, width: 0})), 39 - ) 34 + const scrollElRef = useRef<ScrollView>(null) 35 + const [itemXs, setItemXs] = useState<number[]>([]) 40 36 const itemRefs = useMemo( 41 37 () => Array.from({length: items.length}).map(() => createRef<View>()), 42 38 [items.length], 43 39 ) 44 - const panX = Animated.add(position, offset) 45 - const containerRef = useRef<View>(null) 40 + const indicatorStyle = useMemo( 41 + () => ({borderBottomColor: indicatorColor || pal.colors.link}), 42 + [indicatorColor, pal], 43 + ) 44 + 45 + useEffect(() => { 46 + scrollElRef.current?.scrollTo({x: itemXs[selectedPage] || 0}) 47 + }, [scrollElRef, itemXs, selectedPage]) 46 48 47 - const indicatorStyle = { 48 - backgroundColor: indicatorColor || pal.colors.link, 49 - bottom: 50 - indicatorPosition === 'bottom' ? (isDesktopWeb ? 0 : -1) : undefined, 51 - top: indicatorPosition === 'top' ? (isDesktopWeb ? 0 : -1) : undefined, 52 - transform: [ 53 - { 54 - translateX: panX.interpolate({ 55 - inputRange: items.map((_item, i) => i), 56 - outputRange: itemLayouts.map(l => l.x + l.width / 2), 57 - }), 58 - }, 59 - { 60 - scaleX: panX.interpolate({ 61 - inputRange: items.map((_item, i) => i), 62 - outputRange: itemLayouts.map(l => l.width), 63 - }), 64 - }, 65 - ], 66 - } 49 + const onPressItem = useCallback( 50 + (index: number) => { 51 + onSelect?.(index) 52 + if (index === selectedPage) { 53 + onPressSelected?.() 54 + } 55 + }, 56 + [onSelect, onPressSelected, selectedPage], 57 + ) 67 58 68 59 const onLayout = React.useCallback(() => { 69 60 const promises = [] 70 61 for (let i = 0; i < items.length; i++) { 71 62 promises.push( 72 - new Promise<Layout>(resolve => { 73 - if (!containerRef.current || !itemRefs[i].current) { 74 - return resolve({x: 0, width: 0}) 63 + new Promise<number>(resolve => { 64 + if (!itemRefs[i].current) { 65 + return resolve(0) 75 66 } 76 67 77 - itemRefs[i].current?.measureLayout( 78 - containerRef.current, 79 - (x: number, _y: number, width: number) => { 80 - resolve({x, width}) 81 - }, 82 - ) 68 + itemRefs[i].current?.measure((x: number) => resolve(x)) 83 69 }), 84 70 ) 85 71 } 86 - Promise.all(promises).then((layouts: Layout[]) => { 87 - setItemLayouts(layouts) 72 + Promise.all(promises).then((Xs: number[]) => { 73 + setItemXs(Xs) 88 74 }) 89 - }, [containerRef, itemRefs, setItemLayouts, items.length]) 90 - 91 - const onPressItem = React.useCallback( 92 - (index: number) => { 93 - onSelect?.(index) 94 - if (index === selectedPage) { 95 - onPressSelected?.() 96 - } 97 - }, 98 - [onSelect, onPressSelected, selectedPage], 99 - ) 75 + }, [itemRefs, setItemXs, items.length]) 100 76 101 77 return ( 102 - <View 103 - testID={testID} 104 - style={[pal.view, styles.outer]} 105 - onLayout={onLayout} 106 - ref={containerRef}> 107 - <Animated.View style={[styles.indicator, indicatorStyle]} /> 108 - {items.map((item, i) => { 109 - const selected = i === selectedPage 110 - return ( 111 - <PressableWithHover 112 - ref={itemRefs[i]} 113 - key={item} 114 - style={ 115 - indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom 116 - } 117 - hoverStyle={pal.viewLight} 118 - onPress={() => onPressItem(i)}> 119 - <Text 120 - type="xl-bold" 121 - testID={testID ? `${testID}-${item}` : undefined} 122 - style={selected ? pal.text : pal.textLight}> 123 - {item} 124 - </Text> 125 - </PressableWithHover> 126 - ) 127 - })} 78 + <View testID={testID} style={[pal.view, styles.outer]}> 79 + <DraggableScrollView 80 + horizontal={true} 81 + showsHorizontalScrollIndicator={false} 82 + ref={scrollElRef} 83 + contentContainerStyle={styles.contentContainer} 84 + onLayout={onLayout}> 85 + {items.map((item, i) => { 86 + const selected = i === selectedPage 87 + return ( 88 + <PressableWithHover 89 + ref={itemRefs[i]} 90 + key={item} 91 + style={[styles.item, selected && indicatorStyle]} 92 + hoverStyle={pal.viewLight} 93 + onPress={() => onPressItem(i)}> 94 + <Text 95 + type={isDesktopWeb ? 'xl-bold' : 'lg-bold'} 96 + testID={testID ? `${testID}-${item}` : undefined} 97 + style={selected ? pal.text : pal.textLight}> 98 + {item} 99 + </Text> 100 + </PressableWithHover> 101 + ) 102 + })} 103 + </DraggableScrollView> 128 104 </View> 129 105 ) 130 106 } ··· 133 109 ? StyleSheet.create({ 134 110 outer: { 135 111 flexDirection: 'row', 136 - paddingHorizontal: 18, 112 + width: 598, 137 113 }, 138 - itemTop: { 139 - paddingTop: 16, 140 - paddingBottom: 14, 141 - paddingHorizontal: 12, 114 + contentContainer: { 115 + columnGap: 8, 116 + marginLeft: 14, 117 + paddingRight: 14, 118 + backgroundColor: 'transparent', 142 119 }, 143 - itemBottom: { 120 + item: { 144 121 paddingTop: 14, 145 - paddingBottom: 16, 146 - paddingHorizontal: 12, 147 - }, 148 - indicator: { 149 - position: 'absolute', 150 - left: 0, 151 - width: 1, 152 - height: 3, 153 - zIndex: 1, 122 + paddingBottom: 12, 123 + paddingHorizontal: 10, 124 + borderBottomWidth: 3, 125 + borderBottomColor: 'transparent', 154 126 }, 155 127 }) 156 128 : StyleSheet.create({ 157 129 outer: { 130 + flex: 1, 158 131 flexDirection: 'row', 159 - paddingHorizontal: 14, 132 + backgroundColor: 'transparent', 133 + }, 134 + contentContainer: { 135 + columnGap: isMobileWeb ? 0 : 20, 136 + marginLeft: isMobileWeb ? 0 : 18, 137 + paddingRight: isMobileWeb ? 0 : 36, 138 + backgroundColor: 'transparent', 160 139 }, 161 - itemTop: { 140 + item: { 162 141 paddingTop: 10, 163 142 paddingBottom: 10, 164 - marginRight: 24, 165 - }, 166 - itemBottom: { 167 - paddingTop: 8, 168 - paddingBottom: 12, 169 - marginRight: 24, 170 - }, 171 - indicator: { 172 - position: 'absolute', 173 - left: 0, 174 - width: 1, 175 - height: 3, 143 + paddingHorizontal: isMobileWeb ? 8 : 0, 144 + borderBottomWidth: 3, 145 + borderBottomColor: 'transparent', 176 146 }, 177 147 })
+12
src/view/com/posts/Feed.tsx
··· 18 18 import {s} from 'lib/styles' 19 19 import {useAnalytics} from 'lib/analytics' 20 20 import {usePalette} from 'lib/hooks/usePalette' 21 + import {useTheme} from 'lib/ThemeContext' 21 22 22 23 const LOADING_ITEM = {_reactKey: '__loading__'} 23 24 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} ··· 31 32 scrollElRef, 32 33 onPressTryAgain, 33 34 onScroll, 35 + scrollEventThrottle, 34 36 renderEmptyState, 35 37 testID, 36 38 headerOffset = 0, 39 + ListHeaderComponent, 40 + extraData, 37 41 }: { 38 42 feed: PostsFeedModel 39 43 style?: StyleProp<ViewStyle> ··· 41 45 scrollElRef?: MutableRefObject<FlatList<any> | null> 42 46 onPressTryAgain?: () => void 43 47 onScroll?: OnScrollCb 48 + scrollEventThrottle?: number 44 49 renderEmptyState?: () => JSX.Element 45 50 testID?: string 46 51 headerOffset?: number 52 + ListHeaderComponent?: () => JSX.Element 53 + extraData?: any 47 54 }) { 48 55 const pal = usePalette('default') 56 + const theme = useTheme() 49 57 const {track} = useAnalytics() 50 58 const [isRefreshing, setIsRefreshing] = React.useState(false) 51 59 ··· 163 171 keyExtractor={item => item._reactKey} 164 172 renderItem={renderItem} 165 173 ListFooterComponent={FeedFooter} 174 + ListHeaderComponent={ListHeaderComponent} 166 175 refreshControl={ 167 176 <RefreshControl 168 177 refreshing={isRefreshing} ··· 175 184 contentContainerStyle={s.contentContainer} 176 185 style={{paddingTop: headerOffset}} 177 186 onScroll={onScroll} 187 + scrollEventThrottle={scrollEventThrottle} 188 + indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} 178 189 onEndReached={onEndReached} 179 190 onEndReachedThreshold={0.6} 180 191 removeClippedSubviews={true} 181 192 contentOffset={{x: 0, y: headerOffset * -1}} 193 + extraData={extraData} 182 194 // @ts-ignore our .web version only -prf 183 195 desktopFixedHeight 184 196 />
+1 -1
src/view/com/posts/FeedSlice.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 - import {PostsFeedSliceModel} from 'state/models/feeds/posts' 3 + import {PostsFeedSliceModel} from 'state/models/feeds/post' 4 4 import {AtUri} from '@atproto/api' 5 5 import {Link} from '../util/Link' 6 6 import {Text} from '../util/text/Text'
+246
src/view/com/posts/MultiFeed.tsx
··· 1 + import React, {MutableRefObject} from 'react' 2 + import {observer} from 'mobx-react-lite' 3 + import { 4 + ActivityIndicator, 5 + RefreshControl, 6 + StyleProp, 7 + StyleSheet, 8 + View, 9 + ViewStyle, 10 + } from 'react-native' 11 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 12 + import {FlatList} from '../util/Views' 13 + import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' 14 + import {ErrorMessage} from '../util/error/ErrorMessage' 15 + import {PostsMultiFeedModel, MultiFeedItem} from 'state/models/feeds/multi-feed' 16 + import {FeedSlice} from './FeedSlice' 17 + import {Text} from '../util/text/Text' 18 + import {Link} from '../util/Link' 19 + import {UserAvatar} from '../util/UserAvatar' 20 + import {OnScrollCb} from 'lib/hooks/useOnMainScroll' 21 + import {s} from 'lib/styles' 22 + import {useAnalytics} from 'lib/analytics' 23 + import {usePalette} from 'lib/hooks/usePalette' 24 + import {useTheme} from 'lib/ThemeContext' 25 + import {isDesktopWeb} from 'platform/detection' 26 + import {CogIcon} from 'lib/icons' 27 + 28 + export const MultiFeed = observer(function Feed({ 29 + multifeed, 30 + style, 31 + showPostFollowBtn, 32 + scrollElRef, 33 + onScroll, 34 + scrollEventThrottle, 35 + testID, 36 + headerOffset = 0, 37 + extraData, 38 + }: { 39 + multifeed: PostsMultiFeedModel 40 + style?: StyleProp<ViewStyle> 41 + showPostFollowBtn?: boolean 42 + scrollElRef?: MutableRefObject<FlatList<any> | null> 43 + onPressTryAgain?: () => void 44 + onScroll?: OnScrollCb 45 + scrollEventThrottle?: number 46 + renderEmptyState?: () => JSX.Element 47 + testID?: string 48 + headerOffset?: number 49 + extraData?: any 50 + }) { 51 + const pal = usePalette('default') 52 + const theme = useTheme() 53 + const {track} = useAnalytics() 54 + const [isRefreshing, setIsRefreshing] = React.useState(false) 55 + 56 + // events 57 + // = 58 + 59 + const onRefresh = React.useCallback(async () => { 60 + track('MultiFeed:onRefresh') 61 + setIsRefreshing(true) 62 + try { 63 + await multifeed.refresh() 64 + } catch (err) { 65 + multifeed.rootStore.log.error('Failed to refresh posts feed', err) 66 + } 67 + setIsRefreshing(false) 68 + }, [multifeed, track, setIsRefreshing]) 69 + 70 + const onEndReached = React.useCallback(async () => { 71 + track('MultiFeed:onEndReached') 72 + try { 73 + await multifeed.loadMore() 74 + } catch (err) { 75 + multifeed.rootStore.log.error('Failed to load more posts', err) 76 + } 77 + }, [multifeed, track]) 78 + 79 + // rendering 80 + // = 81 + 82 + const renderItem = React.useCallback( 83 + ({item}: {item: MultiFeedItem}) => { 84 + if (item.type === 'header') { 85 + if (isDesktopWeb) { 86 + return ( 87 + <View style={[pal.view, pal.border, styles.headerDesktop]}> 88 + <Text type="2xl-bold" style={pal.text}> 89 + My Feeds 90 + </Text> 91 + <Link href="/settings/saved-feeds"> 92 + <CogIcon strokeWidth={1.5} style={pal.icon} size={28} /> 93 + </Link> 94 + </View> 95 + ) 96 + } 97 + return <View style={[styles.header, pal.border]} /> 98 + } else if (item.type === 'feed-header') { 99 + return ( 100 + <View style={styles.feedHeader}> 101 + <UserAvatar type="algo" avatar={item.avatar} size={28} /> 102 + <Text type="title-lg" style={[pal.text, styles.feedHeaderTitle]}> 103 + {item.title} 104 + </Text> 105 + </View> 106 + ) 107 + } else if (item.type === 'feed-slice') { 108 + return ( 109 + <FeedSlice slice={item.slice} showFollowBtn={showPostFollowBtn} /> 110 + ) 111 + } else if (item.type === 'feed-loading') { 112 + return <PostFeedLoadingPlaceholder /> 113 + } else if (item.type === 'feed-error') { 114 + return <ErrorMessage message={item.error} /> 115 + } else if (item.type === 'feed-footer') { 116 + return ( 117 + <Link 118 + href={item.uri} 119 + style={[styles.feedFooter, pal.border, pal.view]}> 120 + <Text type="lg" style={pal.link}> 121 + See more from {item.title} 122 + </Text> 123 + <FontAwesomeIcon 124 + icon="angle-right" 125 + size={18} 126 + color={pal.colors.link} 127 + /> 128 + </Link> 129 + ) 130 + } else if (item.type === 'footer') { 131 + return ( 132 + <Link style={[styles.footerLink, pal.viewLight]} href="/search/feeds"> 133 + <FontAwesomeIcon icon="search" size={18} color={pal.colors.text} /> 134 + <Text type="xl-medium" style={pal.text}> 135 + Discover new feeds 136 + </Text> 137 + </Link> 138 + ) 139 + } 140 + return null 141 + }, 142 + [showPostFollowBtn, pal], 143 + ) 144 + 145 + const FeedFooter = React.useCallback( 146 + () => 147 + multifeed.isLoading && !isRefreshing ? ( 148 + <View style={styles.loadMore}> 149 + <ActivityIndicator color={pal.colors.text} /> 150 + </View> 151 + ) : ( 152 + <View /> 153 + ), 154 + [multifeed.isLoading, isRefreshing, pal], 155 + ) 156 + 157 + return ( 158 + <View testID={testID} style={style}> 159 + {multifeed.items.length > 0 && ( 160 + <FlatList 161 + testID={testID ? `${testID}-flatlist` : undefined} 162 + ref={scrollElRef} 163 + data={multifeed.items} 164 + keyExtractor={item => item._reactKey} 165 + renderItem={renderItem} 166 + ListFooterComponent={FeedFooter} 167 + refreshControl={ 168 + <RefreshControl 169 + refreshing={isRefreshing} 170 + onRefresh={onRefresh} 171 + tintColor={pal.colors.text} 172 + titleColor={pal.colors.text} 173 + progressViewOffset={headerOffset} 174 + /> 175 + } 176 + contentContainerStyle={s.contentContainer} 177 + style={[{paddingTop: headerOffset}, pal.view, styles.container]} 178 + onScroll={onScroll} 179 + scrollEventThrottle={scrollEventThrottle} 180 + indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} 181 + onEndReached={onEndReached} 182 + onEndReachedThreshold={0.6} 183 + removeClippedSubviews={true} 184 + contentOffset={{x: 0, y: headerOffset * -1}} 185 + extraData={extraData} 186 + // @ts-ignore our .web version only -prf 187 + desktopFixedHeight 188 + /> 189 + )} 190 + </View> 191 + ) 192 + }) 193 + 194 + const styles = StyleSheet.create({ 195 + container: { 196 + height: '100%', 197 + }, 198 + header: { 199 + borderTopWidth: 1, 200 + marginBottom: 4, 201 + }, 202 + headerDesktop: { 203 + flexDirection: 'row', 204 + alignItems: 'center', 205 + justifyContent: 'space-between', 206 + borderBottomWidth: 1, 207 + marginBottom: 4, 208 + paddingHorizontal: 16, 209 + paddingVertical: 8, 210 + }, 211 + feedHeader: { 212 + flexDirection: 'row', 213 + gap: 8, 214 + alignItems: 'center', 215 + paddingHorizontal: 16, 216 + paddingBottom: 8, 217 + marginTop: 12, 218 + }, 219 + feedHeaderTitle: { 220 + fontWeight: 'bold', 221 + }, 222 + feedFooter: { 223 + flexDirection: 'row', 224 + justifyContent: 'space-between', 225 + alignItems: 'center', 226 + paddingHorizontal: 16, 227 + paddingVertical: 16, 228 + marginBottom: 12, 229 + borderTopWidth: 1, 230 + borderBottomWidth: 1, 231 + }, 232 + footerLink: { 233 + flexDirection: 'row', 234 + alignItems: 'center', 235 + justifyContent: 'center', 236 + borderRadius: 8, 237 + paddingHorizontal: 14, 238 + paddingVertical: 12, 239 + marginHorizontal: 8, 240 + marginBottom: 8, 241 + gap: 8, 242 + }, 243 + loadMore: { 244 + paddingTop: 10, 245 + }, 246 + })
+15 -10
src/view/com/posts/WhatsHotEmptyState.tsx src/view/com/posts/CustomFeedEmptyState.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 + import {useNavigation} from '@react-navigation/native' 3 4 import { 4 5 FontAwesomeIcon, 5 6 FontAwesomeIconStyle, ··· 7 8 import {Text} from '../util/text/Text' 8 9 import {Button} from '../util/forms/Button' 9 10 import {MagnifyingGlassIcon} from 'lib/icons' 10 - import {useStores} from 'state/index' 11 + import {NavigationProp} from 'lib/routes/types' 11 12 import {usePalette} from 'lib/hooks/usePalette' 12 13 import {s} from 'lib/styles' 13 14 14 - export function WhatsHotEmptyState() { 15 + export function CustomFeedEmptyState() { 15 16 const pal = usePalette('default') 16 17 const palInverted = usePalette('inverted') 17 - const store = useStores() 18 + const navigation = useNavigation<NavigationProp>() 18 19 19 - const onPressSettings = React.useCallback(() => { 20 - store.shell.openModal({name: 'content-languages-settings'}) 21 - }, [store]) 20 + const onPressFindAccounts = React.useCallback(() => { 21 + navigation.navigate('SearchTab') 22 + navigation.popToTop() 23 + }, [navigation]) 22 24 23 25 return ( 24 26 <View style={styles.emptyContainer}> ··· 26 28 <MagnifyingGlassIcon style={[styles.emptyIcon, pal.text]} size={62} /> 27 29 </View> 28 30 <Text type="xl-medium" style={[s.textCenter, pal.text]}> 29 - Your What's Hot feed is empty! This is because there aren't enough users 30 - posting in your selected language. 31 + This feed is empty! You may need to follow more users or tune your 32 + language settings. 31 33 </Text> 32 - <Button type="inverted" style={styles.emptyBtn} onPress={onPressSettings}> 34 + <Button 35 + type="inverted" 36 + style={styles.emptyBtn} 37 + onPress={onPressFindAccounts}> 33 38 <Text type="lg-medium" style={palInverted.text}> 34 - Update my settings 39 + Find accounts to follow 35 40 </Text> 36 41 <FontAwesomeIcon 37 42 icon="angle-right"
+8 -3
src/view/com/search/HeaderWithInput.tsx
··· 4 4 FontAwesomeIcon, 5 5 FontAwesomeIconStyle, 6 6 } from '@fortawesome/react-native-fontawesome' 7 - import {UserAvatar} from 'view/com/util/UserAvatar' 8 7 import {Text} from 'view/com/util/text/Text' 9 8 import {MagnifyingGlassIcon} from 'lib/icons' 10 9 import {useTheme} from 'lib/ThemeContext' ··· 58 57 accessibilityRole="button" 59 58 accessibilityLabel="Menu" 60 59 accessibilityHint="Access navigation links and settings"> 61 - <UserAvatar size={30} avatar={store.me.avatar} /> 60 + <FontAwesomeIcon icon="bars" size={18} color={pal.colors.textLight} /> 62 61 </TouchableOpacity> 63 62 <View 64 63 style={[ ··· 87 86 accessibilityRole="search" 88 87 accessibilityLabel="Search" 89 88 accessibilityHint="" 89 + autoCorrect={false} 90 + autoCapitalize="none" 90 91 /> 91 92 {query ? ( 92 93 <TouchableOpacity ··· 119 120 header: { 120 121 flexDirection: 'row', 121 122 alignItems: 'center', 123 + justifyContent: 'center', 122 124 paddingHorizontal: 12, 123 125 paddingVertical: 4, 124 126 }, ··· 126 128 width: 30, 127 129 height: 30, 128 130 borderRadius: 30, 129 - marginHorizontal: 6, 131 + marginRight: 6, 132 + paddingBottom: 2, 133 + alignItems: 'center', 134 + justifyContent: 'center', 130 135 }, 131 136 headerSearchContainer: { 132 137 flex: 1,
+68 -19
src/view/com/util/UserAvatar.tsx
··· 1 1 import React, {useMemo} from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 - import Svg, {Circle, Path} from 'react-native-svg' 3 + import Svg, {Circle, Rect, Path} from 'react-native-svg' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 5 import {IconProp} from '@fortawesome/fontawesome-svg-core' 6 6 import {HighPriorityImage} from 'view/com/util/images/Image' ··· 17 17 import {Image as RNImage} from 'react-native-image-crop-picker' 18 18 import {AvatarModeration} from 'lib/labeling/types' 19 19 20 + type Type = 'user' | 'algo' | 'list' 21 + 20 22 const BLUR_AMOUNT = isWeb ? 5 : 100 21 23 22 - function DefaultAvatar({size}: {size: number}) { 24 + function DefaultAvatar({type, size}: {type: Type; size: number}) { 25 + if (type === 'algo') { 26 + // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. 27 + return ( 28 + <Svg 29 + testID="userAvatarFallback" 30 + width={size} 31 + height={size} 32 + viewBox="0 0 32 32" 33 + fill="none" 34 + stroke="none"> 35 + <Rect width="32" height="32" rx="4" fill="#0070FF" /> 36 + <Path 37 + d="M13.5 7.25C13.5 6.55859 14.0586 6 14.75 6C20.9648 6 26 11.0352 26 17.25C26 17.9414 25.4414 18.5 24.75 18.5C24.0586 18.5 23.5 17.9414 23.5 17.25C23.5 12.418 19.582 8.5 14.75 8.5C14.0586 8.5 13.5 7.94141 13.5 7.25ZM8.36719 14.6172L12.4336 18.6836L13.543 17.5742C13.5156 17.4727 13.5 17.3633 13.5 17.25C13.5 16.5586 14.0586 16 14.75 16C15.4414 16 16 16.5586 16 17.25C16 17.9414 15.4414 18.5 14.75 18.5C14.6367 18.5 14.5312 18.4844 14.4258 18.457L13.3164 19.5664L17.3828 23.6328C17.9492 24.1992 17.8438 25.1484 17.0977 25.4414C16.1758 25.8008 15.1758 26 14.125 26C9.63672 26 6 22.3633 6 17.875C6 16.8242 6.19922 15.8242 6.5625 14.9023C6.85547 14.1602 7.80469 14.0508 8.37109 14.6172H8.36719ZM14.75 9.75C18.8906 9.75 22.25 13.1094 22.25 17.25C22.25 17.9414 21.6914 18.5 21 18.5C20.3086 18.5 19.75 17.9414 19.75 17.25C19.75 14.4883 17.5117 12.25 14.75 12.25C14.0586 12.25 13.5 11.6914 13.5 11C13.5 10.3086 14.0586 9.75 14.75 9.75Z" 38 + fill="white" 39 + /> 40 + </Svg> 41 + ) 42 + } 43 + if (type === 'list') { 44 + // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. 45 + return ( 46 + <Svg 47 + testID="userAvatarFallback" 48 + width={size} 49 + height={size} 50 + viewBox="0 0 32 32" 51 + fill="none" 52 + stroke="none"> 53 + <Path 54 + d="M28 0H4C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H28C30.2091 32 32 30.2091 32 28V4C32 1.79086 30.2091 0 28 0Z" 55 + fill="#0070FF" 56 + /> 57 + <Path 58 + d="M22.1529 22.3542C23.4522 22.4603 24.7593 22.293 25.9899 21.8629C26.0369 21.2838 25.919 20.7032 25.6497 20.1884C25.3805 19.6735 24.9711 19.2454 24.4687 18.9535C23.9663 18.6617 23.3916 18.518 22.8109 18.5392C22.2303 18.5603 21.6676 18.7454 21.1878 19.0731M22.1529 22.3542C22.1489 21.1917 21.8142 20.0534 21.1878 19.0741ZM10.8111 19.0741C10.3313 18.7468 9.7687 18.5619 9.18826 18.5409C8.60781 18.5199 8.03327 18.6636 7.53107 18.9554C7.02888 19.2472 6.61953 19.6752 6.35036 20.1899C6.08119 20.7046 5.96319 21.285 6.01001 21.8639C7.23969 22.2964 8.5461 22.4632 9.84497 22.3531M10.8111 19.0741C10.1851 20.0535 9.84865 21.1908 9.84497 22.3531ZM19.0759 10.077C19.0759 10.8931 18.7518 11.6757 18.1747 12.2527C17.5977 12.8298 16.815 13.154 15.9989 13.154C15.1829 13.154 14.4002 12.8298 13.8232 12.2527C13.2461 11.6757 12.922 10.8931 12.922 10.077C12.922 9.26092 13.2461 8.47828 13.8232 7.90123C14.4002 7.32418 15.1829 7 15.9989 7C16.815 7 17.5977 7.32418 18.1747 7.90123C18.7518 8.47828 19.0759 9.26092 19.0759 10.077ZM25.2299 13.154C25.2299 13.457 25.1702 13.7571 25.0542 14.0371C24.9383 14.3171 24.7683 14.5715 24.554 14.7858C24.3397 15.0001 24.0853 15.1701 23.8053 15.2861C23.5253 15.402 23.2252 15.4617 22.9222 15.4617C22.6191 15.4617 22.319 15.402 22.039 15.2861C21.759 15.1701 21.5046 15.0001 21.2903 14.7858C21.0761 14.5715 20.9061 14.3171 20.7901 14.0371C20.6741 13.7571 20.6144 13.457 20.6144 13.154C20.6144 12.5419 20.8576 11.9549 21.2903 11.5222C21.7231 11.0894 22.3101 10.8462 22.9222 10.8462C23.5342 10.8462 24.1212 11.0894 24.554 11.5222C24.9868 11.9549 25.2299 12.5419 25.2299 13.154ZM11.3835 13.154C11.3835 13.457 11.3238 13.7571 11.2078 14.0371C11.0918 14.3171 10.9218 14.5715 10.7075 14.7858C10.4932 15.0001 10.2388 15.1701 9.95886 15.2861C9.67887 15.402 9.37878 15.4617 9.07572 15.4617C8.77266 15.4617 8.47257 15.402 8.19259 15.2861C7.9126 15.1701 7.6582 15.0001 7.4439 14.7858C7.22961 14.5715 7.05962 14.3171 6.94365 14.0371C6.82767 13.7571 6.76798 13.457 6.76798 13.154C6.76798 12.5419 7.01112 11.9549 7.4439 11.5222C7.87669 11.0894 8.46367 10.8462 9.07572 10.8462C9.68777 10.8462 10.2748 11.0894 10.7075 11.5222C11.1403 11.9549 11.3835 12.5419 11.3835 13.154Z" 59 + fill="white" 60 + /> 61 + <Path 62 + d="M22 22C22 25.3137 19.3137 25.5 16 25.5C12.6863 25.5 10 25.3137 10 22C10 18.6863 12.6863 16 16 16C19.3137 16 22 18.6863 22 22Z" 63 + fill="white" 64 + /> 65 + </Svg> 66 + ) 67 + } 23 68 return ( 24 69 <Svg 25 70 testID="userAvatarFallback" ··· 41 86 } 42 87 43 88 export function UserAvatar({ 89 + type = 'user', 44 90 size, 45 91 avatar, 46 92 moderation, 47 93 onSelectNewAvatar, 48 94 }: { 95 + type?: Type 49 96 size: number 50 97 avatar?: string | null 51 98 moderation?: AvatarModeration ··· 55 102 const pal = usePalette('default') 56 103 const {requestCameraAccessIfNeeded} = useCameraPermission() 57 104 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 105 + 106 + const aviStyle = useMemo(() => { 107 + if (type === 'algo' || type === 'list') { 108 + return { 109 + width: size, 110 + height: size, 111 + borderRadius: 8, 112 + } 113 + } 114 + return { 115 + width: size, 116 + height: size, 117 + borderRadius: Math.floor(size / 2), 118 + } 119 + }, [type, size]) 58 120 59 121 const dropdownItems = useMemo( 60 122 () => [ ··· 146 208 {avatar ? ( 147 209 <HighPriorityImage 148 210 testID="userAvatarImage" 149 - style={{ 150 - width: size, 151 - height: size, 152 - borderRadius: Math.floor(size / 2), 153 - }} 211 + style={aviStyle} 154 212 source={{uri: avatar}} 155 213 accessibilityRole="image" 156 214 /> 157 215 ) : ( 158 - <DefaultAvatar size={size} /> 216 + <DefaultAvatar type={type} size={size} /> 159 217 )} 160 218 <View style={[styles.editButtonContainer, pal.btn]}> 161 219 <FontAwesomeIcon ··· 170 228 <View style={{width: size, height: size}}> 171 229 <HighPriorityImage 172 230 testID="userAvatarImage" 173 - style={{ 174 - width: size, 175 - height: size, 176 - borderRadius: Math.floor(size / 2), 177 - }} 231 + style={aviStyle} 178 232 contentFit="cover" 179 233 source={{uri: avatar}} 180 234 blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} ··· 183 237 </View> 184 238 ) : ( 185 239 <View style={{width: size, height: size}}> 186 - <DefaultAvatar size={size} /> 240 + <DefaultAvatar type={type} size={size} /> 187 241 {warning} 188 242 </View> 189 243 ) ··· 200 254 alignItems: 'center', 201 255 justifyContent: 'center', 202 256 backgroundColor: colors.gray5, 203 - }, 204 - avatarImage: { 205 - width: 80, 206 - height: 80, 207 - borderRadius: 40, 208 257 }, 209 258 warningIconContainer: { 210 259 position: 'absolute',
+32 -5
src/view/com/util/ViewHeader.tsx
··· 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 5 import {useNavigation} from '@react-navigation/native' 6 6 import {CenteredView} from './Views' 7 - import {UserAvatar} from './UserAvatar' 8 7 import {Text} from './text/Text' 9 8 import {useStores} from 'state/index' 10 9 import {usePalette} from 'lib/hooks/usePalette' ··· 20 19 canGoBack, 21 20 hideOnScroll, 22 21 showOnDesktop, 22 + showBorder, 23 23 renderButton, 24 24 }: { 25 25 title: string 26 26 canGoBack?: boolean 27 27 hideOnScroll?: boolean 28 28 showOnDesktop?: boolean 29 + showBorder?: boolean 29 30 renderButton?: () => JSX.Element 30 31 }) { 31 32 const pal = usePalette('default') ··· 57 58 } 58 59 59 60 return ( 60 - <Container hideOnScroll={hideOnScroll || false}> 61 + <Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}> 61 62 <TouchableOpacity 62 63 testID="viewHeaderDrawerBtn" 63 64 onPress={canGoBack ? onPressBack : onPressMenu} ··· 75 76 style={[styles.backIcon, pal.text]} 76 77 /> 77 78 ) : ( 78 - <UserAvatar size={30} avatar={store.me.avatar} /> 79 + <FontAwesomeIcon 80 + size={18} 81 + icon="bars" 82 + style={[styles.backIcon, pal.textLight]} 83 + /> 79 84 )} 80 85 </TouchableOpacity> 81 86 <View style={styles.titleContainer} pointerEvents="none"> ··· 117 122 ({ 118 123 children, 119 124 hideOnScroll, 125 + showBorder, 120 126 }: { 121 127 children: React.ReactNode 122 128 hideOnScroll: boolean 129 + showBorder?: boolean 123 130 }) => { 124 131 const store = useStores() 125 132 const pal = usePalette('default') ··· 147 154 } 148 155 149 156 if (!hideOnScroll) { 150 - return <View style={[styles.header, pal.view]}>{children}</View> 157 + return ( 158 + <View 159 + style={[ 160 + styles.header, 161 + pal.view, 162 + pal.border, 163 + showBorder && styles.border, 164 + ]}> 165 + {children} 166 + </View> 167 + ) 151 168 } 152 169 return ( 153 170 <Animated.View 154 - style={[styles.header, pal.view, styles.headerFloating, transform]}> 171 + style={[ 172 + styles.header, 173 + pal.view, 174 + pal.border, 175 + styles.headerFloating, 176 + transform, 177 + showBorder && styles.border, 178 + ]}> 155 179 {children} 156 180 </Animated.View> 157 181 ) ··· 173 197 desktopHeader: { 174 198 borderBottomWidth: 1, 175 199 paddingVertical: 12, 200 + }, 201 + border: { 202 + borderBottomWidth: 1, 176 203 }, 177 204 178 205 titleContainer: {
+9 -3
src/view/com/util/ViewSelector.tsx
··· 1 1 import React, {useEffect, useState} from 'react' 2 - import {Pressable, StyleSheet, View} from 'react-native' 2 + import {Pressable, RefreshControl, StyleSheet, View} from 'react-native' 3 3 import {FlatList} from './Views' 4 4 import {OnScrollCb} from 'lib/hooks/useOnMainScroll' 5 5 import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' ··· 41 41 onRefresh?: () => void 42 42 onEndReached?: (info: {distanceFromEnd: number}) => void 43 43 }) { 44 + const pal = usePalette('default') 44 45 const [selectedIndex, setSelectedIndex] = useState<number>(0) 45 46 46 47 // events ··· 93 94 ListFooterComponent={ListFooterComponent} 94 95 // NOTE sticky header disabled on android due to major performance issues -prf 95 96 stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES} 96 - refreshing={refreshing} 97 97 onScroll={onScroll} 98 - onRefresh={onRefresh} 99 98 onEndReached={onEndReached} 99 + refreshControl={ 100 + <RefreshControl 101 + refreshing={refreshing!} 102 + onRefresh={onRefresh} 103 + tintColor={pal.colors.text} 104 + /> 105 + } 100 106 onEndReachedThreshold={0.6} 101 107 contentContainerStyle={s.contentContainer} 102 108 removeClippedSubviews={true}
+1
src/view/com/util/Views.web.tsx
··· 126 126 }, 127 127 fixedHeight: { 128 128 height: '100vh', 129 + scrollbarGutter: 'stable both-edges', 129 130 }, 130 131 })
+1 -1
src/view/com/util/fab/FABInner.tsx
··· 47 47 outer: { 48 48 position: 'absolute', 49 49 zIndex: 1, 50 - right: 28, 50 + right: 24, 51 51 bottom: 94, 52 52 width: 60, 53 53 height: 60,
+6 -1
src/view/com/util/forms/DropdownButton.tsx
··· 136 136 } 137 137 return ( 138 138 <View ref={ref2}> 139 - <Button testID={testID} onPress={onPress} style={style} label={label}> 139 + <Button 140 + type={type} 141 + testID={testID} 142 + onPress={onPress} 143 + style={style} 144 + label={label}> 140 145 {children} 141 146 </Button> 142 147 </View>
+70 -24
src/view/com/util/load-latest/LoadLatestBtn.web.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TouchableOpacity} from 'react-native' 3 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 3 4 import {Text} from '../text/Text' 4 5 import {usePalette} from 'lib/hooks/usePalette' 5 - import {UpIcon} from 'lib/icons' 6 6 import {LoadLatestBtn as LoadLatestBtnMobile} from './LoadLatestBtnMobile' 7 7 import {isMobileWeb} from 'platform/detection' 8 8 ··· 11 11 export const LoadLatestBtn = ({ 12 12 onPress, 13 13 label, 14 + showIndicator, 15 + minimalShellMode, 14 16 }: { 15 17 onPress: () => void 16 18 label: string 19 + showIndicator: boolean 20 + minimalShellMode?: boolean 17 21 }) => { 18 22 const pal = usePalette('default') 19 23 if (isMobileWeb) { 20 - return <LoadLatestBtnMobile onPress={onPress} label={label} /> 24 + return ( 25 + <LoadLatestBtnMobile 26 + onPress={onPress} 27 + label={label} 28 + showIndicator={showIndicator} 29 + /> 30 + ) 21 31 } 22 32 return ( 23 - <TouchableOpacity 24 - style={[pal.view, pal.borderDark, styles.loadLatest]} 25 - onPress={onPress} 26 - hitSlop={HITSLOP} 27 - accessibilityRole="button" 28 - accessibilityLabel={`Load new ${label}`} 29 - accessibilityHint=""> 30 - <Text type="md-bold" style={pal.text}> 31 - <UpIcon size={16} strokeWidth={1} style={[pal.text, styles.icon]} /> 32 - Load new {label} 33 - </Text> 34 - </TouchableOpacity> 33 + <> 34 + {showIndicator && ( 35 + <TouchableOpacity 36 + style={[ 37 + pal.view, 38 + pal.borderDark, 39 + styles.loadLatestCentered, 40 + minimalShellMode && styles.loadLatestCenteredMinimal, 41 + ]} 42 + onPress={onPress} 43 + hitSlop={HITSLOP} 44 + accessibilityRole="button" 45 + accessibilityLabel={label} 46 + accessibilityHint=""> 47 + <Text type="md-bold" style={pal.text}> 48 + {label} 49 + </Text> 50 + </TouchableOpacity> 51 + )} 52 + <TouchableOpacity 53 + style={[pal.view, pal.borderDark, styles.loadLatest]} 54 + onPress={onPress} 55 + hitSlop={HITSLOP} 56 + accessibilityRole="button" 57 + accessibilityLabel={label} 58 + accessibilityHint=""> 59 + <Text type="md-bold" style={pal.text}> 60 + <FontAwesomeIcon 61 + icon="angle-up" 62 + size={21} 63 + style={[pal.text, styles.icon]} 64 + /> 65 + </Text> 66 + </TouchableOpacity> 67 + </> 35 68 ) 36 69 } 37 70 38 71 const styles = StyleSheet.create({ 39 72 loadLatest: { 40 73 flexDirection: 'row', 74 + alignItems: 'center', 75 + justifyContent: 'center', 41 76 position: 'absolute', 42 77 left: '50vw', 43 78 // @ts-ignore web only -prf 44 - transform: 'translateX(-50%)', 45 - top: 60, 46 - shadowColor: '#000', 47 - shadowOpacity: 0.2, 48 - shadowOffset: {width: 0, height: 2}, 49 - shadowRadius: 4, 50 - paddingLeft: 20, 51 - paddingRight: 24, 52 - paddingVertical: 10, 79 + transform: 'translateX(-282px)', 80 + bottom: 40, 81 + width: 54, 82 + height: 54, 53 83 borderRadius: 30, 54 84 borderWidth: 1, 55 85 }, 56 86 icon: { 57 87 position: 'relative', 58 88 top: 2, 59 - marginRight: 5, 89 + }, 90 + loadLatestCentered: { 91 + flexDirection: 'row', 92 + alignItems: 'center', 93 + justifyContent: 'center', 94 + position: 'absolute', 95 + left: '50vw', 96 + // @ts-ignore web only -prf 97 + transform: 'translateX(-50%)', 98 + top: 60, 99 + paddingHorizontal: 24, 100 + paddingVertical: 14, 101 + borderRadius: 30, 102 + borderWidth: 1, 103 + }, 104 + loadLatestCenteredMinimal: { 105 + top: 20, 60 106 }, 61 107 })
+37 -27
src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, TouchableOpacity} from 'react-native' 2 + import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import {observer} from 'mobx-react-lite' 4 - import LinearGradient from 'react-native-linear-gradient' 4 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 5 import {useSafeAreaInsets} from 'react-native-safe-area-context' 6 - import {Text} from '../text/Text' 7 - import {colors, gradients} from 'lib/styles' 8 6 import {clamp} from 'lodash' 9 7 import {useStores} from 'state/index' 8 + import {usePalette} from 'lib/hooks/usePalette' 9 + import {colors} from 'lib/styles' 10 10 11 11 const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20} 12 12 13 13 export const LoadLatestBtn = observer( 14 - ({onPress, label}: {onPress: () => void; label: string}) => { 14 + ({ 15 + onPress, 16 + label, 17 + showIndicator, 18 + }: { 19 + onPress: () => void 20 + label: string 21 + showIndicator: boolean 22 + minimalShellMode?: boolean // NOTE not used on mobile -prf 23 + }) => { 15 24 const store = useStores() 25 + const pal = usePalette('default') 16 26 const safeAreaInsets = useSafeAreaInsets() 17 27 return ( 18 28 <TouchableOpacity 19 29 style={[ 20 30 styles.loadLatest, 31 + pal.borderDark, 32 + pal.view, 21 33 !store.shell.minimalShellMode && { 22 34 bottom: 60 + clamp(safeAreaInsets.bottom, 15, 30), 23 35 }, ··· 25 37 onPress={onPress} 26 38 hitSlop={HITSLOP} 27 39 accessibilityRole="button" 28 - accessibilityLabel={`Load new ${label}`} 29 - accessibilityHint={`Loads new ${label}`}> 30 - <LinearGradient 31 - colors={[gradients.blueLight.start, gradients.blueLight.end]} 32 - start={{x: 0, y: 0}} 33 - end={{x: 1, y: 1}} 34 - style={styles.loadLatestInner}> 35 - <Text type="md-bold" style={styles.loadLatestText}> 36 - Load new {label} 37 - </Text> 38 - </LinearGradient> 40 + accessibilityLabel={label} 41 + accessibilityHint=""> 42 + <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} /> 43 + {showIndicator && <View style={[styles.indicator, pal.borderDark]} />} 39 44 </TouchableOpacity> 40 45 ) 41 46 }, ··· 44 49 const styles = StyleSheet.create({ 45 50 loadLatest: { 46 51 position: 'absolute', 47 - left: 20, 52 + left: 18, 48 53 bottom: 35, 49 - shadowColor: '#000', 50 - shadowOpacity: 0.3, 51 - shadowOffset: {width: 0, height: 1}, 52 - }, 53 - loadLatestInner: { 54 + borderWidth: 1, 55 + width: 52, 56 + height: 52, 57 + borderRadius: 26, 54 58 flexDirection: 'row', 55 - paddingHorizontal: 14, 56 - paddingVertical: 10, 57 - borderRadius: 30, 59 + alignItems: 'center', 60 + justifyContent: 'center', 58 61 }, 59 - loadLatestText: { 60 - color: colors.white, 62 + indicator: { 63 + position: 'absolute', 64 + top: 3, 65 + right: 3, 66 + backgroundColor: colors.blue3, 67 + width: 12, 68 + height: 12, 69 + borderRadius: 6, 70 + borderWidth: 1, 61 71 }, 62 72 })
+4 -4
src/view/com/util/moderation/ImageHider.tsx
··· 27 27 setOverride(false) 28 28 }, [setOverride]) 29 29 30 + if (moderation.behavior === ModerationBehaviorCode.Hide) { 31 + return null 32 + } 33 + 30 34 if (moderation.behavior !== ModerationBehaviorCode.WarnImages) { 31 35 return ( 32 36 <View testID={testID} style={style}> 33 37 {children} 34 38 </View> 35 39 ) 36 - } 37 - 38 - if (moderation.behavior === ModerationBehaviorCode.Hide) { 39 - return null 40 40 } 41 41 42 42 return (
+10
src/view/com/util/numeric/format.ts
··· 3 3 notation: 'compact', 4 4 maximumFractionDigits: 1, 5 5 }).format(num) 6 + 7 + export function formatCountShortOnly(num: number): string { 8 + if (num >= 1000000) { 9 + return (num / 1000000).toFixed(1) + 'M' 10 + } 11 + if (num >= 1000) { 12 + return (num / 1000).toFixed(1) + 'K' 13 + } 14 + return String(num) 15 + }
+6 -15
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 10 10 FontAwesomeIcon, 11 11 FontAwesomeIconStyle, 12 12 } from '@fortawesome/react-native-fontawesome' 13 - import ReactNativeHapticFeedback, { 14 - HapticFeedbackTypes, 15 - } from 'react-native-haptic-feedback' 16 13 // DISABLED see #135 17 14 // import { 18 15 // TriggerableAnimated, ··· 24 21 import {s, colors} from 'lib/styles' 25 22 import {useTheme} from 'lib/ThemeContext' 26 23 import {useStores} from 'state/index' 27 - import {isIOS, isNative} from 'platform/detection' 28 24 import {RepostButton} from './RepostButton' 25 + import {Haptics} from 'lib/haptics' 29 26 30 27 interface PostCtrlsOpts { 31 28 itemUri: string ··· 58 55 } 59 56 60 57 const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5} 61 - const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537 62 58 63 59 // DISABLED see #135 64 60 /* ··· 111 107 const onRepost = useCallback(() => { 112 108 store.shell.closeModal() 113 109 if (!opts.isReposted) { 114 - if (isNative) { 115 - ReactNativeHapticFeedback.trigger(hapticImpact) 116 - } 110 + Haptics.default() 117 111 opts.onPressToggleRepost().catch(_e => undefined) 118 112 // DISABLED see #135 119 113 // repostRef.current?.trigger( ··· 139 133 indexedAt: opts.indexedAt, 140 134 }, 141 135 }) 142 - 143 - if (isNative) { 144 - ReactNativeHapticFeedback.trigger(hapticImpact) 145 - } 136 + Haptics.default() 146 137 }, [ 147 138 opts.author, 148 139 opts.indexedAt, ··· 154 145 155 146 const onPressToggleLikeWrapper = async () => { 156 147 if (!opts.isLiked) { 157 - ReactNativeHapticFeedback.trigger(hapticImpact) 148 + Haptics.default() 158 149 await opts.onPressToggleLike().catch(_e => undefined) 159 150 // DISABLED see #135 160 151 // likeRef.current?.trigger( ··· 201 192 accessibilityRole="button" 202 193 accessibilityLabel={opts.isLiked ? 'Unlike' : 'Like'} 203 194 accessibilityHint={ 204 - opts.isReposted ? `Removes like from the post` : `Like the post` 195 + opts.isReposted ? 'Removes like from the post' : 'Like the post' 205 196 }> 206 197 {opts.isLiked ? ( 207 198 <HeartIconSolid 208 - style={styles.ctrlIconLiked as StyleProp<ViewStyle>} 199 + style={styles.ctrlIconLiked} 209 200 size={opts.big ? 22 : 16} 210 201 /> 211 202 ) : (
+44 -1
src/view/com/util/post-embeds/index.tsx
··· 13 13 AppBskyEmbedRecord, 14 14 AppBskyEmbedRecordWithMedia, 15 15 AppBskyFeedPost, 16 + AppBskyFeedDefs, 16 17 } from '@atproto/api' 17 18 import {Link} from '../Link' 18 19 import {ImageLayoutGrid} from '../images/ImageLayoutGrid' ··· 24 25 import {getYoutubeVideoId} from 'lib/strings/url-helpers' 25 26 import QuoteEmbed from './QuoteEmbed' 26 27 import {AutoSizedImage} from '../images/AutoSizedImage' 28 + import {CustomFeed} from 'view/com/feeds/CustomFeed' 29 + import {CustomFeedModel} from 'state/models/feeds/custom-feed' 27 30 28 31 type Embed = 29 32 | AppBskyEmbedRecord.View ··· 42 45 const pal = usePalette('default') 43 46 const store = useStores() 44 47 48 + // quote post with media 49 + // = 45 50 if ( 46 51 AppBskyEmbedRecordWithMedia.isView(embed) && 47 52 AppBskyEmbedRecord.isViewRecord(embed.record.record) && ··· 65 70 ) 66 71 } 67 72 73 + // quote post 74 + // = 68 75 if (AppBskyEmbedRecord.isView(embed)) { 69 76 if ( 70 77 AppBskyEmbedRecord.isViewRecord(embed.record) && ··· 87 94 } 88 95 } 89 96 97 + // image embed 98 + // = 90 99 if (AppBskyEmbedImages.isView(embed)) { 91 100 const {images} = embed 92 101 ··· 132 141 /> 133 142 </View> 134 143 ) 135 - // } 136 144 } 137 145 } 138 146 147 + // external link embed 148 + // = 139 149 if (AppBskyEmbedExternal.isView(embed)) { 140 150 const link = embed.external 141 151 const youtubeVideoId = getYoutubeVideoId(link.uri) ··· 153 163 </Link> 154 164 ) 155 165 } 166 + 167 + // custom feed embed (i.e. generator view) 168 + // = 169 + if ( 170 + AppBskyEmbedRecord.isView(embed) && 171 + AppBskyFeedDefs.isGeneratorView(embed.record) 172 + ) { 173 + return <CustomFeedEmbed record={embed.record} /> 174 + } 175 + 156 176 return <View /> 157 177 } 158 178 179 + function CustomFeedEmbed({record}: {record: AppBskyFeedDefs.GeneratorView}) { 180 + const pal = usePalette('default') 181 + const store = useStores() 182 + const item = React.useMemo( 183 + () => new CustomFeedModel(store, record), 184 + [store, record], 185 + ) 186 + return ( 187 + <CustomFeed 188 + item={item} 189 + style={[pal.view, pal.border, styles.customFeedOuter]} 190 + showLikes 191 + /> 192 + ) 193 + } 194 + 159 195 const styles = StyleSheet.create({ 160 196 stackContainer: { 161 197 gap: 6, ··· 171 207 borderWidth: 1, 172 208 borderRadius: 8, 173 209 marginTop: 4, 210 + }, 211 + customFeedOuter: { 212 + borderWidth: 1, 213 + borderRadius: 8, 214 + marginTop: 4, 215 + paddingHorizontal: 12, 216 + paddingVertical: 12, 174 217 }, 175 218 alt: { 176 219 backgroundColor: 'rgba(0, 0, 0, 0.75)',
+11 -5
src/view/index.ts
··· 8 8 import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft' 9 9 import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight' 10 10 import {faArrowUp} from '@fortawesome/free-solid-svg-icons/faArrowUp' 11 + import {faArrowDown} from '@fortawesome/free-solid-svg-icons/faArrowDown' 11 12 import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowRightFromBracket' 12 13 import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket' 13 14 import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare' ··· 59 60 import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare' 60 61 import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' 61 62 import {faQuoteLeft} from '@fortawesome/free-solid-svg-icons/faQuoteLeft' 63 + import {faReply} from '@fortawesome/free-solid-svg-icons/faReply' 64 + import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet' 65 + import {faRss} from '@fortawesome/free-solid-svg-icons/faRss' 66 + import {faSatelliteDish} from '@fortawesome/free-solid-svg-icons/faSatelliteDish' 62 67 import {faShare} from '@fortawesome/free-solid-svg-icons/faShare' 63 68 import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare' 64 69 import {faShield} from '@fortawesome/free-solid-svg-icons/faShield' 65 70 import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus' 66 71 import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal' 67 - import {faReply} from '@fortawesome/free-solid-svg-icons/faReply' 68 - import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet' 69 - import {faRss} from '@fortawesome/free-solid-svg-icons/faRss' 72 + import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' 73 + import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' 70 74 import {faUser} from '@fortawesome/free-regular-svg-icons/faUser' 71 75 import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers' 72 76 import {faUserCheck} from '@fortawesome/free-solid-svg-icons/faUserCheck' ··· 74 78 import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus' 75 79 import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark' 76 80 import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash' 77 - import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' 78 - import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' 79 81 import {faX} from '@fortawesome/free-solid-svg-icons/faX' 80 82 import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' 81 83 import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' 82 84 import {faPause} from '@fortawesome/free-solid-svg-icons/faPause' 85 + import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack' 83 86 84 87 export function setup() { 85 88 library.add( ··· 91 94 faArrowLeft, 92 95 faArrowRight, 93 96 faArrowUp, 97 + faArrowDown, 94 98 faArrowRightFromBracket, 95 99 faArrowUpFromBracket, 96 100 faArrowUpRightFromSquare, ··· 145 149 faReply, 146 150 faRetweet, 147 151 faRss, 152 + faSatelliteDish, 148 153 faShare, 149 154 faShareFromSquare, 150 155 faShield, ··· 159 164 faUsersSlash, 160 165 faTicket, 161 166 faTrashCan, 167 + faThumbtack, 162 168 faX, 163 169 faXmark, 164 170 faPlay,
+418
src/view/screens/CustomFeed.tsx
··· 1 + import React, {useMemo, useRef} from 'react' 2 + import {NativeStackScreenProps} from '@react-navigation/native-stack' 3 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 + import {usePalette} from 'lib/hooks/usePalette' 5 + import {HeartIcon, HeartIconSolid} from 'lib/icons' 6 + import {CommonNavigatorParams} from 'lib/routes/types' 7 + import {makeRecordUri} from 'lib/strings/url-helpers' 8 + import {colors, s} from 'lib/styles' 9 + import {observer} from 'mobx-react-lite' 10 + import {FlatList, StyleSheet, View} from 'react-native' 11 + import {useStores} from 'state/index' 12 + import {PostsFeedModel} from 'state/models/feeds/posts' 13 + import {useCustomFeed} from 'lib/hooks/useCustomFeed' 14 + import {withAuthRequired} from 'view/com/auth/withAuthRequired' 15 + import {Feed} from 'view/com/posts/Feed' 16 + import {pluralize} from 'lib/strings/helpers' 17 + import {TextLink} from 'view/com/util/Link' 18 + import {UserAvatar} from 'view/com/util/UserAvatar' 19 + import {ViewHeader} from 'view/com/util/ViewHeader' 20 + import {Button} from 'view/com/util/forms/Button' 21 + import {Text} from 'view/com/util/text/Text' 22 + import * as Toast from 'view/com/util/Toast' 23 + import {isDesktopWeb} from 'platform/detection' 24 + import {useSetTitle} from 'lib/hooks/useSetTitle' 25 + import {shareUrl} from 'lib/sharing' 26 + import {toShareUrl} from 'lib/strings/url-helpers' 27 + import {Haptics} from 'lib/haptics' 28 + import {ComposeIcon2} from 'lib/icons' 29 + import {FAB} from '../com/util/fab/FAB' 30 + import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 31 + import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton' 32 + import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' 33 + import {EmptyState} from 'view/com/util/EmptyState' 34 + 35 + type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'> 36 + export const CustomFeedScreen = withAuthRequired( 37 + observer(({route}: Props) => { 38 + const store = useStores() 39 + const pal = usePalette('default') 40 + const {rkey, name} = route.params 41 + const uri = useMemo( 42 + () => makeRecordUri(name, 'app.bsky.feed.generator', rkey), 43 + [rkey, name], 44 + ) 45 + const scrollElRef = useRef<FlatList>(null) 46 + const currentFeed = useCustomFeed(uri) 47 + const algoFeed: PostsFeedModel = useMemo(() => { 48 + const feed = new PostsFeedModel(store, 'custom', { 49 + feed: uri, 50 + }) 51 + feed.setup() 52 + return feed 53 + }, [store, uri]) 54 + const isPinned = store.me.savedFeeds.isPinned(uri) 55 + const [onMainScroll, isScrolledDown, resetMainScroll] = 56 + useOnMainScroll(store) 57 + useSetTitle(currentFeed?.displayName) 58 + 59 + const onToggleSaved = React.useCallback(async () => { 60 + try { 61 + Haptics.default() 62 + if (currentFeed?.isSaved) { 63 + await currentFeed?.unsave() 64 + } else { 65 + await currentFeed?.save() 66 + } 67 + } catch (err) { 68 + Toast.show( 69 + 'There was an an issue updating your feeds, please check your internet connection and try again.', 70 + ) 71 + store.log.error('Failed up update feeds', {err}) 72 + } 73 + }, [store, currentFeed]) 74 + 75 + const onToggleLiked = React.useCallback(async () => { 76 + Haptics.default() 77 + try { 78 + if (currentFeed?.isLiked) { 79 + await currentFeed?.unlike() 80 + } else { 81 + await currentFeed?.like() 82 + } 83 + } catch (err) { 84 + Toast.show( 85 + 'There was an an issue contacting the server, please check your internet connection and try again.', 86 + ) 87 + store.log.error('Failed up toggle like', {err}) 88 + } 89 + }, [store, currentFeed]) 90 + 91 + const onTogglePinned = React.useCallback(async () => { 92 + Haptics.default() 93 + store.me.savedFeeds.togglePinnedFeed(currentFeed!).catch(e => { 94 + Toast.show('There was an issue contacting the server') 95 + store.log.error('Failed to toggle pinned feed', {e}) 96 + }) 97 + }, [store, currentFeed]) 98 + 99 + const onPressShare = React.useCallback(() => { 100 + const url = toShareUrl(`/profile/${name}/feed/${rkey}`) 101 + shareUrl(url) 102 + }, [name, rkey]) 103 + 104 + const onScrollToTop = React.useCallback(() => { 105 + scrollElRef.current?.scrollToOffset({offset: 0, animated: true}) 106 + resetMainScroll() 107 + }, [scrollElRef, resetMainScroll]) 108 + 109 + const onPressCompose = React.useCallback(() => { 110 + store.shell.openComposer({}) 111 + }, [store]) 112 + 113 + const dropdownItems: DropdownItem[] = React.useMemo(() => { 114 + let items: DropdownItem[] = [ 115 + { 116 + testID: 'feedHeaderDropdownRemoveBtn', 117 + label: 'Remove from my feeds', 118 + onPress: onToggleSaved, 119 + }, 120 + { 121 + testID: 'feedHeaderDropdownShareBtn', 122 + label: 'Share link', 123 + onPress: onPressShare, 124 + }, 125 + ] 126 + return items 127 + }, [onToggleSaved, onPressShare]) 128 + 129 + const renderHeaderBtns = React.useCallback(() => { 130 + return ( 131 + <View style={styles.headerBtns}> 132 + <Button 133 + type="default-light" 134 + testID="toggleLikeBtn" 135 + accessibilityLabel="Like this feed" 136 + accessibilityHint="" 137 + onPress={onToggleLiked}> 138 + {currentFeed?.isLiked ? ( 139 + <HeartIconSolid size={19} style={styles.liked} /> 140 + ) : ( 141 + <HeartIcon strokeWidth={3} size={19} style={pal.textLight} /> 142 + )} 143 + </Button> 144 + {currentFeed?.isSaved ? ( 145 + <Button 146 + type="default-light" 147 + accessibilityLabel={ 148 + isPinned ? 'Unpin this feed' : 'Pin this feed' 149 + } 150 + accessibilityHint="" 151 + onPress={onTogglePinned}> 152 + <FontAwesomeIcon 153 + icon="thumb-tack" 154 + size={17} 155 + color={isPinned ? colors.blue3 : pal.colors.textLight} 156 + style={styles.top1} 157 + /> 158 + </Button> 159 + ) : undefined} 160 + {currentFeed?.isSaved ? ( 161 + <DropdownButton 162 + testID="feedHeaderDropdownBtn" 163 + type="default-light" 164 + items={dropdownItems} 165 + menuWidth={250}> 166 + <FontAwesomeIcon 167 + icon="ellipsis" 168 + color={pal.colors.textLight} 169 + size={18} 170 + /> 171 + </DropdownButton> 172 + ) : ( 173 + <Button 174 + type="default-light" 175 + onPress={onToggleSaved} 176 + accessibilityLabel="Add to my feeds" 177 + accessibilityHint="" 178 + style={styles.headerAddBtn}> 179 + <FontAwesomeIcon icon="plus" color={pal.colors.link} size={19} /> 180 + <Text type="xl-medium" style={pal.link}> 181 + Add to My Feeds 182 + </Text> 183 + </Button> 184 + )} 185 + </View> 186 + ) 187 + }, [ 188 + pal, 189 + currentFeed?.isSaved, 190 + currentFeed?.isLiked, 191 + isPinned, 192 + onToggleSaved, 193 + onTogglePinned, 194 + onToggleLiked, 195 + dropdownItems, 196 + ]) 197 + 198 + const renderListHeaderComponent = React.useCallback(() => { 199 + return ( 200 + <> 201 + <View style={[styles.header, pal.border]}> 202 + <View style={s.flex1}> 203 + <Text 204 + testID="feedName" 205 + type="title-xl" 206 + style={[pal.text, s.bold]}> 207 + {currentFeed?.displayName} 208 + </Text> 209 + {currentFeed && ( 210 + <Text type="md" style={[pal.textLight]} numberOfLines={1}> 211 + by{' '} 212 + {currentFeed.data.creator.did === store.me.did ? ( 213 + 'you' 214 + ) : ( 215 + <TextLink 216 + text={`@${currentFeed.data.creator.handle}`} 217 + href={`/profile/${currentFeed.data.creator.did}`} 218 + style={[pal.textLight]} 219 + /> 220 + )} 221 + </Text> 222 + )} 223 + {isDesktopWeb && ( 224 + <View style={[styles.headerBtns, styles.headerBtnsDesktop]}> 225 + <Button 226 + type={currentFeed?.isSaved ? 'default' : 'inverted'} 227 + onPress={onToggleSaved} 228 + accessibilityLabel={ 229 + currentFeed?.isSaved 230 + ? 'Unsave this feed' 231 + : 'Save this feed' 232 + } 233 + accessibilityHint="" 234 + label={ 235 + currentFeed?.isSaved 236 + ? 'Remove from My Feeds' 237 + : 'Add to My Feeds' 238 + } 239 + /> 240 + <Button 241 + type="default" 242 + accessibilityLabel={ 243 + isPinned ? 'Unpin this feed' : 'Pin this feed' 244 + } 245 + accessibilityHint="" 246 + onPress={onTogglePinned}> 247 + <FontAwesomeIcon 248 + icon="thumb-tack" 249 + size={15} 250 + color={isPinned ? colors.blue3 : pal.colors.icon} 251 + style={styles.top2} 252 + /> 253 + </Button> 254 + <Button 255 + type="default" 256 + accessibilityLabel="Like this feed" 257 + accessibilityHint="" 258 + onPress={onToggleLiked}> 259 + {currentFeed?.isLiked ? ( 260 + <HeartIconSolid size={18} style={styles.liked} /> 261 + ) : ( 262 + <HeartIcon strokeWidth={3} size={18} style={pal.icon} /> 263 + )} 264 + </Button> 265 + <Button 266 + type="default" 267 + accessibilityLabel="Share this feed" 268 + accessibilityHint="" 269 + onPress={onPressShare}> 270 + <FontAwesomeIcon 271 + icon="share" 272 + size={18} 273 + color={pal.colors.icon} 274 + /> 275 + </Button> 276 + </View> 277 + )} 278 + </View> 279 + <View> 280 + <UserAvatar 281 + type="algo" 282 + avatar={currentFeed?.data.avatar} 283 + size={64} 284 + /> 285 + </View> 286 + </View> 287 + <View style={styles.headerDetails}> 288 + {currentFeed?.data.description ? ( 289 + <Text style={[pal.text, s.mb10]} numberOfLines={6}> 290 + {currentFeed.data.description} 291 + </Text> 292 + ) : null} 293 + <View style={styles.headerDetailsFooter}> 294 + {currentFeed ? ( 295 + <TextLink 296 + type="md-medium" 297 + style={pal.textLight} 298 + href={`/profile/${name}/feed/${rkey}/liked-by`} 299 + text={`Liked by ${currentFeed.data.likeCount} ${pluralize( 300 + currentFeed?.data.likeCount || 0, 301 + 'user', 302 + )}`} 303 + /> 304 + ) : null} 305 + </View> 306 + </View> 307 + <View style={[styles.fakeSelector, pal.border]}> 308 + <View 309 + style={[styles.fakeSelectorItem, {borderColor: pal.colors.link}]}> 310 + <Text type="md-medium" style={[pal.text]}> 311 + Feed 312 + </Text> 313 + </View> 314 + </View> 315 + </> 316 + ) 317 + }, [ 318 + pal, 319 + currentFeed, 320 + store.me.did, 321 + onToggleSaved, 322 + onToggleLiked, 323 + onPressShare, 324 + name, 325 + rkey, 326 + isPinned, 327 + onTogglePinned, 328 + ]) 329 + 330 + const renderEmptyState = React.useCallback(() => { 331 + return <EmptyState icon="feed" message="This list is empty!" /> 332 + }, []) 333 + 334 + return ( 335 + <View style={s.hContentRegion}> 336 + <ViewHeader title="" renderButton={currentFeed && renderHeaderBtns} /> 337 + <Feed 338 + scrollElRef={scrollElRef} 339 + feed={algoFeed} 340 + onScroll={onMainScroll} 341 + scrollEventThrottle={100} 342 + ListHeaderComponent={renderListHeaderComponent} 343 + renderEmptyState={renderEmptyState} 344 + extraData={[uri, isPinned]} 345 + /> 346 + {isScrolledDown ? ( 347 + <LoadLatestBtn 348 + onPress={onScrollToTop} 349 + label="Scroll to top" 350 + showIndicator={false} 351 + /> 352 + ) : null} 353 + <FAB 354 + testID="composeFAB" 355 + onPress={onPressCompose} 356 + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 357 + accessibilityRole="button" 358 + accessibilityLabel="Compose post" 359 + accessibilityHint="" 360 + /> 361 + </View> 362 + ) 363 + }), 364 + ) 365 + 366 + const styles = StyleSheet.create({ 367 + header: { 368 + flexDirection: 'row', 369 + gap: 12, 370 + paddingHorizontal: 16, 371 + paddingTop: 12, 372 + paddingBottom: 16, 373 + borderTopWidth: 1, 374 + }, 375 + headerBtns: { 376 + flexDirection: 'row', 377 + alignItems: 'center', 378 + }, 379 + headerBtnsDesktop: { 380 + marginTop: 8, 381 + gap: 4, 382 + }, 383 + headerAddBtn: { 384 + flexDirection: 'row', 385 + alignItems: 'center', 386 + gap: 4, 387 + paddingLeft: 4, 388 + }, 389 + headerDetails: { 390 + paddingHorizontal: 16, 391 + paddingBottom: 16, 392 + }, 393 + headerDetailsFooter: { 394 + flexDirection: 'row', 395 + alignItems: 'center', 396 + justifyContent: 'space-between', 397 + }, 398 + fakeSelector: { 399 + flexDirection: 'row', 400 + paddingHorizontal: isDesktopWeb ? 16 : 6, 401 + }, 402 + fakeSelectorItem: { 403 + paddingHorizontal: 12, 404 + paddingBottom: 8, 405 + borderBottomWidth: 3, 406 + }, 407 + liked: { 408 + color: colors.red3, 409 + }, 410 + top1: { 411 + position: 'relative', 412 + top: 1, 413 + }, 414 + top2: { 415 + position: 'relative', 416 + top: 2, 417 + }, 418 + })
+29
src/view/screens/CustomFeedLikedBy.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {useFocusEffect} from '@react-navigation/native' 4 + import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 5 + import {withAuthRequired} from 'view/com/auth/withAuthRequired' 6 + import {ViewHeader} from '../com/util/ViewHeader' 7 + import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' 8 + import {useStores} from 'state/index' 9 + import {makeRecordUri} from 'lib/strings/url-helpers' 10 + 11 + type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeedLikedBy'> 12 + export const CustomFeedLikedByScreen = withAuthRequired(({route}: Props) => { 13 + const store = useStores() 14 + const {name, rkey} = route.params 15 + const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) 16 + 17 + useFocusEffect( 18 + React.useCallback(() => { 19 + store.shell.setMinimalShellMode(false) 20 + }, [store]), 21 + ) 22 + 23 + return ( 24 + <View> 25 + <ViewHeader title="Liked by" /> 26 + <PostLikedByComponent uri={uri} /> 27 + </View> 28 + ) 29 + })
+112
src/view/screens/DiscoverFeeds.tsx
··· 1 + import React from 'react' 2 + import {RefreshControl, StyleSheet, View} from 'react-native' 3 + import {observer} from 'mobx-react-lite' 4 + import {useFocusEffect} from '@react-navigation/native' 5 + import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 6 + import {withAuthRequired} from 'view/com/auth/withAuthRequired' 7 + import {ViewHeader} from '../com/util/ViewHeader' 8 + import {useStores} from 'state/index' 9 + import {FeedsDiscoveryModel} from 'state/models/discovery/feeds' 10 + import {CenteredView, FlatList} from 'view/com/util/Views' 11 + import {CustomFeed} from 'view/com/feeds/CustomFeed' 12 + import {Text} from 'view/com/util/text/Text' 13 + import {isDesktopWeb} from 'platform/detection' 14 + import {usePalette} from 'lib/hooks/usePalette' 15 + import {s} from 'lib/styles' 16 + 17 + type Props = NativeStackScreenProps<CommonNavigatorParams, 'DiscoverFeeds'> 18 + export const DiscoverFeedsScreen = withAuthRequired( 19 + observer(({}: Props) => { 20 + const store = useStores() 21 + const pal = usePalette('default') 22 + const feeds = React.useMemo(() => new FeedsDiscoveryModel(store), [store]) 23 + 24 + useFocusEffect( 25 + React.useCallback(() => { 26 + store.shell.setMinimalShellMode(false) 27 + feeds.refresh() 28 + }, [store, feeds]), 29 + ) 30 + 31 + const onRefresh = React.useCallback(() => { 32 + store.me.savedFeeds.refresh() 33 + }, [store]) 34 + 35 + const renderListEmptyComponent = React.useCallback(() => { 36 + return ( 37 + <View 38 + style={[ 39 + pal.border, 40 + !isDesktopWeb && s.flex1, 41 + pal.viewLight, 42 + styles.empty, 43 + ]}> 44 + <Text type="lg" style={[pal.text]}> 45 + {feeds.isLoading 46 + ? 'Loading...' 47 + : `We can't find any feeds for some reason. This is probably an error - try refreshing!`} 48 + </Text> 49 + </View> 50 + ) 51 + }, [pal, feeds.isLoading]) 52 + 53 + const renderItem = React.useCallback( 54 + ({item}) => ( 55 + <CustomFeed 56 + key={item.data.uri} 57 + item={item} 58 + showSaveBtn 59 + showDescription 60 + showLikes 61 + /> 62 + ), 63 + [], 64 + ) 65 + 66 + return ( 67 + <CenteredView style={[styles.container, pal.view]}> 68 + <View style={[isDesktopWeb && styles.containerDesktop, pal.border]}> 69 + <ViewHeader title="Discover Feeds" showOnDesktop /> 70 + </View> 71 + <FlatList 72 + style={[!isDesktopWeb && s.flex1]} 73 + data={feeds.feeds} 74 + keyExtractor={item => item.data.uri} 75 + contentContainerStyle={styles.contentContainer} 76 + refreshControl={ 77 + <RefreshControl 78 + refreshing={feeds.isRefreshing} 79 + onRefresh={onRefresh} 80 + tintColor={pal.colors.text} 81 + titleColor={pal.colors.text} 82 + /> 83 + } 84 + renderItem={renderItem} 85 + initialNumToRender={10} 86 + ListEmptyComponent={renderListEmptyComponent} 87 + extraData={feeds.isLoading} 88 + /> 89 + </CenteredView> 90 + ) 91 + }), 92 + ) 93 + 94 + const styles = StyleSheet.create({ 95 + container: { 96 + flex: 1, 97 + }, 98 + contentContainer: { 99 + paddingBottom: 100, 100 + }, 101 + containerDesktop: { 102 + borderLeftWidth: 1, 103 + borderRightWidth: 1, 104 + }, 105 + empty: { 106 + paddingHorizontal: 18, 107 + paddingVertical: 16, 108 + borderRadius: 8, 109 + marginHorizontal: 18, 110 + marginTop: 10, 111 + }, 112 + })
+126
src/view/screens/Feeds.tsx
··· 1 + import React from 'react' 2 + import {StyleSheet, View} from 'react-native' 3 + import {useFocusEffect} from '@react-navigation/native' 4 + import isEqual from 'lodash.isequal' 5 + import {withAuthRequired} from 'view/com/auth/withAuthRequired' 6 + import {FlatList} from 'view/com/util/Views' 7 + import {ViewHeader} from 'view/com/util/ViewHeader' 8 + import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 9 + import {FAB} from 'view/com/util/fab/FAB' 10 + import {Link} from 'view/com/util/Link' 11 + import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types' 12 + import {observer} from 'mobx-react-lite' 13 + import {PostsMultiFeedModel} from 'state/models/feeds/multi-feed' 14 + import {MultiFeed} from 'view/com/posts/MultiFeed' 15 + import {isDesktopWeb} from 'platform/detection' 16 + import {usePalette} from 'lib/hooks/usePalette' 17 + import {useStores} from 'state/index' 18 + import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' 19 + import {ComposeIcon2, CogIcon} from 'lib/icons' 20 + import {s} from 'lib/styles' 21 + 22 + const HEADER_OFFSET = isDesktopWeb ? 0 : 40 23 + 24 + type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'> 25 + export const FeedsScreen = withAuthRequired( 26 + observer<Props>(({}: Props) => { 27 + const pal = usePalette('default') 28 + const store = useStores() 29 + const flatListRef = React.useRef<FlatList>(null) 30 + const multifeed = React.useMemo<PostsMultiFeedModel>( 31 + () => new PostsMultiFeedModel(store), 32 + [store], 33 + ) 34 + const [onMainScroll, isScrolledDown, resetMainScroll] = 35 + useOnMainScroll(store) 36 + 37 + const onSoftReset = React.useCallback(() => { 38 + flatListRef.current?.scrollToOffset({offset: 0}) 39 + resetMainScroll() 40 + }, [flatListRef, resetMainScroll]) 41 + 42 + useFocusEffect( 43 + React.useCallback(() => { 44 + const softResetSub = store.onScreenSoftReset(onSoftReset) 45 + const multifeedCleanup = multifeed.registerListeners() 46 + const cleanup = () => { 47 + softResetSub.remove() 48 + multifeedCleanup() 49 + } 50 + 51 + store.shell.setMinimalShellMode(false) 52 + return cleanup 53 + }, [store, multifeed, onSoftReset]), 54 + ) 55 + 56 + React.useEffect(() => { 57 + if ( 58 + isEqual( 59 + multifeed.feedInfos.map(f => f.uri), 60 + store.me.savedFeeds.all.map(f => f.uri), 61 + ) 62 + ) { 63 + // no changes 64 + return 65 + } 66 + multifeed.refresh() 67 + }, [multifeed, store.me.savedFeeds.all]) 68 + 69 + const onPressCompose = React.useCallback(() => { 70 + store.shell.openComposer({}) 71 + }, [store]) 72 + 73 + const renderHeaderBtn = React.useCallback(() => { 74 + return ( 75 + <Link 76 + href="/settings/saved-feeds" 77 + hitSlop={10} 78 + accessibilityRole="button" 79 + accessibilityLabel="Edit Saved Feeds" 80 + accessibilityHint="Opens screen to edit Saved Feeds"> 81 + <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> 82 + </Link> 83 + ) 84 + }, [pal]) 85 + 86 + return ( 87 + <View style={[pal.view, styles.container]}> 88 + <MultiFeed 89 + scrollElRef={flatListRef} 90 + multifeed={multifeed} 91 + onScroll={onMainScroll} 92 + scrollEventThrottle={100} 93 + headerOffset={HEADER_OFFSET} 94 + showPostFollowBtn 95 + /> 96 + <ViewHeader 97 + title="My Feeds" 98 + canGoBack={false} 99 + hideOnScroll 100 + renderButton={renderHeaderBtn} 101 + /> 102 + {isScrolledDown ? ( 103 + <LoadLatestBtn 104 + onPress={onSoftReset} 105 + label="Scroll to top" 106 + showIndicator={false} 107 + /> 108 + ) : null} 109 + <FAB 110 + testID="composeFAB" 111 + onPress={onPressCompose} 112 + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 113 + accessibilityRole="button" 114 + accessibilityLabel="Compose post" 115 + accessibilityHint="" 116 + /> 117 + </View> 118 + ) 119 + }), 120 + ) 121 + 122 + const styles = StyleSheet.create({ 123 + container: { 124 + flex: 1, 125 + }, 126 + })
+54 -33
src/view/screens/Home.tsx
··· 1 1 import React from 'react' 2 2 import {FlatList, View} from 'react-native' 3 3 import {useFocusEffect, useIsFocused} from '@react-navigation/native' 4 + import {AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api' 4 5 import {observer} from 'mobx-react-lite' 5 6 import useAppState from 'react-native-appstate-hook' 7 + import isEqual from 'lodash.isequal' 6 8 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' 7 9 import {PostsFeedModel} from 'state/models/feeds/posts' 8 10 import {withAuthRequired} from 'view/com/auth/withAuthRequired' 9 11 import {useTabFocusEffect} from 'lib/hooks/useTabFocusEffect' 10 12 import {Feed} from '../com/posts/Feed' 11 13 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' 12 - import {WhatsHotEmptyState} from 'view/com/posts/WhatsHotEmptyState' 14 + import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' 13 15 import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn' 14 16 import {FeedsTabBar} from '../com/pager/FeedsTabBar' 15 - import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager' 17 + import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' 16 18 import {FAB} from '../com/util/fab/FAB' 17 19 import {useStores} from 'state/index' 18 20 import {s} from 'lib/styles' ··· 21 23 import {ComposeIcon2} from 'lib/icons' 22 24 import {isDesktopWeb} from 'platform/detection' 23 25 24 - const HEADER_OFFSET = isDesktopWeb ? 50 : 40 26 + const HEADER_OFFSET = isDesktopWeb ? 50 : 78 25 27 const POLL_FREQ = 30e3 // 30sec 26 28 27 29 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> 28 30 export const HomeScreen = withAuthRequired( 29 31 observer((_opts: Props) => { 30 32 const store = useStores() 33 + const pagerRef = React.useRef<PagerRef>(null) 31 34 const [selectedPage, setSelectedPage] = React.useState(0) 32 - const [initialLanguages] = React.useState( 33 - store.preferences.contentLanguages, 34 - ) 35 - 36 - const algoFeed: PostsFeedModel = React.useMemo(() => { 37 - const feed = new PostsFeedModel(store, 'goodstuff', {}) 38 - feed.setup() 39 - return feed 40 - }, [store]) 35 + const [customFeeds, setCustomFeeds] = React.useState<PostsFeedModel[]>([]) 41 36 42 37 React.useEffect(() => { 43 - // refresh whats hot when lang preferences change 44 - if (initialLanguages !== store.preferences.contentLanguages) { 45 - algoFeed.refresh() 38 + const {pinned} = store.me.savedFeeds 39 + if ( 40 + isEqual( 41 + pinned.map(p => p.uri), 42 + customFeeds.map(f => (f.params as GetCustomFeed.QueryParams).feed), 43 + ) 44 + ) { 45 + // no changes 46 + return 46 47 } 47 - }, [initialLanguages, store.preferences.contentLanguages, algoFeed]) 48 + 49 + const feeds = [] 50 + for (const feed of pinned) { 51 + const model = new PostsFeedModel(store, 'custom', {feed: feed.uri}) 52 + model.setup() 53 + feeds.push(model) 54 + } 55 + setCustomFeeds(feeds) 56 + }, [store, store.me.savedFeeds.pinned, customFeeds, setCustomFeeds]) 48 57 49 58 useFocusEffect( 50 59 React.useCallback(() => { ··· 86 95 return <FollowingEmptyState /> 87 96 }, []) 88 97 89 - const renderWhatsHotEmptyState = React.useCallback(() => { 90 - return <WhatsHotEmptyState /> 98 + const renderCustomFeedEmptyState = React.useCallback(() => { 99 + return <CustomFeedEmptyState /> 91 100 }, []) 92 101 93 - const initialPage = store.me.followsCount === 0 ? 1 : 0 94 102 return ( 95 103 <Pager 104 + ref={pagerRef} 96 105 testID="homeScreen" 97 106 onPageSelected={onPageSelected} 98 107 renderTabBar={renderTabBar} 99 - tabBarPosition="top" 100 - initialPage={initialPage}> 108 + tabBarPosition="top"> 101 109 <FeedPage 102 110 key="1" 103 111 testID="followingFeedPage" ··· 105 113 feed={store.me.mainFeed} 106 114 renderEmptyState={renderFollowingEmptyState} 107 115 /> 108 - <FeedPage 109 - key="2" 110 - testID="whatshotFeedPage" 111 - isPageFocused={selectedPage === 1} 112 - feed={algoFeed} 113 - renderEmptyState={renderWhatsHotEmptyState} 114 - /> 116 + {customFeeds.map((f, index) => { 117 + return ( 118 + <FeedPage 119 + key={(f.params as GetCustomFeed.QueryParams).feed} 120 + testID="customFeedPage" 121 + isPageFocused={selectedPage === 1 + index} 122 + feed={f} 123 + renderEmptyState={renderCustomFeedEmptyState} 124 + /> 125 + ) 126 + })} 115 127 </Pager> 116 128 ) 117 129 }), ··· 130 142 renderEmptyState?: () => JSX.Element 131 143 }) => { 132 144 const store = useStores() 133 - const onMainScroll = useOnMainScroll(store) 145 + const [onMainScroll, isScrolledDown, resetMainScroll] = 146 + useOnMainScroll(store) 134 147 const {screen, track} = useAnalytics() 135 148 const scrollElRef = React.useRef<FlatList>(null) 136 149 const {appState} = useAppState({ ··· 158 171 159 172 const scrollToTop = React.useCallback(() => { 160 173 scrollElRef.current?.scrollToOffset({offset: -HEADER_OFFSET}) 161 - }, [scrollElRef]) 174 + resetMainScroll() 175 + }, [scrollElRef, resetMainScroll]) 162 176 163 177 const onSoftReset = React.useCallback(() => { 164 178 if (isPageFocused) { 165 - feed.refresh() 166 179 scrollToTop() 180 + feed.refresh() 167 181 } 168 182 }, [isPageFocused, scrollToTop, feed]) 169 183 ··· 224 238 feed.refresh() 225 239 }, [feed, scrollToTop]) 226 240 241 + const hasNew = feed.hasNewLatest && !feed.isRefreshing 227 242 return ( 228 243 <View testID={testID} style={s.h100pct}> 229 244 <Feed ··· 234 249 showPostFollowBtn 235 250 onPressTryAgain={onPressTryAgain} 236 251 onScroll={onMainScroll} 252 + scrollEventThrottle={100} 237 253 renderEmptyState={renderEmptyState} 238 254 headerOffset={HEADER_OFFSET} 239 255 /> 240 - {feed.hasNewLatest && !feed.isRefreshing && ( 241 - <LoadLatestBtn onPress={onPressLoadLatest} label="posts" /> 256 + {(isScrolledDown || hasNew) && ( 257 + <LoadLatestBtn 258 + onPress={onPressLoadLatest} 259 + label="Load new posts" 260 + showIndicator={hasNew} 261 + minimalShellMode={store.shell.minimalShellMode} 262 + /> 242 263 )} 243 264 <FAB 244 265 testID="composeFAB"
+1 -1
src/view/screens/ModerationMutedAccounts.tsx
··· 100 100 <FlatList 101 101 style={[!isDesktopWeb && styles.flex1]} 102 102 data={mutedAccounts.mutes} 103 - keyExtractor={(item: ActorDefs.ProfileView) => item.did} 103 + keyExtractor={item => item.did} 104 104 refreshControl={ 105 105 <RefreshControl 106 106 refreshing={mutedAccounts.isRefreshing}
+15 -6
src/view/screens/Notifications.tsx
··· 25 25 export const NotificationsScreen = withAuthRequired( 26 26 observer(({}: Props) => { 27 27 const store = useStores() 28 - const onMainScroll = useOnMainScroll(store) 28 + const [onMainScroll, isScrolledDown, resetMainScroll] = 29 + useOnMainScroll(store) 29 30 const scrollElRef = React.useRef<FlatList>(null) 30 31 const {screen} = useAnalytics() 31 32 ··· 37 38 38 39 const scrollToTop = React.useCallback(() => { 39 40 scrollElRef.current?.scrollToOffset({offset: 0}) 40 - }, [scrollElRef]) 41 + resetMainScroll() 42 + }, [scrollElRef, resetMainScroll]) 41 43 42 44 const onPressLoadLatest = React.useCallback(() => { 43 45 scrollToTop() ··· 86 88 ), 87 89 ) 88 90 91 + const hasNew = 92 + store.me.notifications.hasNewLatest && 93 + !store.me.notifications.isRefreshing 89 94 return ( 90 95 <View testID="notificationsScreen" style={s.hContentRegion}> 91 96 <ViewHeader title="Notifications" canGoBack={false} /> ··· 96 101 onScroll={onMainScroll} 97 102 scrollElRef={scrollElRef} 98 103 /> 99 - {store.me.notifications.hasNewLatest && 100 - !store.me.notifications.isRefreshing && ( 101 - <LoadLatestBtn onPress={onPressLoadLatest} label="notifications" /> 102 - )} 104 + {(isScrolledDown || hasNew) && ( 105 + <LoadLatestBtn 106 + onPress={onPressLoadLatest} 107 + label="Load new notifications" 108 + showIndicator={hasNew} 109 + minimalShellMode={true} 110 + /> 111 + )} 103 112 </View> 104 113 ) 105 114 }),
+30 -1
src/view/screens/Profile.tsx
··· 9 9 import {ScreenHider} from 'view/com/util/moderation/ScreenHider' 10 10 import {ProfileUiModel, Sections} from 'state/models/ui/profile' 11 11 import {useStores} from 'state/index' 12 - import {PostsFeedSliceModel} from 'state/models/feeds/posts' 12 + import {PostsFeedSliceModel} from 'state/models/feeds/post' 13 13 import {ProfileHeader} from '../com/profile/ProfileHeader' 14 14 import {FeedSlice} from '../com/posts/FeedSlice' 15 15 import {ListCard} from 'view/com/lists/ListCard' ··· 25 25 import {s, colors} from 'lib/styles' 26 26 import {useAnalytics} from 'lib/analytics' 27 27 import {ComposeIcon2} from 'lib/icons' 28 + import {CustomFeed} from 'view/com/feeds/CustomFeed' 29 + import {CustomFeedModel} from 'state/models/feeds/custom-feed' 28 30 import {useSetTitle} from 'lib/hooks/useSetTitle' 29 31 import {combinedDisplayName} from 'lib/strings/display-names' 30 32 ··· 118 120 }, [uiState.showLoadingMoreFooter]) 119 121 const renderItem = React.useCallback( 120 122 (item: any) => { 123 + // if section is lists 121 124 if (uiState.selectedView === Sections.Lists) { 122 125 if (item === ProfileUiModel.LOADING_ITEM) { 123 126 return <ProfileCardFeedLoadingPlaceholder /> ··· 142 145 } else { 143 146 return <ListCard testID={`list-${item.name}`} list={item} /> 144 147 } 148 + // if section is custom algorithms 149 + } else if (uiState.selectedView === Sections.CustomAlgorithms) { 150 + if (item === ProfileUiModel.LOADING_ITEM) { 151 + return <ProfileCardFeedLoadingPlaceholder /> 152 + } else if (item._reactKey === '__error__') { 153 + return ( 154 + <View style={s.p5}> 155 + <ErrorMessage 156 + message={item.error} 157 + onPressTryAgain={onPressTryAgain} 158 + /> 159 + </View> 160 + ) 161 + } else if (item === ProfileUiModel.EMPTY_ITEM) { 162 + return ( 163 + <EmptyState 164 + testID="customAlgorithmsEmpty" 165 + icon="list-ul" 166 + message="No custom algorithms yet!" 167 + style={styles.emptyState} 168 + /> 169 + ) 170 + } else if (item instanceof CustomFeedModel) { 171 + return <CustomFeed item={item} showSaveBtn showLikes /> 172 + } 173 + // if section is posts or posts & replies 145 174 } else { 146 175 if (item === ProfileUiModel.END_ITEM) { 147 176 return <Text style={styles.endItem}>- end of feed -</Text>
+2 -2
src/view/screens/ProfileList.tsx
··· 87 87 return <EmptyState icon="users-slash" message="This list is empty!" /> 88 88 }, []) 89 89 90 - const renderHeaderBtn = React.useCallback(() => { 90 + const renderHeaderBtns = React.useCallback(() => { 91 91 return ( 92 92 <View style={styles.headerBtns}> 93 93 {list?.isOwner && ( ··· 148 148 pal.border, 149 149 ]} 150 150 testID="moderationMutelistsScreen"> 151 - <ViewHeader title="" renderButton={renderHeaderBtn} /> 151 + <ViewHeader title="" renderButton={renderHeaderBtns} /> 152 152 <ListItems 153 153 list={list} 154 154 renderEmptyState={renderEmptyState}
+293
src/view/screens/SavedFeeds.tsx
··· 1 + import React, {useCallback, useMemo} from 'react' 2 + import { 3 + RefreshControl, 4 + StyleSheet, 5 + View, 6 + ActivityIndicator, 7 + Pressable, 8 + TouchableOpacity, 9 + } from 'react-native' 10 + import {useFocusEffect} from '@react-navigation/native' 11 + import {NativeStackScreenProps} from '@react-navigation/native-stack' 12 + import {useAnalytics} from 'lib/analytics' 13 + import {usePalette} from 'lib/hooks/usePalette' 14 + import {CommonNavigatorParams} from 'lib/routes/types' 15 + import {observer} from 'mobx-react-lite' 16 + import {useStores} from 'state/index' 17 + import {withAuthRequired} from 'view/com/auth/withAuthRequired' 18 + import {ViewHeader} from 'view/com/util/ViewHeader' 19 + import {CenteredView} from 'view/com/util/Views' 20 + import {Text} from 'view/com/util/text/Text' 21 + import {isDesktopWeb, isWeb} from 'platform/detection' 22 + import {s, colors} from 'lib/styles' 23 + import DraggableFlatList, { 24 + ShadowDecorator, 25 + ScaleDecorator, 26 + } from 'react-native-draggable-flatlist' 27 + import {CustomFeed} from 'view/com/feeds/CustomFeed' 28 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 29 + import {CustomFeedModel} from 'state/models/feeds/custom-feed' 30 + import * as Toast from 'view/com/util/Toast' 31 + import {Haptics} from 'lib/haptics' 32 + import {Link, TextLink} from 'view/com/util/Link' 33 + 34 + type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'> 35 + 36 + export const SavedFeeds = withAuthRequired( 37 + observer(({}: Props) => { 38 + const pal = usePalette('default') 39 + const store = useStores() 40 + const {screen} = useAnalytics() 41 + 42 + const savedFeeds = useMemo(() => store.me.savedFeeds, [store]) 43 + useFocusEffect( 44 + useCallback(() => { 45 + screen('SavedFeeds') 46 + store.shell.setMinimalShellMode(false) 47 + savedFeeds.refresh() 48 + }, [screen, store, savedFeeds]), 49 + ) 50 + 51 + const renderListEmptyComponent = useCallback(() => { 52 + return ( 53 + <View 54 + style={[ 55 + pal.border, 56 + !isDesktopWeb && s.flex1, 57 + pal.viewLight, 58 + styles.empty, 59 + ]}> 60 + <Text type="lg" style={[pal.text]}> 61 + You don't have any saved feeds. 62 + </Text> 63 + </View> 64 + ) 65 + }, [pal]) 66 + 67 + const renderListFooterComponent = useCallback(() => { 68 + return ( 69 + <> 70 + <View style={[styles.footerLinks, pal.border]}> 71 + <Link style={styles.footerLink} href="/search/feeds"> 72 + <FontAwesomeIcon 73 + icon="search" 74 + size={18} 75 + color={pal.colors.icon} 76 + /> 77 + <Text type="lg-medium" style={pal.textLight}> 78 + Discover new feeds 79 + </Text> 80 + </Link> 81 + </View> 82 + <View style={styles.footerText}> 83 + <Text type="sm" style={pal.textLight}> 84 + Feeds are custom algorithms that users build with a little coding 85 + expertise.{' '} 86 + <TextLink 87 + type="sm" 88 + style={pal.link} 89 + href="https://github.com/bluesky-social/feed-generator" 90 + text="See this guide" 91 + />{' '} 92 + for more information. 93 + </Text> 94 + </View> 95 + {savedFeeds.isLoading && <ActivityIndicator />} 96 + </> 97 + ) 98 + }, [pal, savedFeeds.isLoading]) 99 + 100 + const onRefresh = useCallback(() => savedFeeds.refresh(), [savedFeeds]) 101 + 102 + const onDragEnd = useCallback( 103 + async ({data}) => { 104 + try { 105 + await savedFeeds.reorderPinnedFeeds(data) 106 + } catch (e) { 107 + Toast.show('There was an issue contacting the server') 108 + store.log.error('Failed to save pinned feed order', {e}) 109 + } 110 + }, 111 + [savedFeeds, store], 112 + ) 113 + 114 + return ( 115 + <CenteredView 116 + style={[ 117 + s.hContentRegion, 118 + pal.border, 119 + isDesktopWeb && styles.desktopContainer, 120 + ]}> 121 + <ViewHeader 122 + title="Edit My Feeds" 123 + showOnDesktop 124 + showBorder={!isDesktopWeb} 125 + /> 126 + <DraggableFlatList 127 + containerStyle={[!isDesktopWeb && s.flex1]} 128 + data={savedFeeds.all} 129 + keyExtractor={item => item.data.uri} 130 + refreshing={savedFeeds.isRefreshing} 131 + refreshControl={ 132 + <RefreshControl 133 + refreshing={savedFeeds.isRefreshing} 134 + onRefresh={onRefresh} 135 + tintColor={pal.colors.text} 136 + titleColor={pal.colors.text} 137 + /> 138 + } 139 + renderItem={({item, drag}) => <ListItem item={item} drag={drag} />} 140 + getItemLayout={(data, index) => ({ 141 + length: 77, 142 + offset: 77 * index, 143 + index, 144 + })} 145 + initialNumToRender={10} 146 + ListFooterComponent={renderListFooterComponent} 147 + ListEmptyComponent={renderListEmptyComponent} 148 + extraData={savedFeeds.isLoading} 149 + onDragEnd={onDragEnd} 150 + /> 151 + </CenteredView> 152 + ) 153 + }), 154 + ) 155 + 156 + const ListItem = observer( 157 + ({item, drag}: {item: CustomFeedModel; drag: () => void}) => { 158 + const pal = usePalette('default') 159 + const store = useStores() 160 + const savedFeeds = useMemo(() => store.me.savedFeeds, [store]) 161 + const isPinned = savedFeeds.isPinned(item) 162 + 163 + const onTogglePinned = useCallback(() => { 164 + Haptics.default() 165 + savedFeeds.togglePinnedFeed(item).catch(e => { 166 + Toast.show('There was an issue contacting the server') 167 + store.log.error('Failed to toggle pinned feed', {e}) 168 + }) 169 + }, [savedFeeds, item, store]) 170 + const onPressUp = useCallback( 171 + () => 172 + savedFeeds.movePinnedFeed(item, 'up').catch(e => { 173 + Toast.show('There was an issue contacting the server') 174 + store.log.error('Failed to set pinned feed order', {e}) 175 + }), 176 + [store, savedFeeds, item], 177 + ) 178 + const onPressDown = useCallback( 179 + () => 180 + savedFeeds.movePinnedFeed(item, 'down').catch(e => { 181 + Toast.show('There was an issue contacting the server') 182 + store.log.error('Failed to set pinned feed order', {e}) 183 + }), 184 + [store, savedFeeds, item], 185 + ) 186 + 187 + return ( 188 + <ScaleDecorator> 189 + <ShadowDecorator> 190 + <Pressable 191 + accessibilityRole="button" 192 + onLongPress={isPinned ? drag : undefined} 193 + delayLongPress={200} 194 + style={[styles.itemContainer, pal.border]}> 195 + {isPinned && isWeb ? ( 196 + <View style={styles.webArrowButtonsContainer}> 197 + <TouchableOpacity 198 + accessibilityRole="button" 199 + onPress={onPressUp}> 200 + <FontAwesomeIcon 201 + icon="arrow-up" 202 + size={12} 203 + style={[pal.text, styles.webArrowUpButton]} 204 + /> 205 + </TouchableOpacity> 206 + <TouchableOpacity 207 + accessibilityRole="button" 208 + onPress={onPressDown}> 209 + <FontAwesomeIcon 210 + icon="arrow-down" 211 + size={12} 212 + style={[pal.text]} 213 + /> 214 + </TouchableOpacity> 215 + </View> 216 + ) : isPinned ? ( 217 + <FontAwesomeIcon 218 + icon="bars" 219 + size={20} 220 + color={pal.colors.text} 221 + style={s.ml20} 222 + /> 223 + ) : null} 224 + <CustomFeed 225 + key={item.data.uri} 226 + item={item} 227 + showSaveBtn 228 + style={styles.noBorder} 229 + /> 230 + <TouchableOpacity 231 + accessibilityRole="button" 232 + hitSlop={10} 233 + onPress={onTogglePinned}> 234 + <FontAwesomeIcon 235 + icon="thumb-tack" 236 + size={20} 237 + color={isPinned ? colors.blue3 : pal.colors.icon} 238 + /> 239 + </TouchableOpacity> 240 + </Pressable> 241 + </ShadowDecorator> 242 + </ScaleDecorator> 243 + ) 244 + }, 245 + ) 246 + 247 + const styles = StyleSheet.create({ 248 + desktopContainer: { 249 + borderLeftWidth: 1, 250 + borderRightWidth: 1, 251 + minHeight: '100vh', 252 + }, 253 + empty: { 254 + paddingHorizontal: 20, 255 + paddingVertical: 20, 256 + borderRadius: 16, 257 + marginHorizontal: 24, 258 + marginTop: 10, 259 + }, 260 + itemContainer: { 261 + flex: 1, 262 + flexDirection: 'row', 263 + alignItems: 'center', 264 + borderBottomWidth: 1, 265 + paddingRight: 16, 266 + }, 267 + webArrowButtonsContainer: { 268 + paddingLeft: 16, 269 + flexDirection: 'column', 270 + justifyContent: 'space-around', 271 + }, 272 + webArrowUpButton: { 273 + marginBottom: 10, 274 + }, 275 + noBorder: { 276 + borderTopWidth: 0, 277 + }, 278 + footerText: { 279 + paddingHorizontal: 26, 280 + paddingTop: 22, 281 + paddingBottom: 100, 282 + }, 283 + footerLinks: { 284 + borderBottomWidth: 1, 285 + borderTopWidth: 0, 286 + }, 287 + footerLink: { 288 + flexDirection: 'row', 289 + paddingHorizontal: 26, 290 + paddingVertical: 18, 291 + gap: 18, 292 + }, 293 + })
+1 -1
src/view/screens/SearchMobile.tsx
··· 35 35 const store = useStores() 36 36 const scrollViewRef = React.useRef<ScrollView>(null) 37 37 const flatListRef = React.useRef<FlatList>(null) 38 - const onMainScroll = useOnMainScroll(store) 38 + const [onMainScroll] = useOnMainScroll(store) 39 39 const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) 40 40 const [query, setQuery] = React.useState<string>('') 41 41 const autocompleteView = React.useMemo<UserAutocompleteModel>(
+31
src/view/screens/Settings.tsx
··· 141 141 store.shell.openModal({name: 'delete-account'}) 142 142 }, [store]) 143 143 144 + const onPressResetPreferences = React.useCallback(async () => { 145 + await store.preferences.reset() 146 + Toast.show('Preferences reset') 147 + }, [store]) 148 + 144 149 return ( 145 150 <View style={[s.hContentRegion]} testID="settingsScreen"> 146 151 <ViewHeader title="Settings" /> ··· 301 306 App passwords 302 307 </Text> 303 308 </Link> 309 + <Link 310 + testID="savedFeedsBtn" 311 + style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} 312 + accessibilityHint="Saved Feeds" 313 + accessibilityLabel="Opens screen with all saved feeds" 314 + href="/settings/saved-feeds"> 315 + <View style={[styles.iconContainer, pal.btn]}> 316 + <FontAwesomeIcon 317 + icon="satellite-dish" 318 + style={pal.text as FontAwesomeIconStyle} 319 + /> 320 + </View> 321 + <Text type="lg" style={pal.text}> 322 + Saved Feeds 323 + </Text> 324 + </Link> 304 325 <TouchableOpacity 305 326 testID="contentLanguagesBtn" 306 327 style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} ··· 377 398 Storybook 378 399 </Text> 379 400 </Link> 401 + {__DEV__ ? ( 402 + <Link 403 + style={[pal.view, styles.linkCardNoIcon]} 404 + onPress={onPressResetPreferences} 405 + title="Debug tools"> 406 + <Text type="lg" style={pal.text}> 407 + Reset preferences state 408 + </Text> 409 + </Link> 410 + ) : null} 380 411 <Text type="sm" style={[styles.buildInfo, pal.textLight]}> 381 412 Build version {AppInfo.appVersion} ({AppInfo.buildVersion}) 382 413 </Text>
+39 -13
src/view/shell/Drawer.tsx
··· 2 2 import { 3 3 Linking, 4 4 SafeAreaView, 5 + ScrollView, 5 6 StyleProp, 6 7 StyleSheet, 7 8 TouchableOpacity, ··· 28 29 MagnifyingGlassIcon2Solid, 29 30 MoonIcon, 30 31 UserIconSolid, 32 + SatelliteDishIcon, 33 + SatelliteDishIconSolid, 31 34 HandIcon, 32 35 } from 'lib/icons' 33 36 import {UserAvatar} from 'view/com/util/UserAvatar' ··· 40 43 import {NavigationProp} from 'lib/routes/types' 41 44 import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' 42 45 import {isWeb} from 'platform/detection' 43 - import {formatCount} from 'view/com/util/numeric/format' 46 + import {formatCount, formatCountShortOnly} from 'view/com/util/numeric/format' 44 47 45 48 export const DrawerContent = observer(() => { 46 49 const theme = useTheme() ··· 48 51 const store = useStores() 49 52 const navigation = useNavigation<NavigationProp>() 50 53 const {track} = useAnalytics() 51 - const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} = 54 + const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = 52 55 useNavigationTabState() 53 56 54 57 const {notifications} = store.me ··· 95 98 onPressTab('MyProfile') 96 99 }, [onPressTab]) 97 100 101 + const onPressMyFeeds = React.useCallback( 102 + () => onPressTab('Feeds'), 103 + [onPressTab], 104 + ) 105 + 98 106 const onPressModeration = React.useCallback(() => { 99 107 track('Menu:ItemClicked', {url: 'Moderation'}) 100 108 navigation.navigate('Moderation') ··· 147 155 type="xl" 148 156 style={[pal.textLight, styles.profileCardFollowers]}> 149 157 <Text type="xl-medium" style={pal.text}> 150 - {formatCount(store.me.followersCount ?? 0)} 158 + {formatCountShortOnly(store.me.followersCount ?? 0)} 151 159 </Text>{' '} 152 160 {pluralize(store.me.followersCount || 0, 'follower')} &middot;{' '} 153 161 <Text type="xl-medium" style={pal.text}> 154 - {formatCount(store.me.followsCount ?? 0)} 162 + {formatCountShortOnly(store.me.followsCount ?? 0)} 155 163 </Text>{' '} 156 164 following 157 165 </Text> 158 166 </TouchableOpacity> 159 167 </View> 160 168 <InviteCodes /> 161 - <View style={s.flex1} /> 162 - <View style={styles.main}> 169 + <ScrollView style={styles.main}> 163 170 <MenuItem 164 171 icon={ 165 172 isAtSearch ? ( ··· 233 240 /> 234 241 <MenuItem 235 242 icon={ 236 - <HandIcon 237 - strokeWidth={5} 238 - style={pal.text as FontAwesomeIconStyle} 239 - size={24} 240 - /> 243 + isAtFeeds ? ( 244 + <SatelliteDishIconSolid 245 + strokeWidth={1.5} 246 + style={pal.text as FontAwesomeIconStyle} 247 + size={24} 248 + /> 249 + ) : ( 250 + <SatelliteDishIcon 251 + strokeWidth={1.5} 252 + style={pal.text as FontAwesomeIconStyle} 253 + size={24} 254 + /> 255 + ) 241 256 } 257 + label="My Feeds" 258 + accessibilityLabel="My Feeds" 259 + accessibilityHint="" 260 + onPress={onPressMyFeeds} 261 + /> 262 + <MenuItem 263 + icon={<HandIcon strokeWidth={5} style={pal.text} size={24} />} 242 264 label="Moderation" 243 265 accessibilityLabel="Moderation" 244 266 accessibilityHint="" ··· 278 300 accessibilityHint="" 279 301 onPress={onPressSettings} 280 302 /> 281 - </View> 282 - <View style={s.flex1} /> 303 + <View style={styles.smallSpacer} /> 304 + </ScrollView> 283 305 <View style={styles.footer}> 284 306 {!isWeb && ( 285 307 <TouchableOpacity ··· 435 457 }, 436 458 main: { 437 459 paddingLeft: 20, 460 + paddingTop: 20, 461 + }, 462 + smallSpacer: { 463 + height: 20, 438 464 }, 439 465 440 466 profileCardDisplayName: {
+42 -13
src/view/shell/bottom-bar/BottomBar.tsx
··· 18 18 HomeIconSolid, 19 19 MagnifyingGlassIcon2, 20 20 MagnifyingGlassIcon2Solid, 21 + SatelliteDishIcon, 22 + SatelliteDishIconSolid, 21 23 BellIcon, 22 24 BellIconSolid, 23 - UserIcon, 24 - UserIconSolid, 25 25 } from 'lib/icons' 26 26 import {usePalette} from 'lib/hooks/usePalette' 27 27 import {getTabState, TabState} from 'lib/routes/helpers' 28 28 import {styles} from './BottomBarStyles' 29 29 import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' 30 30 import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' 31 + import {UserAvatar} from 'view/com/util/UserAvatar' 31 32 32 33 export const BottomBar = observer(({navigation}: BottomTabBarProps) => { 33 34 const store = useStores() 34 35 const pal = usePalette('default') 35 36 const safeAreaInsets = useSafeAreaInsets() 36 37 const {track} = useAnalytics() 37 - const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} = 38 + const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = 38 39 useNavigationTabState() 39 40 40 41 const {footerMinimalShellTransform} = useMinimalShellMode() ··· 58 59 const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab]) 59 60 const onPressSearch = React.useCallback( 60 61 () => onPressTab('Search'), 62 + [onPressTab], 63 + ) 64 + const onPressFeeds = React.useCallback( 65 + () => onPressTab('Feeds'), 61 66 [onPressTab], 62 67 ) 63 68 const onPressNotifications = React.useCallback( ··· 122 127 accessibilityHint="" 123 128 /> 124 129 <Btn 130 + testID="bottomBarFeedsBtn" 131 + icon={ 132 + isAtFeeds ? ( 133 + <SatelliteDishIconSolid 134 + size={25} 135 + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 136 + strokeWidth={1.8} 137 + /> 138 + ) : ( 139 + <SatelliteDishIcon 140 + size={25} 141 + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 142 + strokeWidth={1.8} 143 + /> 144 + ) 145 + } 146 + onPress={onPressFeeds} 147 + accessibilityRole="tab" 148 + accessibilityLabel="Feeds" 149 + accessibilityHint="" 150 + /> 151 + <Btn 125 152 testID="bottomBarNotificationsBtn" 126 153 icon={ 127 154 isAtNotifications ? ( ··· 154 181 icon={ 155 182 <View style={styles.ctrlIconSizingWrapper}> 156 183 {isAtMyProfile ? ( 157 - <UserIconSolid 158 - size={28} 159 - strokeWidth={1.5} 160 - style={[styles.ctrlIcon, pal.text, styles.profileIcon]} 161 - /> 184 + <View 185 + style={[ 186 + styles.ctrlIcon, 187 + pal.text, 188 + styles.profileIcon, 189 + styles.onProfile, 190 + ]}> 191 + <UserAvatar avatar={store.me.avatar} size={27} /> 192 + </View> 162 193 ) : ( 163 - <UserIcon 164 - size={28} 165 - strokeWidth={1.5} 166 - style={[styles.ctrlIcon, pal.text, styles.profileIcon]} 167 - /> 194 + <View style={[styles.ctrlIcon, pal.text, styles.profileIcon]}> 195 + <UserAvatar avatar={store.me.avatar} size={28} /> 196 + </View> 168 197 )} 169 198 </View> 170 199 }
+5
src/view/shell/bottom-bar/BottomBarStyles.tsx
··· 58 58 profileIcon: { 59 59 top: -4, 60 60 }, 61 + onProfile: { 62 + borderColor: colors.black, 63 + borderWidth: 1, 64 + borderRadius: 100, 65 + }, 61 66 })
+14
src/view/shell/bottom-bar/BottomBarWeb.tsx
··· 15 15 HomeIconSolid, 16 16 MagnifyingGlassIcon2, 17 17 MagnifyingGlassIcon2Solid, 18 + SatelliteDishIcon, 19 + SatelliteDishIconSolid, 18 20 UserIcon, 19 21 } from 'lib/icons' 20 22 import {Link} from 'view/com/util/Link' ··· 52 54 const Icon = isActive 53 55 ? MagnifyingGlassIcon2Solid 54 56 : MagnifyingGlassIcon2 57 + return ( 58 + <Icon 59 + size={25} 60 + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 61 + strokeWidth={1.8} 62 + /> 63 + ) 64 + }} 65 + </NavItem> 66 + <NavItem routeName="Feeds" href="/feeds"> 67 + {({isActive}) => { 68 + const Icon = isActive ? SatelliteDishIconSolid : SatelliteDishIcon 55 69 return ( 56 70 <Icon 57 71 size={25}
+20
src/view/shell/desktop/LeftNav.tsx
··· 30 30 CogIconSolid, 31 31 ComposeIcon2, 32 32 HandIcon, 33 + SatelliteDishIcon, 34 + SatelliteDishIconSolid, 33 35 } from 'lib/icons' 34 36 import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers' 35 37 import {NavigationProp} from 'lib/routes/types' ··· 194 196 /> 195 197 } 196 198 label="Search" 199 + /> 200 + <NavItem 201 + href="/feeds" 202 + icon={ 203 + <SatelliteDishIcon 204 + strokeWidth={1.75} 205 + style={pal.text as FontAwesomeIconStyle} 206 + size={24} 207 + /> 208 + } 209 + iconFilled={ 210 + <SatelliteDishIconSolid 211 + strokeWidth={1.75} 212 + style={pal.text as FontAwesomeIconStyle} 213 + size={24} 214 + /> 215 + } 216 + label="My Feeds" 197 217 /> 198 218 <NavItem 199 219 href="/notifications"
+521 -496
yarn.lock
··· 30 30 leven "^3.1.0" 31 31 32 32 "@atproto/api@*": 33 - version "0.2.11" 34 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.11.tgz#53b70b0f4942b2e2dd5cb46433f133cde83917bf" 35 - integrity sha512-5JY1Ii/81Bcy1ZTGRqALsaOdc8fIJTSlMNoSptpGH73uAPQE93weDrb8sc3KoxWi1G2ss3IIBSLPJWxALocJSQ== 33 + version "0.3.3" 34 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.3.tgz#8c8d41567beb7b37217f76d2aacf2c280e9fd07e" 35 + integrity sha512-BlgpYbdPO0KSBypg2KgqHM0kS2Pk82P3X0w2rJs/vrdcMl72d2WeI9kQ5PPFiz80p6C6XcLcpnzzKKtQeFvh4A== 36 36 dependencies: 37 37 "@atproto/common-web" "*" 38 38 "@atproto/uri" "*" ··· 40 40 tlds "^1.234.0" 41 41 typed-emitter "^2.1.0" 42 42 43 - "@atproto/api@0.3.3": 44 - version "0.3.3" 45 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.3.tgz#8c8d41567beb7b37217f76d2aacf2c280e9fd07e" 46 - integrity sha512-BlgpYbdPO0KSBypg2KgqHM0kS2Pk82P3X0w2rJs/vrdcMl72d2WeI9kQ5PPFiz80p6C6XcLcpnzzKKtQeFvh4A== 43 + "@atproto/api@0.3.8": 44 + version "0.3.8" 45 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.8.tgz#3fc0ebd092cc212c2d0b31a600fe1945a02f9cf7" 46 + integrity sha512-7qaIZGEP5J9FW4z8bXezzAmLRzHSXXHo6bWP9Jyu5MLp8tYt9vG6yR2N0QA7GvO0xSYqP87Q5vblPjYXGqtDKg== 47 47 dependencies: 48 48 "@atproto/common-web" "*" 49 49 "@atproto/uri" "*" ··· 114 114 uint8arrays "3.0.0" 115 115 116 116 "@atproto/did-resolver@*": 117 - version "0.0.1" 118 - resolved "https://registry.yarnpkg.com/@atproto/did-resolver/-/did-resolver-0.0.1.tgz#e54c1b7fddff2cd6adf87c044b4a3b6f00d5eff7" 119 - integrity sha512-sdva3+nydMaWXwHJED558UZdVZuajfC2CHcsIZz0pQybicm3VI+khkf42ClZeOhf4Bwa4V4SOaaAqwyf86bDew== 117 + version "0.1.0" 118 + resolved "https://registry.yarnpkg.com/@atproto/did-resolver/-/did-resolver-0.1.0.tgz#58f42447700aaad61bad2f0d70b721966268aa02" 119 + integrity sha512-ztljyMMCqXvJSi/Qqa2zEQFvOm1AUUR7Bybr3cM1BCddbhW46gk6/g8BgdZeDt2sMBdye37qTctR9O/FjhigvQ== 120 120 dependencies: 121 - "@atproto/common" "*" 121 + "@atproto/common-web" "*" 122 122 "@atproto/crypto" "*" 123 - axios "^0.24.0" 124 - did-resolver "^4.0.0" 123 + axios "^0.27.2" 124 + zod "^3.14.2" 125 125 126 126 "@atproto/identifier@*": 127 127 version "0.1.0" ··· 148 148 resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4" 149 149 integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw== 150 150 151 - "@atproto/pds@^0.1.8": 152 - version "0.1.8" 153 - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.8.tgz#cf1a9bab2301c3fe1120c63576153ac5a20bf70d" 154 - integrity sha512-I493U+/NNU9D8L8tVbM/OpD6gQ6/Mv7uE+/i4a1vfBGO6NqYJ6jKw3qeCy4jq3NVbTxcs+lSSpK27hgApx4PtA== 151 + "@atproto/pds@^0.1.10": 152 + version "0.1.10" 153 + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.10.tgz#cde4a06982ec0ba7166e978afed78f58abc5c6b1" 154 + integrity sha512-Yxnpv3mQNrIcR5GFPUUoffSSDpZHzXXHuk36wtPXm7dDSg+ACgtILPcDSpkjr27JwE5OcfgD2UbQwXt7az7OLA== 155 155 dependencies: 156 156 "@atproto/api" "*" 157 157 "@atproto/common" "*" ··· 212 212 "@atproto/nsid" "*" 213 213 214 214 "@atproto/xrpc-server@*": 215 - version "0.1.0" 216 - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.1.0.tgz#2dd3172bb35fbfefb98c3d727d29be8eca5c3d9b" 217 - integrity sha512-I7EjhnLUrlqQKTe2jDEnyAaOTvj26pg9NRjTXflbIOqCOkh+K9+5ztGSI0djF7TSQ7pegXroj3qRnmpVVCBr7Q== 215 + version "0.2.0" 216 + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.2.0.tgz#a36616c2ac70339cd79cda83ede0a0b305c74f9b" 217 + integrity sha512-sCJuVUIb1tDIlKCFwHPRHbAgEy0HYGlQ7XhpNqMRKXECh8Z+DRICEne3gLDVaXhyNaC/N7OjHcsyuofDDbuGFQ== 218 218 dependencies: 219 219 "@atproto/common" "*" 220 + "@atproto/crypto" "*" 220 221 "@atproto/lexicon" "*" 221 222 cbor-x "^1.5.1" 222 223 express "^4.17.2" ··· 254 255 integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA== 255 256 256 257 "@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.16.0", "@babel/core@^7.20.0", "@babel/core@^7.20.2", "@babel/core@^7.7.2", "@babel/core@^7.8.0": 257 - version "7.21.5" 258 - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.5.tgz#92f753e8b9f96e15d4b398dbe2f25d1408c9c426" 259 - integrity sha512-9M398B/QH5DlfCOTKDZT1ozXr0x8uBEeFd+dJraGUZGiaNpGCDVGCc14hZexsMblw3XxltJ+6kSvogp9J+5a9g== 258 + version "7.21.8" 259 + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4" 260 + integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== 260 261 dependencies: 261 262 "@ampproject/remapping" "^2.2.0" 262 263 "@babel/code-frame" "^7.21.4" ··· 264 265 "@babel/helper-compilation-targets" "^7.21.5" 265 266 "@babel/helper-module-transforms" "^7.21.5" 266 267 "@babel/helpers" "^7.21.5" 267 - "@babel/parser" "^7.21.5" 268 + "@babel/parser" "^7.21.8" 268 269 "@babel/template" "^7.20.7" 269 270 "@babel/traverse" "^7.21.5" 270 271 "@babel/types" "^7.21.5" ··· 275 276 semver "^6.3.0" 276 277 277 278 "@babel/eslint-parser@^7.16.3", "@babel/eslint-parser@^7.18.2": 278 - version "7.21.3" 279 - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.21.3.tgz#d79e822050f2de65d7f368a076846e7184234af7" 280 - integrity sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg== 279 + version "7.21.8" 280 + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.21.8.tgz#59fb6fc4f3b017ab86987c076226ceef7b2b2ef2" 281 + integrity sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ== 281 282 dependencies: 282 283 "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" 283 284 eslint-visitor-keys "^2.1.0" ··· 319 320 semver "^6.3.0" 320 321 321 322 "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": 322 - version "7.21.5" 323 - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.5.tgz#09a259305467d2020bd2492119ee1c1bc55029e9" 324 - integrity sha512-yNSEck9SuDvPTEUYm4BSXl6ZVC7yO5ZLEMAhG3v3zi7RDxyL/nQDemWWZmw4L0stPWwhpnznRRyJHPRcbXR2jw== 323 + version "7.21.8" 324 + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.8.tgz#205b26330258625ef8869672ebca1e0dee5a0f02" 325 + integrity sha512-+THiN8MqiH2AczyuZrnrKL6cAxFRRQDKW9h1YkBvbgKmAm6mwiacig1qT73DHIWMGo40GRnsEfN3LA+E6NtmSw== 325 326 dependencies: 326 327 "@babel/helper-annotate-as-pure" "^7.18.6" 327 328 "@babel/helper-environment-visitor" "^7.21.5" ··· 334 335 semver "^6.3.0" 335 336 336 337 "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": 337 - version "7.21.5" 338 - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.5.tgz#4ce6ffaf497a241aa6c62192416b273987a8daa3" 339 - integrity sha512-1+DPMcln46eNAta/rPIqQYXYRGvQ/LRy6bRKnSt9Dzt/yLjNUbbsh+6yzD6fUHmtzc9kWvVnAhtcMSMyziHmUA== 338 + version "7.21.8" 339 + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.8.tgz#a7886f61c2e29e21fd4aaeaf1e473deba6b571dc" 340 + integrity sha512-zGuSdedkFtsFHGbexAvNuipg1hbtitDLo2XE8/uf6Y9sOQV1xsYX/2pNbtedp/X0eU1pIt+kGvaqHCowkRbS5g== 340 341 dependencies: 341 342 "@babel/helper-annotate-as-pure" "^7.18.6" 342 343 regexpu-core "^5.3.1" ··· 500 501 chalk "^2.0.0" 501 502 js-tokens "^4.0.0" 502 503 503 - "@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5": 504 - version "7.21.5" 505 - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.5.tgz#821bb520118fd25b982eaf8d37421cf5c64a312b" 506 - integrity sha512-J+IxH2IsxV4HbnTrSWgMAQj0UEo61hDA4Ny8h8PCX0MLXiibqHbqIOVneqdocemSBc22VpBKxt4J6FQzy9HarQ== 504 + "@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8": 505 + version "7.21.8" 506 + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" 507 + integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== 507 508 508 509 "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": 509 510 version "7.18.6" ··· 1279 1280 "@babel/plugin-transform-react-jsx-development" "^7.18.6" 1280 1281 "@babel/plugin-transform-react-pure-annotations" "^7.18.6" 1281 1282 1282 - "@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.0", "@babel/preset-typescript@^7.16.7": 1283 + "@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.0", "@babel/preset-typescript@^7.16.7", "@babel/preset-typescript@^7.17.12": 1283 1284 version "7.21.5" 1284 1285 resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.5.tgz#68292c884b0e26070b4d66b202072d391358395f" 1285 1286 integrity sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA== ··· 1579 1580 resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" 1580 1581 integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== 1581 1582 1582 - "@eslint/eslintrc@^2.0.2": 1583 - version "2.0.2" 1584 - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.2.tgz#01575e38707add677cf73ca1589abba8da899a02" 1585 - integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ== 1583 + "@eslint/eslintrc@^2.0.3": 1584 + version "2.0.3" 1585 + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" 1586 + integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== 1586 1587 dependencies: 1587 1588 ajv "^6.12.4" 1588 1589 debug "^4.3.2" 1589 - espree "^9.5.1" 1590 + espree "^9.5.2" 1590 1591 globals "^13.19.0" 1591 1592 ignore "^5.2.0" 1592 1593 import-fresh "^3.2.1" ··· 1594 1595 minimatch "^3.1.2" 1595 1596 strip-json-comments "^3.1.1" 1596 1597 1597 - "@eslint/js@8.39.0": 1598 - version "8.39.0" 1599 - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" 1600 - integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== 1598 + "@eslint/js@8.40.0": 1599 + version "8.40.0" 1600 + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.40.0.tgz#3ba73359e11f5a7bd3e407f70b3528abfae69cec" 1601 + integrity sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA== 1601 1602 1602 1603 "@expo/bunyan@4.0.0", "@expo/bunyan@^4.0.0": 1603 1604 version "4.0.0" ··· 1705 1706 xcode "^3.0.1" 1706 1707 xml2js "0.4.23" 1707 1708 1708 - "@expo/config-plugins@6.0.1", "@expo/config-plugins@~6.0.0": 1709 + "@expo/config-plugins@6.0.2": 1710 + version "6.0.2" 1711 + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-6.0.2.tgz#cf07319515022ba94d9aa9fa30e0cff43a14256f" 1712 + integrity sha512-Cn01fXMHwjU042EgO9oO3Mna0o/UCrW91MQLMbJa4pXM41CYGjNgVy1EVXiuRRx/upegHhvltBw5D+JaUm8aZQ== 1713 + dependencies: 1714 + "@expo/config-types" "^48.0.0" 1715 + "@expo/json-file" "~8.2.37" 1716 + "@expo/plist" "^0.0.20" 1717 + "@expo/sdk-runtime-versions" "^1.0.0" 1718 + "@react-native/normalize-color" "^2.0.0" 1719 + chalk "^4.1.2" 1720 + debug "^4.3.1" 1721 + find-up "~5.0.0" 1722 + getenv "^1.0.0" 1723 + glob "7.1.6" 1724 + resolve-from "^5.0.0" 1725 + semver "^7.3.5" 1726 + slash "^3.0.0" 1727 + xcode "^3.0.1" 1728 + xml2js "0.4.23" 1729 + 1730 + "@expo/config-plugins@~6.0.0": 1709 1731 version "6.0.1" 1710 1732 resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-6.0.1.tgz#827cb34c51f725d8825b0768df6550c1cf81d457" 1711 1733 integrity sha512-6mqZutxeibXFeqFfoZApFUEH2n1RxGXYMHCdJrDj4eXDBBFZ3aJ0XBoroZcHHHvfRieEsf54vNyJoWp7JZGj8g== ··· 2656 2678 picocolors "^1.0.0" 2657 2679 2658 2680 "@linaria/tags@^4.3.4": 2659 - version "4.3.4" 2660 - resolved "https://registry.yarnpkg.com/@linaria/tags/-/tags-4.3.4.tgz#3c98108e4b48b8413662b4c62c2b2abdebacaca4" 2661 - integrity sha512-W8zaLKtC4YFCwkZ9DMu2enCiD/zGyYmFSTzEvJP7ZycdftMizoOrWNOyF9kITyjGdq+jZvAXJz0BZDT6axgIRg== 2681 + version "4.3.5" 2682 + resolved "https://registry.yarnpkg.com/@linaria/tags/-/tags-4.3.5.tgz#bf2e070d11179addf2f27a66cd29d8192e71ca89" 2683 + integrity sha512-PgaIi8Vv89YOjc6rpKL/uPg2w4k0rAwAYxcqeXqzKqsEAste5rgB8xp1/KUOG0oAOkPd3MRL6Duj+m0ZwJ3g+g== 2662 2684 dependencies: 2663 2685 "@babel/generator" "^7.20.4" 2664 2686 "@linaria/logger" "^4.0.0" 2665 - "@linaria/utils" "^4.3.3" 2687 + "@linaria/utils" "^4.3.4" 2666 2688 2667 - "@linaria/utils@^4.3.3": 2668 - version "4.3.3" 2669 - resolved "https://registry.yarnpkg.com/@linaria/utils/-/utils-4.3.3.tgz#9f66ae41187e8a2f2cc3471b44935128ebd1dab3" 2670 - integrity sha512-xSe/tod9A44aIMbtds9fWLNe2TT080lLdRSaoqX+UHsBWqClkrw5cXEt3lm8Vr4hZiXT2r/1AldjuHb9YbUlMg== 2689 + "@linaria/utils@^4.3.3", "@linaria/utils@^4.3.4": 2690 + version "4.3.4" 2691 + resolved "https://registry.yarnpkg.com/@linaria/utils/-/utils-4.3.4.tgz#860db9131e498b62510e49dc6fd4a8f0ed44bf4d" 2692 + integrity sha512-vt6WJG54n+KANaqxOfzIIU7aSfFHEWFbnGLsgxL7nASHqO0zezrNA2y2Rrp80zSeTW+wSpbmDM4uJyC9UW1qoA== 2671 2693 dependencies: 2672 2694 "@babel/core" "^7.20.2" 2673 2695 "@babel/plugin-proposal-export-namespace-from" "^7.18.9" ··· 2794 2816 integrity sha512-bHyZVW62TuleiZsXNHS1Pv16fWc0fh8O9WvBzl4h2fykqZRW9a+Pv/RGTH56E3X2PqzHP38K5go8zmCZUoIsoQ== 2795 2817 2796 2818 "@react-native-community/blur@^4.3.0": 2797 - version "4.3.1" 2798 - resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.1.tgz#817a9b9762f738e578a2cd5306902f4510a6df34" 2799 - integrity sha512-XVjTKs+nSXG7DCmxIr7HSjeAB276OO9KZ7XUVCdjK+RGTlvlCRZIPV0ygi+WN87zsdvfWsQOTZv3k0/BI86gsA== 2819 + version "4.3.2" 2820 + resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.2.tgz#185a2c7dd03ba168cc95069bc4742e9505fd6c6c" 2821 + integrity sha512-0ID+pyZKdC4RdgC7HePxUQ6JmsbNrgz03u+6SgqYpmBoK/rE+7JffqIw7IEsfoKitLEcRNLGekIBsfwCqiEkew== 2800 2822 2801 2823 "@react-native-community/cli-clean@^10.1.1": 2802 2824 version "10.1.1" ··· 3143 3165 resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" 3144 3166 integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== 3145 3167 3146 - "@segment/analytics-core@1.2.4": 3147 - version "1.2.4" 3148 - resolved "https://registry.yarnpkg.com/@segment/analytics-core/-/analytics-core-1.2.4.tgz#a01f0c87246292e0b9790e12c73d2f7e5fceb168" 3149 - integrity sha512-M16osD6+z/bQPSVCZdlU+BAhCk968ppi+SGxU2gVa4B196Qr8SEkBPr3NxUCGTSoULo4/T+k8Ea5cF+pXlgf6Q== 3168 + "@segment/analytics-core@1.2.5": 3169 + version "1.2.5" 3170 + resolved "https://registry.yarnpkg.com/@segment/analytics-core/-/analytics-core-1.2.5.tgz#bd17034be9393aa245a684f137a7950debee240b" 3171 + integrity sha512-T+AyZe4eKAO08T138RcvCTqUCT609H07VQL6+kh58VJiIalowrnqSdWf9rc+AsUMvPnxUOsnyGBOqNdZ5KNqYw== 3150 3172 dependencies: 3151 3173 "@lukeed/uuid" "^2.0.0" 3152 3174 dset "^3.1.2" 3153 3175 tslib "^2.4.1" 3154 3176 3155 3177 "@segment/analytics-next@^1.51.3": 3156 - version "1.51.6" 3157 - resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.51.6.tgz#56c99782fc333025906dbf8f5efd9b7f9f87c197" 3158 - integrity sha512-SiuuCHLq2sWM3fwF0peQ9J9Ku+FAbsx5XsGl9pL4rfHoaItlQuBETxkmT3BD2YltFxhr4FCQ5+phdT0/X5QUFA== 3178 + version "1.51.7" 3179 + resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.51.7.tgz#f0d3c9b06b2ff5e46802a0ec8e9f676b41bd3658" 3180 + integrity sha512-WXrgQPkB4MBa4Op4X1SKZL/WEBX3vM04jChHClXZK3QcMAezy4A0vblcHK9vtm8RGOuiickVuemU7ZN3/iaGPA== 3159 3181 dependencies: 3160 3182 "@lukeed/uuid" "^2.0.0" 3161 - "@segment/analytics-core" "1.2.4" 3183 + "@segment/analytics-core" "1.2.5" 3162 3184 "@segment/analytics.js-video-plugins" "^0.2.1" 3163 3185 "@segment/facade" "^3.4.9" 3164 3186 "@segment/tsub" "1.0.1" ··· 3170 3192 unfetch "^4.1.0" 3171 3193 3172 3194 "@segment/analytics-react-native@^2.10.1": 3173 - version "2.13.5" 3174 - resolved "https://registry.yarnpkg.com/@segment/analytics-react-native/-/analytics-react-native-2.13.5.tgz#e8373d1584812afbe39e9fb935b83655d15ce750" 3175 - integrity sha512-uWezHOghP3yf3tgEfpe2OxP/54l9SM7+YNwkrFhumhoe4cw4xTptlFi6zU4p8lRdmmoJQgQ+/rh3AUP/i4yFTA== 3195 + version "2.14.0" 3196 + resolved "https://registry.yarnpkg.com/@segment/analytics-react-native/-/analytics-react-native-2.14.0.tgz#24727b9fee5c559a2ce8e67df6a23dd92670273d" 3197 + integrity sha512-614qdb4pncYZJw09pvHd1cQURHYkbTQOWAPo262F98ouxiiR7SAHPQv0vRrkflhiDyHm9glez0PY168J+Mw0jg== 3176 3198 dependencies: 3177 3199 "@segment/sovran-react-native" "^1" 3178 3200 deepmerge "^4.2.2" ··· 3427 3449 dependencies: 3428 3450 type-detect "4.0.8" 3429 3451 3430 - "@sinonjs/commons@^2.0.0": 3431 - version "2.0.0" 3432 - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" 3433 - integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== 3452 + "@sinonjs/commons@^3.0.0": 3453 + version "3.0.0" 3454 + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" 3455 + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== 3434 3456 dependencies: 3435 3457 type-detect "4.0.8" 3436 3458 3437 3459 "@sinonjs/fake-timers@^10.0.2": 3438 - version "10.0.2" 3439 - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" 3440 - integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== 3460 + version "10.1.0" 3461 + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.1.0.tgz#3595e42b3f0a7df80a9681cf58d8cb418eac1e99" 3462 + integrity sha512-w1qd368vtrwttm1PRJWPW1QHlbmHrVDGs1eBH/jZvRPUFS4MNXV9Q33EQdjOdeAxZ7O8+3wM7zxztm2nfUSyKw== 3441 3463 dependencies: 3442 - "@sinonjs/commons" "^2.0.0" 3464 + "@sinonjs/commons" "^3.0.0" 3443 3465 3444 3466 "@sinonjs/fake-timers@^8.0.1": 3445 3467 version "8.1.0" ··· 4561 4583 integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== 4562 4584 4563 4585 "@tsconfig/node16@^1.0.2": 4564 - version "1.0.3" 4565 - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" 4566 - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== 4586 + version "1.0.4" 4587 + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" 4588 + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== 4567 4589 4568 4590 "@tsconfig/react-native@^2.0.3": 4569 4591 version "2.0.3" ··· 4660 4682 integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== 4661 4683 4662 4684 "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": 4663 - version "4.17.34" 4664 - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz#c119e85b75215178bc127de588e93100698ab4cc" 4665 - integrity sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w== 4685 + version "4.17.35" 4686 + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" 4687 + integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== 4666 4688 dependencies: 4667 4689 "@types/node" "*" 4668 4690 "@types/qs" "*" ··· 4839 4861 integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== 4840 4862 4841 4863 "@types/node@*": 4842 - version "18.16.3" 4843 - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.3.tgz#6bda7819aae6ea0b386ebc5b24bdf602f1b42b01" 4844 - integrity sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q== 4864 + version "20.1.7" 4865 + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.7.tgz#ce10c802f7731909d0a44ac9888e8b3a9125eb62" 4866 + integrity sha512-WCuw/o4GSwDGMoonES8rcvwsig77dGCMbZDrZr2x4ZZiNW4P/gcoZXe/0twgtobcTkmg9TuKflxYL/DuwDyJzg== 4845 4867 4846 4868 "@types/node@^18.16.2": 4847 - version "18.16.2" 4848 - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.2.tgz#2f610ea71034b3971c312192377f8a7178eb57f1" 4849 - integrity sha512-GQW/JL/5Fz/0I8RpeBG9lKp0+aNcXEaVL71c0D2Q0QHDTFvlYKT7an0onCUXj85anv7b4/WesqdfchLc0jtsCg== 4869 + version "18.16.12" 4870 + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.12.tgz#f11e19055c5b3daeb79dc6eb7ccdd3d036313034" 4871 + integrity sha512-tIRrjbY9C277MOfP8M3zjMIhtMlUJ6YVqkGgLjz+74jVsdf4/UjC6Hku4+1N0BS0qyC0JAS6tJLUk9H6JUKviQ== 4850 4872 4851 4873 "@types/object.omit@^3.0.0": 4852 4874 version "3.0.0" ··· 4917 4939 "@types/react" "^17" 4918 4940 4919 4941 "@types/react@*", "@types/react@^17": 4920 - version "17.0.58" 4921 - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.58.tgz#c8bbc82114e5c29001548ebe8ed6c4ba4d3c9fb0" 4922 - integrity sha512-c1GzVY97P0fGxwGxhYq989j4XwlcHQoto6wQISOC2v6wm3h0PORRWJFHlkRjfGsiG3y1609WdQ+J+tKxvrEd6A== 4942 + version "17.0.59" 4943 + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.59.tgz#5aa4e161a356fcb824d81f166e01bad9e82243bb" 4944 + integrity sha512-gSON5zWYIGyoBcycCE75E9+r6dCC2dHdsrVkOEiIYNU5+Q28HcBAuqvDuxHcCbMfHBHdeT5Tva/AFn3rnMKE4g== 4923 4945 dependencies: 4924 4946 "@types/prop-types" "*" 4925 4947 "@types/scheduler" "*" ··· 4943 4965 integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== 4944 4966 4945 4967 "@types/semver@^7.3.12": 4946 - version "7.3.13" 4947 - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" 4948 - integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== 4968 + version "7.5.0" 4969 + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" 4970 + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== 4949 4971 4950 4972 "@types/send@*": 4951 4973 version "0.17.1" ··· 5031 5053 "@types/yargs-parser" "*" 5032 5054 5033 5055 "@typescript-eslint/eslint-plugin@^5.30.5", "@typescript-eslint/eslint-plugin@^5.48.2", "@typescript-eslint/eslint-plugin@^5.5.0": 5034 - version "5.59.2" 5035 - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz#684a2ce7182f3b4dac342eef7caa1c2bae476abd" 5036 - integrity sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A== 5056 + version "5.59.6" 5057 + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.6.tgz#a350faef1baa1e961698240f922d8de1761a9e2b" 5058 + integrity sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw== 5037 5059 dependencies: 5038 5060 "@eslint-community/regexpp" "^4.4.0" 5039 - "@typescript-eslint/scope-manager" "5.59.2" 5040 - "@typescript-eslint/type-utils" "5.59.2" 5041 - "@typescript-eslint/utils" "5.59.2" 5061 + "@typescript-eslint/scope-manager" "5.59.6" 5062 + "@typescript-eslint/type-utils" "5.59.6" 5063 + "@typescript-eslint/utils" "5.59.6" 5042 5064 debug "^4.3.4" 5043 5065 grapheme-splitter "^1.0.4" 5044 5066 ignore "^5.2.0" ··· 5047 5069 tsutils "^3.21.0" 5048 5070 5049 5071 "@typescript-eslint/experimental-utils@^5.0.0": 5050 - version "5.59.2" 5051 - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.59.2.tgz#c2785247c4c8929cb6946e46280ea44f54d9cf79" 5052 - integrity sha512-JLw2UImsjHDuVukpA8Nt+UK7JKE/LQAeV3tU5f7wJo2/NNYVwcakzkWjoYzu/2qzWY/Z9c7zojngNDfecNt92g== 5072 + version "5.59.6" 5073 + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.59.6.tgz#9f4d81700dcea51a107658a44992ba1e4d8b4320" 5074 + integrity sha512-UIVfEaaHggOuhgqdpFlFQ7IN9UFMCiBR/N7uPBUyUlwNdJzYfAu9m4wbOj0b59oI/HSPW1N63Q7lsvfwTQY13w== 5053 5075 dependencies: 5054 - "@typescript-eslint/utils" "5.59.2" 5076 + "@typescript-eslint/utils" "5.59.6" 5055 5077 5056 5078 "@typescript-eslint/parser@^5.30.5", "@typescript-eslint/parser@^5.48.2", "@typescript-eslint/parser@^5.5.0": 5057 - version "5.59.2" 5058 - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.2.tgz#c2c443247901d95865b9f77332d9eee7c55655e8" 5059 - integrity sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ== 5079 + version "5.59.6" 5080 + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.6.tgz#bd36f71f5a529f828e20b627078d3ed6738dbb40" 5081 + integrity sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA== 5060 5082 dependencies: 5061 - "@typescript-eslint/scope-manager" "5.59.2" 5062 - "@typescript-eslint/types" "5.59.2" 5063 - "@typescript-eslint/typescript-estree" "5.59.2" 5083 + "@typescript-eslint/scope-manager" "5.59.6" 5084 + "@typescript-eslint/types" "5.59.6" 5085 + "@typescript-eslint/typescript-estree" "5.59.6" 5064 5086 debug "^4.3.4" 5065 5087 5066 - "@typescript-eslint/scope-manager@5.59.2": 5067 - version "5.59.2" 5068 - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz#f699fe936ee4e2c996d14f0fdd3a7da5ba7b9a4c" 5069 - integrity sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA== 5088 + "@typescript-eslint/scope-manager@5.59.6": 5089 + version "5.59.6" 5090 + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz#d43a3687aa4433868527cfe797eb267c6be35f19" 5091 + integrity sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ== 5070 5092 dependencies: 5071 - "@typescript-eslint/types" "5.59.2" 5072 - "@typescript-eslint/visitor-keys" "5.59.2" 5093 + "@typescript-eslint/types" "5.59.6" 5094 + "@typescript-eslint/visitor-keys" "5.59.6" 5073 5095 5074 - "@typescript-eslint/type-utils@5.59.2": 5075 - version "5.59.2" 5076 - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz#0729c237503604cd9a7084b5af04c496c9a4cdcf" 5077 - integrity sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ== 5096 + "@typescript-eslint/type-utils@5.59.6": 5097 + version "5.59.6" 5098 + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.6.tgz#37c51d2ae36127d8b81f32a0a4d2efae19277c48" 5099 + integrity sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ== 5078 5100 dependencies: 5079 - "@typescript-eslint/typescript-estree" "5.59.2" 5080 - "@typescript-eslint/utils" "5.59.2" 5101 + "@typescript-eslint/typescript-estree" "5.59.6" 5102 + "@typescript-eslint/utils" "5.59.6" 5081 5103 debug "^4.3.4" 5082 5104 tsutils "^3.21.0" 5083 5105 5084 - "@typescript-eslint/types@5.59.2": 5085 - version "5.59.2" 5086 - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.2.tgz#b511d2b9847fe277c5cb002a2318bd329ef4f655" 5087 - integrity sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w== 5106 + "@typescript-eslint/types@5.59.6": 5107 + version "5.59.6" 5108 + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.6.tgz#5a6557a772af044afe890d77c6a07e8c23c2460b" 5109 + integrity sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA== 5088 5110 5089 - "@typescript-eslint/typescript-estree@5.59.2": 5090 - version "5.59.2" 5091 - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz#6e2fabd3ba01db5d69df44e0b654c0b051fe9936" 5092 - integrity sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q== 5111 + "@typescript-eslint/typescript-estree@5.59.6": 5112 + version "5.59.6" 5113 + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz#2fb80522687bd3825504925ea7e1b8de7bb6251b" 5114 + integrity sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA== 5093 5115 dependencies: 5094 - "@typescript-eslint/types" "5.59.2" 5095 - "@typescript-eslint/visitor-keys" "5.59.2" 5116 + "@typescript-eslint/types" "5.59.6" 5117 + "@typescript-eslint/visitor-keys" "5.59.6" 5096 5118 debug "^4.3.4" 5097 5119 globby "^11.1.0" 5098 5120 is-glob "^4.0.3" 5099 5121 semver "^7.3.7" 5100 5122 tsutils "^3.21.0" 5101 5123 5102 - "@typescript-eslint/utils@5.59.2", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.0": 5103 - version "5.59.2" 5104 - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.2.tgz#0c45178124d10cc986115885688db6abc37939f4" 5105 - integrity sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ== 5124 + "@typescript-eslint/utils@5.59.6", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.0": 5125 + version "5.59.6" 5126 + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.6.tgz#82960fe23788113fc3b1f9d4663d6773b7907839" 5127 + integrity sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg== 5106 5128 dependencies: 5107 5129 "@eslint-community/eslint-utils" "^4.2.0" 5108 5130 "@types/json-schema" "^7.0.9" 5109 5131 "@types/semver" "^7.3.12" 5110 - "@typescript-eslint/scope-manager" "5.59.2" 5111 - "@typescript-eslint/types" "5.59.2" 5112 - "@typescript-eslint/typescript-estree" "5.59.2" 5132 + "@typescript-eslint/scope-manager" "5.59.6" 5133 + "@typescript-eslint/types" "5.59.6" 5134 + "@typescript-eslint/typescript-estree" "5.59.6" 5113 5135 eslint-scope "^5.1.1" 5114 5136 semver "^7.3.7" 5115 5137 5116 - "@typescript-eslint/visitor-keys@5.59.2": 5117 - version "5.59.2" 5118 - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz#37a419dc2723a3eacbf722512b86d6caf7d3b750" 5119 - integrity sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig== 5138 + "@typescript-eslint/visitor-keys@5.59.6": 5139 + version "5.59.6" 5140 + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz#673fccabf28943847d0c8e9e8d008e3ada7be6bb" 5141 + integrity sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q== 5120 5142 dependencies: 5121 - "@typescript-eslint/types" "5.59.2" 5143 + "@typescript-eslint/types" "5.59.6" 5122 5144 eslint-visitor-keys "^3.3.0" 5123 5145 5124 5146 "@urql/core@2.3.6": ··· 5145 5167 "@urql/core" ">=2.3.1" 5146 5168 wonka "^4.0.14" 5147 5169 5148 - "@webassemblyjs/ast@1.11.5", "@webassemblyjs/ast@^1.11.5": 5149 - version "1.11.5" 5150 - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.5.tgz#6e818036b94548c1fb53b754b5cae3c9b208281c" 5151 - integrity sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ== 5170 + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": 5171 + version "1.11.6" 5172 + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" 5173 + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== 5152 5174 dependencies: 5153 - "@webassemblyjs/helper-numbers" "1.11.5" 5154 - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" 5175 + "@webassemblyjs/helper-numbers" "1.11.6" 5176 + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" 5155 5177 5156 - "@webassemblyjs/floating-point-hex-parser@1.11.5": 5157 - version "1.11.5" 5158 - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz#e85dfdb01cad16b812ff166b96806c050555f1b4" 5159 - integrity sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ== 5178 + "@webassemblyjs/floating-point-hex-parser@1.11.6": 5179 + version "1.11.6" 5180 + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" 5181 + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== 5160 5182 5161 - "@webassemblyjs/helper-api-error@1.11.5": 5162 - version "1.11.5" 5163 - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz#1e82fa7958c681ddcf4eabef756ce09d49d442d1" 5164 - integrity sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA== 5183 + "@webassemblyjs/helper-api-error@1.11.6": 5184 + version "1.11.6" 5185 + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" 5186 + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== 5165 5187 5166 - "@webassemblyjs/helper-buffer@1.11.5": 5167 - version "1.11.5" 5168 - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz#91381652ea95bb38bbfd270702351c0c89d69fba" 5169 - integrity sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg== 5188 + "@webassemblyjs/helper-buffer@1.11.6": 5189 + version "1.11.6" 5190 + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" 5191 + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== 5170 5192 5171 - "@webassemblyjs/helper-numbers@1.11.5": 5172 - version "1.11.5" 5173 - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz#23380c910d56764957292839006fecbe05e135a9" 5174 - integrity sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA== 5193 + "@webassemblyjs/helper-numbers@1.11.6": 5194 + version "1.11.6" 5195 + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" 5196 + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== 5175 5197 dependencies: 5176 - "@webassemblyjs/floating-point-hex-parser" "1.11.5" 5177 - "@webassemblyjs/helper-api-error" "1.11.5" 5198 + "@webassemblyjs/floating-point-hex-parser" "1.11.6" 5199 + "@webassemblyjs/helper-api-error" "1.11.6" 5178 5200 "@xtuc/long" "4.2.2" 5179 5201 5180 - "@webassemblyjs/helper-wasm-bytecode@1.11.5": 5181 - version "1.11.5" 5182 - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz#e258a25251bc69a52ef817da3001863cc1c24b9f" 5183 - integrity sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA== 5202 + "@webassemblyjs/helper-wasm-bytecode@1.11.6": 5203 + version "1.11.6" 5204 + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" 5205 + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== 5184 5206 5185 - "@webassemblyjs/helper-wasm-section@1.11.5": 5186 - version "1.11.5" 5187 - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz#966e855a6fae04d5570ad4ec87fbcf29b42ba78e" 5188 - integrity sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA== 5207 + "@webassemblyjs/helper-wasm-section@1.11.6": 5208 + version "1.11.6" 5209 + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" 5210 + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== 5189 5211 dependencies: 5190 - "@webassemblyjs/ast" "1.11.5" 5191 - "@webassemblyjs/helper-buffer" "1.11.5" 5192 - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" 5193 - "@webassemblyjs/wasm-gen" "1.11.5" 5212 + "@webassemblyjs/ast" "1.11.6" 5213 + "@webassemblyjs/helper-buffer" "1.11.6" 5214 + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" 5215 + "@webassemblyjs/wasm-gen" "1.11.6" 5194 5216 5195 - "@webassemblyjs/ieee754@1.11.5": 5196 - version "1.11.5" 5197 - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz#b2db1b33ce9c91e34236194c2b5cba9b25ca9d60" 5198 - integrity sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg== 5217 + "@webassemblyjs/ieee754@1.11.6": 5218 + version "1.11.6" 5219 + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" 5220 + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== 5199 5221 dependencies: 5200 5222 "@xtuc/ieee754" "^1.2.0" 5201 5223 5202 - "@webassemblyjs/leb128@1.11.5": 5203 - version "1.11.5" 5204 - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.5.tgz#482e44d26b6b949edf042a8525a66c649e38935a" 5205 - integrity sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ== 5224 + "@webassemblyjs/leb128@1.11.6": 5225 + version "1.11.6" 5226 + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" 5227 + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== 5206 5228 dependencies: 5207 5229 "@xtuc/long" "4.2.2" 5208 5230 5209 - "@webassemblyjs/utf8@1.11.5": 5210 - version "1.11.5" 5211 - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.5.tgz#83bef94856e399f3740e8df9f63bc47a987eae1a" 5212 - integrity sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ== 5231 + "@webassemblyjs/utf8@1.11.6": 5232 + version "1.11.6" 5233 + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" 5234 + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== 5213 5235 5214 5236 "@webassemblyjs/wasm-edit@^1.11.5": 5215 - version "1.11.5" 5216 - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz#93ee10a08037657e21c70de31c47fdad6b522b2d" 5217 - integrity sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ== 5237 + version "1.11.6" 5238 + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" 5239 + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== 5218 5240 dependencies: 5219 - "@webassemblyjs/ast" "1.11.5" 5220 - "@webassemblyjs/helper-buffer" "1.11.5" 5221 - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" 5222 - "@webassemblyjs/helper-wasm-section" "1.11.5" 5223 - "@webassemblyjs/wasm-gen" "1.11.5" 5224 - "@webassemblyjs/wasm-opt" "1.11.5" 5225 - "@webassemblyjs/wasm-parser" "1.11.5" 5226 - "@webassemblyjs/wast-printer" "1.11.5" 5241 + "@webassemblyjs/ast" "1.11.6" 5242 + "@webassemblyjs/helper-buffer" "1.11.6" 5243 + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" 5244 + "@webassemblyjs/helper-wasm-section" "1.11.6" 5245 + "@webassemblyjs/wasm-gen" "1.11.6" 5246 + "@webassemblyjs/wasm-opt" "1.11.6" 5247 + "@webassemblyjs/wasm-parser" "1.11.6" 5248 + "@webassemblyjs/wast-printer" "1.11.6" 5227 5249 5228 - "@webassemblyjs/wasm-gen@1.11.5": 5229 - version "1.11.5" 5230 - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz#ceb1c82b40bf0cf67a492c53381916756ef7f0b1" 5231 - integrity sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA== 5250 + "@webassemblyjs/wasm-gen@1.11.6": 5251 + version "1.11.6" 5252 + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" 5253 + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== 5232 5254 dependencies: 5233 - "@webassemblyjs/ast" "1.11.5" 5234 - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" 5235 - "@webassemblyjs/ieee754" "1.11.5" 5236 - "@webassemblyjs/leb128" "1.11.5" 5237 - "@webassemblyjs/utf8" "1.11.5" 5255 + "@webassemblyjs/ast" "1.11.6" 5256 + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" 5257 + "@webassemblyjs/ieee754" "1.11.6" 5258 + "@webassemblyjs/leb128" "1.11.6" 5259 + "@webassemblyjs/utf8" "1.11.6" 5238 5260 5239 - "@webassemblyjs/wasm-opt@1.11.5": 5240 - version "1.11.5" 5241 - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz#b52bac29681fa62487e16d3bb7f0633d5e62ca0a" 5242 - integrity sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw== 5261 + "@webassemblyjs/wasm-opt@1.11.6": 5262 + version "1.11.6" 5263 + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" 5264 + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== 5243 5265 dependencies: 5244 - "@webassemblyjs/ast" "1.11.5" 5245 - "@webassemblyjs/helper-buffer" "1.11.5" 5246 - "@webassemblyjs/wasm-gen" "1.11.5" 5247 - "@webassemblyjs/wasm-parser" "1.11.5" 5266 + "@webassemblyjs/ast" "1.11.6" 5267 + "@webassemblyjs/helper-buffer" "1.11.6" 5268 + "@webassemblyjs/wasm-gen" "1.11.6" 5269 + "@webassemblyjs/wasm-parser" "1.11.6" 5248 5270 5249 - "@webassemblyjs/wasm-parser@1.11.5", "@webassemblyjs/wasm-parser@^1.11.5": 5250 - version "1.11.5" 5251 - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz#7ba0697ca74c860ea13e3ba226b29617046982e2" 5252 - integrity sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew== 5271 + "@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": 5272 + version "1.11.6" 5273 + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" 5274 + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== 5253 5275 dependencies: 5254 - "@webassemblyjs/ast" "1.11.5" 5255 - "@webassemblyjs/helper-api-error" "1.11.5" 5256 - "@webassemblyjs/helper-wasm-bytecode" "1.11.5" 5257 - "@webassemblyjs/ieee754" "1.11.5" 5258 - "@webassemblyjs/leb128" "1.11.5" 5259 - "@webassemblyjs/utf8" "1.11.5" 5276 + "@webassemblyjs/ast" "1.11.6" 5277 + "@webassemblyjs/helper-api-error" "1.11.6" 5278 + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" 5279 + "@webassemblyjs/ieee754" "1.11.6" 5280 + "@webassemblyjs/leb128" "1.11.6" 5281 + "@webassemblyjs/utf8" "1.11.6" 5260 5282 5261 - "@webassemblyjs/wast-printer@1.11.5": 5262 - version "1.11.5" 5263 - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz#7a5e9689043f3eca82d544d7be7a8e6373a6fa98" 5264 - integrity sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA== 5283 + "@webassemblyjs/wast-printer@1.11.6": 5284 + version "1.11.6" 5285 + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" 5286 + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== 5265 5287 dependencies: 5266 - "@webassemblyjs/ast" "1.11.5" 5288 + "@webassemblyjs/ast" "1.11.6" 5267 5289 "@xtuc/long" "4.2.2" 5268 5290 5269 - "@webpack-cli/configtest@^2.0.1": 5270 - version "2.0.1" 5271 - resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz#a69720f6c9bad6aef54a8fa6ba9c3533e7ef4c7f" 5272 - integrity sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A== 5291 + "@webpack-cli/configtest@^2.1.0": 5292 + version "2.1.0" 5293 + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.0.tgz#b59b33377b1b896a9a7357cfc643b39c1524b1e6" 5294 + integrity sha512-K/vuv72vpfSEZoo5KIU0a2FsEoYdW0DUMtMpB5X3LlUwshetMZRZRxB7sCsVji/lFaSxtQQ3aM9O4eMolXkU9w== 5273 5295 5274 5296 "@webpack-cli/info@^2.0.1": 5275 5297 version "2.0.1" 5276 5298 resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.1.tgz#eed745799c910d20081e06e5177c2b2569f166c0" 5277 5299 integrity sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA== 5278 5300 5279 - "@webpack-cli/serve@^2.0.2": 5280 - version "2.0.2" 5281 - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.2.tgz#10aa290e44a182c02e173a89452781b1acbc86d9" 5282 - integrity sha512-S9h3GmOmzUseyeFW3tYNnWS7gNUuwxZ3mmMq0JyW78Vx1SGKPSkt5bT4pB0rUnVfHjP0EL9gW2bOzmtiTfQt0A== 5301 + "@webpack-cli/serve@^2.0.4": 5302 + version "2.0.4" 5303 + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.4.tgz#3982ee6f8b42845437fc4d391e93ac5d9da52f0f" 5304 + integrity sha512-0xRgjgDLdz6G7+vvDLlaRpFatJaJ69uTalZLRSMX5B3VUrDmXcrVA3+6fXXQgmYz7bY9AAgs348XQdmtLsK41A== 5283 5305 5284 5306 "@xmldom/xmldom@~0.7.0", "@xmldom/xmldom@~0.7.7": 5285 5307 version "0.7.10" ··· 5348 5370 acorn-walk "^8.0.2" 5349 5371 5350 5372 acorn-import-assertions@^1.7.6: 5351 - version "1.8.0" 5352 - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" 5353 - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== 5373 + version "1.9.0" 5374 + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" 5375 + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== 5354 5376 5355 5377 acorn-jsx@^5.3.2: 5356 5378 version "5.3.2" ··· 5789 5811 integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== 5790 5812 5791 5813 axe-core@^4.6.2: 5792 - version "4.7.0" 5793 - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" 5794 - integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== 5814 + version "4.7.1" 5815 + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.1.tgz#04392c9ccb3d7d7c5d2f8684f148d56d3442f33d" 5816 + integrity sha512-sCXXUhA+cljomZ3ZAwb8i1p3oOlkABzPy08ZDAoGcYuvtBPlQ1Ytde129ArXyHWDhfeewq7rlx9F+cUx2SSlkg== 5795 5817 5796 - axios@^0.24.0: 5797 - version "0.24.0" 5798 - resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" 5799 - integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== 5818 + axios@^0.27.2: 5819 + version "0.27.2" 5820 + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" 5821 + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== 5800 5822 dependencies: 5801 - follow-redirects "^1.14.4" 5823 + follow-redirects "^1.14.9" 5824 + form-data "^4.0.0" 5802 5825 5803 5826 axios@^1.3.4: 5804 5827 version "1.4.0" ··· 6519 6542 lodash.uniq "^4.5.0" 6520 6543 6521 6544 caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: 6522 - version "1.0.30001482" 6523 - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz#8b3fad73dc35b2674a5c96df2d4f9f1c561435de" 6524 - integrity sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ== 6545 + version "1.0.30001488" 6546 + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz#d19d7b6e913afae3e98f023db97c19e9ddc5e91f" 6547 + integrity sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ== 6525 6548 6526 6549 case-anything@^2.1.10: 6527 - version "2.1.10" 6528 - resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.10.tgz#d18a6ca968d54ec3421df71e3e190f3bced23410" 6529 - integrity sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ== 6550 + version "2.1.11" 6551 + resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.11.tgz#39a00ff733f26e48729f5c6c7f23763ac8fa0467" 6552 + integrity sha512-uzKDXzdM/x914cepWPzElU3y50NRKYhjkO4ittOHLq+rF6M0AgRLF/+yPR1tvwLNAh8WHEPTfhuciZGPfX+oyg== 6530 6553 6531 6554 case-sensitive-paths-webpack-plugin@^2.4.0: 6532 6555 version "2.4.0" ··· 6553 6576 "@cbor-extract/cbor-extract-win32-x64" "2.1.1" 6554 6577 6555 6578 cbor-x@^1.5.1: 6556 - version "1.5.2" 6557 - resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.2.tgz#ceabc48bda06185de1f3a078bb4a793e6e222de5" 6558 - integrity sha512-JArE6xcgj3eo13fpnShO42QFBUuXP2uG12RLeF2Nb+dJcETFYxkUa27gXQrRYp67Ahtaxyfbg+ihc62XTyQqsQ== 6579 + version "1.5.3" 6580 + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.3.tgz#f8252fec7cab86b66c500e0c991788618e6638de" 6581 + integrity sha512-adrN0S67C7jY2hgqeGcw+Uj6iEGLQa5D/p6/9YNl5AaVIYJaJz/bARfWsP8UikBZWbhS27LN0DJK4531vo9ODw== 6559 6582 optionalDependencies: 6560 6583 cbor-extract "^2.1.1" 6561 6584 6562 6585 cborg@^1.6.0: 6563 - version "1.10.1" 6564 - resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.1.tgz#24cfe52c69ec0f66f95e23dc57f2086954c8d718" 6565 - integrity sha512-et6Qm8MOUY2kCWa5GKk2MlBVoPjHv0hQBmlzI/Z7+5V3VJCeIkGehIB3vWknNsm2kOkAIs6wEKJFJo8luWQQ/w== 6586 + version "1.10.2" 6587 + resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1" 6588 + integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug== 6566 6589 6567 6590 chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: 6568 6591 version "2.4.2" ··· 6712 6735 restore-cursor "^3.1.0" 6713 6736 6714 6737 cli-spinners@^2.0.0, cli-spinners@^2.5.0: 6715 - version "2.8.0" 6716 - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.8.0.tgz#e97a3e2bd00e6d85aa0c13d7f9e3ce236f7787fc" 6717 - integrity sha512-/eG5sJcvEIwxcdYM86k5tPwn0MUzkX5YY3eImTGpJOZgVe4SdTMY14vQpcxgBzJ0wXwAYrS8E+c3uHeK4JNyzQ== 6738 + version "2.9.0" 6739 + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.0.tgz#5881d0ad96381e117bbe07ad91f2008fe6ffd8db" 6740 + integrity sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g== 6718 6741 6719 6742 cli-width@^2.0.0: 6720 6743 version "2.2.1" ··· 7041 7064 serialize-javascript "^6.0.0" 7042 7065 7043 7066 core-js-compat@^3.25.1: 7044 - version "3.30.1" 7045 - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.30.1.tgz#961541e22db9c27fc48bfc13a3cafa8734171dfe" 7046 - integrity sha512-d690npR7MC6P0gq4npTl5n2VQeNAmUrJ90n+MHiKS7W2+xno4o3F5GDEuylSdi6EJ3VssibSGXOa1r3YXD3Mhw== 7067 + version "3.30.2" 7068 + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.30.2.tgz#83f136e375babdb8c80ad3c22d67c69098c1dd8b" 7069 + integrity sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA== 7047 7070 dependencies: 7048 7071 browserslist "^4.21.5" 7049 7072 7050 7073 core-js-pure@^3.23.3: 7051 - version "3.30.1" 7052 - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.30.1.tgz#7d93dc89e7d47b8ef05d7e79f507b0e99ea77eec" 7053 - integrity sha512-nXBEVpmUnNRhz83cHd9JRQC52cTMcuXAmR56+9dSMpRdpeA4I1PX6yjmhd71Eyc/wXNsdBdUDIj1QTIeZpU5Tg== 7074 + version "3.30.2" 7075 + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.30.2.tgz#005a82551f4af3250dcfb46ed360fad32ced114e" 7076 + integrity sha512-p/npFUJXXBkCCTIlEGBdghofn00jWG6ZOtdoIXSJmAu2QBvN0IqpZXWweOytcwE6cfx8ZvVUy1vw8zxhe4Y2vg== 7054 7077 7055 7078 core-js@^3.19.2: 7056 - version "3.30.1" 7057 - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.1.tgz#fc9c5adcc541d8e9fa3e381179433cbf795628ba" 7058 - integrity sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ== 7079 + version "3.30.2" 7080 + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.2.tgz#6528abfda65e5ad728143ea23f7a14f0dcf503fc" 7081 + integrity sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg== 7059 7082 7060 7083 core-util-is@~1.0.0: 7061 7084 version "1.0.3" ··· 7116 7139 integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 7117 7140 7118 7141 crelt@^1.0.0: 7119 - version "1.0.5" 7120 - resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94" 7121 - integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA== 7142 + version "1.0.6" 7143 + resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" 7144 + integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== 7122 7145 7123 7146 cross-fetch@^3.1.5: 7124 - version "3.1.5" 7125 - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" 7126 - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== 7147 + version "3.1.6" 7148 + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" 7149 + integrity sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g== 7127 7150 dependencies: 7128 - node-fetch "2.6.7" 7151 + node-fetch "^2.6.11" 7129 7152 7130 7153 cross-spawn@^4.0.2: 7131 7154 version "4.0.2" ··· 7296 7319 integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== 7297 7320 7298 7321 cssdb@^7.1.0: 7299 - version "7.5.4" 7300 - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.5.4.tgz#e34dafee5184d67634604e345e389ca79ac179ea" 7301 - integrity sha512-fGD+J6Jlq+aurfE1VDXlLS4Pt0VtNlu2+YgfGOdMxRyl/HQ9bDiHTwSck1Yz8A97Dt/82izSK6Bp/4nVqacOsg== 7322 + version "7.6.0" 7323 + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.6.0.tgz#beac8f7a5f676db62d3c33da517ef4c9eb008f8b" 7324 + integrity sha512-Nna7rph8V0jC6+JBY4Vk4ndErUmfJfV6NJCaZdurL0omggabiy+QB2HCQtu5c/ACLZ0I7REv7A4QyPIoYzZx0w== 7302 7325 7303 7326 cssesc@^3.0.0: 7304 7327 version "3.0.0" ··· 7685 7708 debug "^2.6.0" 7686 7709 7687 7710 detox@^20.1.2: 7688 - version "20.7.1" 7689 - resolved "https://registry.yarnpkg.com/detox/-/detox-20.7.1.tgz#3e3981a8eaa223135ca85d44aa9dc3b742b8ed46" 7690 - integrity sha512-a8y+M40g4goqWnyHZnestmVL/EII8Hq4utCK4kuSpvRHWkBA5KuQFlXErfNrrOcqXuXhPqdBnxqO9VsMAlLHFA== 7711 + version "20.9.0" 7712 + resolved "https://registry.yarnpkg.com/detox/-/detox-20.9.0.tgz#936050d7d72e141aebd92483e8893598686aa76a" 7713 + integrity sha512-juf0UQLcdk+CsggD25y8sDu0bYzJL9VWmwPKKtW2jsjOigI16jO1xwKCCYsuPYjprQmH+cJWXQ5k03zF1+JvXQ== 7691 7714 dependencies: 7692 7715 ajv "^8.6.3" 7693 7716 bunyan "^1.8.12" ··· 7723 7746 yargs "^17.0.0" 7724 7747 yargs-parser "^21.0.0" 7725 7748 yargs-unparser "^2.0.0" 7726 - 7727 - did-resolver@^4.0.0: 7728 - version "4.1.0" 7729 - resolved "https://registry.yarnpkg.com/did-resolver/-/did-resolver-4.1.0.tgz#740852083c4fd5bf9729d528eca5d105aff45eb6" 7730 - integrity sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA== 7731 7749 7732 7750 didyoumean@^1.2.2: 7733 7751 version "1.2.2" ··· 7956 7974 jake "^10.8.5" 7957 7975 7958 7976 electron-to-chromium@^1.4.284: 7959 - version "1.4.378" 7960 - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.378.tgz#73431ffd5fffebc18b4e897fac2e7d4ae6d559d9" 7961 - integrity sha512-RfCD26kGStl6+XalfX3DGgt3z2DNwJS5DKRHCpkPq5T/PqpZMPB1moSRXuK9xhkt/sF57LlpzJgNoYl7mO7Z6w== 7977 + version "1.4.397" 7978 + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.397.tgz#82a7e26c657538d59bb713b97ac22f97ea3a90ea" 7979 + integrity sha512-jwnPxhh350Q/aMatQia31KAIQdhEsYS0fFZ0BQQlN9tfvOEwShu6ZNwI4kL/xBabjcB/nTy6lSt17kNIluJZ8Q== 7962 7980 7963 7981 email-validator@^2.0.4: 7964 7982 version "2.0.4" ··· 8007 8025 dependencies: 8008 8026 once "^1.4.0" 8009 8027 8010 - enhanced-resolve@^5.13.0: 8011 - version "5.13.0" 8012 - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz#26d1ecc448c02de997133217b5c1053f34a0a275" 8013 - integrity sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg== 8028 + enhanced-resolve@^5.14.0: 8029 + version "5.14.0" 8030 + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz#0b6c676c8a3266c99fa281e4433a706f5c0c61c4" 8031 + integrity sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw== 8014 8032 dependencies: 8015 8033 graceful-fs "^4.2.4" 8016 8034 tapable "^2.2.0" ··· 8379 8397 string.prototype.matchall "^4.0.8" 8380 8398 8381 8399 eslint-plugin-testing-library@^5.0.1: 8382 - version "5.10.3" 8383 - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.3.tgz#e613fbaf9a145e9eef115d080b32cb488fae622e" 8384 - integrity sha512-0yhsKFsjHLud5PM+f2dWr9K3rqYzMy4cSHs3lcmFYMa1CdSzRvHGgXvsFarBjZ41gU8jhTdMIkg8jHLxGJqLqw== 8400 + version "5.11.0" 8401 + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.0.tgz#0bad7668e216e20dd12f8c3652ca353009163121" 8402 + integrity sha512-ELY7Gefo+61OfXKlQeXNIDVVLPcvKTeiQOoMZG9TeuWa7Ln4dUNRv8JdRWBQI9Mbb427XGlVB1aa1QPZxBJM8Q== 8385 8403 dependencies: 8386 8404 "@typescript-eslint/utils" "^5.58.0" 8387 8405 ··· 8406 8424 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" 8407 8425 integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== 8408 8426 8409 - eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0: 8410 - version "3.4.0" 8411 - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc" 8412 - integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== 8427 + eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: 8428 + version "3.4.1" 8429 + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" 8430 + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== 8413 8431 8414 8432 eslint-webpack-plugin@^3.1.1: 8415 8433 version "3.2.0" ··· 8423 8441 schema-utils "^4.0.0" 8424 8442 8425 8443 eslint@^8.19.0, eslint@^8.3.0: 8426 - version "8.39.0" 8427 - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" 8428 - integrity sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og== 8444 + version "8.40.0" 8445 + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.40.0.tgz#a564cd0099f38542c4e9a2f630fa45bf33bc42a4" 8446 + integrity sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ== 8429 8447 dependencies: 8430 8448 "@eslint-community/eslint-utils" "^4.2.0" 8431 8449 "@eslint-community/regexpp" "^4.4.0" 8432 - "@eslint/eslintrc" "^2.0.2" 8433 - "@eslint/js" "8.39.0" 8450 + "@eslint/eslintrc" "^2.0.3" 8451 + "@eslint/js" "8.40.0" 8434 8452 "@humanwhocodes/config-array" "^0.11.8" 8435 8453 "@humanwhocodes/module-importer" "^1.0.1" 8436 8454 "@nodelib/fs.walk" "^1.2.8" ··· 8441 8459 doctrine "^3.0.0" 8442 8460 escape-string-regexp "^4.0.0" 8443 8461 eslint-scope "^7.2.0" 8444 - eslint-visitor-keys "^3.4.0" 8445 - espree "^9.5.1" 8462 + eslint-visitor-keys "^3.4.1" 8463 + espree "^9.5.2" 8446 8464 esquery "^1.4.2" 8447 8465 esutils "^2.0.2" 8448 8466 fast-deep-equal "^3.1.3" ··· 8468 8486 strip-json-comments "^3.1.0" 8469 8487 text-table "^0.2.0" 8470 8488 8471 - espree@^9.5.1: 8472 - version "9.5.1" 8473 - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4" 8474 - integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg== 8489 + espree@^9.5.2: 8490 + version "9.5.2" 8491 + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" 8492 + integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== 8475 8493 dependencies: 8476 8494 acorn "^8.8.0" 8477 8495 acorn-jsx "^5.3.2" 8478 - eslint-visitor-keys "^3.4.0" 8496 + eslint-visitor-keys "^3.4.1" 8479 8497 8480 8498 esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: 8481 8499 version "4.0.1" ··· 8783 8801 find-up "^5.0.0" 8784 8802 fs-extra "^9.1.0" 8785 8803 8786 - expo-modules-core@1.2.6: 8787 - version "1.2.6" 8788 - resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.2.6.tgz#921abc8031fe0e5474ee48905071902b9627d051" 8789 - integrity sha512-vyleKepkP8F6L+D55B/E4FbZ8x9pdy3yw/mdbGBkDkrmo2gmeMjOM1mKLSszOkLIqet05O7Wy8m0FZHZTo0VBg== 8804 + expo-modules-core@1.2.7: 8805 + version "1.2.7" 8806 + resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.2.7.tgz#c80627b13a8f1c94ae9da8eea41e1ef1df5788c8" 8807 + integrity sha512-sulqn2M8+tIdxi6QFkKppDEzbePAscgE2LEHocYoQOgHxJpeT7axE0Hkzc+81EeviQilZzGeFZMtNMGh3c9yJg== 8790 8808 dependencies: 8791 8809 compare-versions "^3.4.0" 8792 8810 invariant "^2.2.4" ··· 8855 8873 resolve-from "^5.0.0" 8856 8874 8857 8875 expo@~48.0.15: 8858 - version "48.0.15" 8859 - resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.15.tgz#28194c03ac85f7f5a87b7493b8cef0eb405eccbe" 8860 - integrity sha512-me2Xxr7Faxf60BiKq8WBSwkYV9BVbS+VqeHRFdXduVA0Uj2zp1a0zYB5eblmWqpRco75VBUgOa9M+/eR1YVZmw== 8876 + version "48.0.17" 8877 + resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.17.tgz#000773a2675e5bf95688a84fbc625c105f8b8af9" 8878 + integrity sha512-5T1CsMUlfI+xFB89GOU+/xtSSbSBBFVTqwgheAU0cQolfbs+YyJCMTKU5vN45N5OK+ym7p/LKPa6DQAxYPF8YQ== 8861 8879 dependencies: 8862 8880 "@babel/runtime" "^7.20.0" 8863 8881 "@expo/cli" "0.7.1" 8864 8882 "@expo/config" "8.0.2" 8865 - "@expo/config-plugins" "6.0.1" 8883 + "@expo/config-plugins" "6.0.2" 8866 8884 "@expo/vector-icons" "^13.0.0" 8867 8885 babel-preset-expo "~9.3.2" 8868 8886 cross-spawn "^6.0.5" ··· 8873 8891 expo-font "~11.1.1" 8874 8892 expo-keep-awake "~12.0.1" 8875 8893 expo-modules-autolinking "1.2.0" 8876 - expo-modules-core "1.2.6" 8894 + expo-modules-core "1.2.7" 8877 8895 fbemitter "^3.0.0" 8878 8896 getenv "^1.0.0" 8879 8897 invariant "^2.2.4" ··· 9021 9039 boolean "^3.1.4" 9022 9040 9023 9041 fast-redact@^3.1.1: 9024 - version "3.1.2" 9025 - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" 9026 - integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== 9042 + version "3.2.0" 9043 + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.2.0.tgz#b1e2d39bc731376d28bde844454fa23e26919987" 9044 + integrity sha512-zaTadChr+NekyzallAMXATXLOR8MNx3zqpZ0MUF2aGf4EathnG0f32VLODNlY8IuGY3HoRO2L6/6fSzNsLaHIw== 9027 9045 9028 9046 fast-text-encoding@^1.0.6: 9029 9047 version "1.0.6" ··· 9129 9147 resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" 9130 9148 integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== 9131 9149 9132 - filelist@^1.0.1: 9150 + filelist@^1.0.4: 9133 9151 version "1.0.4" 9134 9152 resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" 9135 9153 integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== ··· 9272 9290 integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== 9273 9291 9274 9292 flow-parser@0.*: 9275 - version "0.205.0" 9276 - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.205.0.tgz#8756173b6488dedc31ab838e80c8f008d7a44e05" 9277 - integrity sha512-ZJ6VuLe/BoqeI4GsF+ZuzlpfGi3FCnBrb4xDYhgEJxRt7SAj3ibRuRSsuJSRcY+lQhPZRPNbNWiQqFMxramUzw== 9293 + version "0.206.0" 9294 + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.206.0.tgz#f4f794f8026535278393308e01ea72f31000bfef" 9295 + integrity sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w== 9278 9296 9279 9297 flow-parser@^0.185.0: 9280 9298 version "0.185.2" 9281 9299 resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.185.2.tgz#cb7ee57f77377d6c5d69a469e980f6332a15e492" 9282 9300 integrity sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ== 9283 9301 9284 - follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.15.0: 9302 + follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0: 9285 9303 version "1.15.2" 9286 9304 resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" 9287 9305 integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== ··· 9491 9509 integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 9492 9510 9493 9511 get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: 9494 - version "1.2.0" 9495 - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" 9496 - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== 9512 + version "1.2.1" 9513 + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" 9514 + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== 9497 9515 dependencies: 9498 9516 function-bind "^1.1.1" 9499 9517 has "^1.0.3" 9518 + has-proto "^1.0.1" 9500 9519 has-symbols "^1.0.3" 9501 9520 9502 9521 get-own-enumerable-property-symbols@^3.0.0: ··· 10346 10365 ci-info "^2.0.0" 10347 10366 10348 10367 is-core-module@^2.11.0, is-core-module@^2.9.0: 10349 - version "2.12.0" 10350 - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.0.tgz#36ad62f6f73c8253fd6472517a12483cf03e7ec4" 10351 - integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ== 10368 + version "2.12.1" 10369 + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" 10370 + integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== 10352 10371 dependencies: 10353 10372 has "^1.0.3" 10354 10373 ··· 10750 10769 istanbul-lib-report "^3.0.0" 10751 10770 10752 10771 jake@^10.8.5: 10753 - version "10.8.5" 10754 - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" 10755 - integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== 10772 + version "10.8.6" 10773 + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.6.tgz#227a96786a1e035214e0ba84b482d6223d41ef04" 10774 + integrity sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA== 10756 10775 dependencies: 10757 10776 async "^3.2.3" 10758 10777 chalk "^4.0.2" 10759 - filelist "^1.0.1" 10760 - minimatch "^3.0.4" 10778 + filelist "^1.0.4" 10779 + minimatch "^3.1.2" 10761 10780 10762 10781 jest-changed-files@^27.5.1: 10763 10782 version "27.5.1" ··· 12903 12922 dependencies: 12904 12923 yallist "^4.0.0" 12905 12924 12906 - minipass@^4.0.0: 12907 - version "4.2.8" 12908 - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" 12909 - integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== 12925 + minipass@^5.0.0: 12926 + version "5.0.0" 12927 + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" 12928 + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== 12910 12929 12911 12930 minizlib@^2.1.1: 12912 12931 version "2.1.2" ··· 13131 13150 dependencies: 13132 13151 minimatch "^3.0.2" 13133 13152 13134 - node-fetch@2.6.7: 13135 - version "2.6.7" 13136 - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" 13137 - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== 13138 - dependencies: 13139 - whatwg-url "^5.0.0" 13140 - 13141 - node-fetch@^2.0.0-alpha.8, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: 13142 - version "2.6.9" 13143 - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" 13144 - integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== 13153 + node-fetch@^2.0.0-alpha.8, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.11, node-fetch@^2.6.7: 13154 + version "2.6.11" 13155 + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" 13156 + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== 13145 13157 dependencies: 13146 13158 whatwg-url "^5.0.0" 13147 13159 ··· 13200 13212 html-to-text "7.1.1" 13201 13213 13202 13214 nodemailer@^6.8.0: 13203 - version "6.9.1" 13204 - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.1.tgz#8249d928a43ed85fec17b13d2870c8f758a126ed" 13205 - integrity sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA== 13215 + version "6.9.2" 13216 + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.2.tgz#b79051811edd52c2436ad1c6aed2dc45b9c9cf1f" 13217 + integrity sha512-4+TYaa/e1nIxQfyw/WzNPYTEZ5OvHIDEnmjs4LPmIfccPQN+2CYKmGHjWixn/chzD3bmUTu5FMfpltizMxqzdg== 13206 13218 13207 13219 normalize-css-color@^1.0.2: 13208 13220 version "1.0.2" ··· 13559 13571 wcwidth "^1.0.1" 13560 13572 13561 13573 orderedmap@^2.0.0: 13562 - version "2.1.0" 13563 - resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.0.tgz#819457082fa3a06abd316d83a281a1ca467437cd" 13564 - integrity sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA== 13574 + version "2.1.1" 13575 + resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2" 13576 + integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== 13565 13577 13566 13578 os-homedir@^1.0.0: 13567 13579 version "1.0.2" ··· 13831 13843 resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 13832 13844 integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== 13833 13845 13834 - pg-connection-string@^2.5.0: 13835 - version "2.5.0" 13836 - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" 13837 - integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== 13846 + pg-cloudflare@^1.1.0: 13847 + version "1.1.0" 13848 + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz#833d70870d610d14bf9df7afb40e1cba310c17a0" 13849 + integrity sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA== 13850 + 13851 + pg-connection-string@^2.6.0: 13852 + version "2.6.0" 13853 + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.0.tgz#12a36cc4627df19c25cc1b9b736cc39ee1f73ae8" 13854 + integrity sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg== 13838 13855 13839 13856 pg-int8@1.0.1: 13840 13857 version "1.0.1" ··· 13863 13880 postgres-interval "^1.1.0" 13864 13881 13865 13882 pg@^8.10.0, pg@^8.9.0: 13866 - version "8.10.0" 13867 - resolved "https://registry.yarnpkg.com/pg/-/pg-8.10.0.tgz#5b8379c9b4a36451d110fc8cd98fc325fe62ad24" 13868 - integrity sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ== 13883 + version "8.11.0" 13884 + resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.0.tgz#a37e534e94b57a7ed811e926f23a7c56385f55d9" 13885 + integrity sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA== 13869 13886 dependencies: 13870 13887 buffer-writer "2.0.0" 13871 13888 packet-reader "1.0.0" 13872 - pg-connection-string "^2.5.0" 13889 + pg-connection-string "^2.6.0" 13873 13890 pg-pool "^3.6.0" 13874 13891 pg-protocol "^1.6.0" 13875 13892 pg-types "^2.1.0" 13876 13893 pgpass "1.x" 13894 + optionalDependencies: 13895 + pg-cloudflare "^1.1.0" 13877 13896 13878 13897 pgpass@1.x: 13879 13898 version "1.0.5" ··· 13938 13957 process-warning "^2.0.0" 13939 13958 13940 13959 pino-std-serializers@^6.0.0: 13941 - version "6.2.0" 13942 - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz#169048c0df3f61352fce56aeb7fb962f1b66ab43" 13943 - integrity sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA== 13960 + version "6.2.1" 13961 + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.1.tgz#369f4ae2a19eb6d769ddf2c88a2164b76879a284" 13962 + integrity sha512-wHuWB+CvSVb2XqXM0W/WOYUkVSPbiJb9S5fNB7TBhd8s892Xq910bRxwHtC4l71hgztObTjXL6ZheZXFjhDrDQ== 13944 13963 13945 13964 pino@^8.0.0, pino@^8.11.0, pino@^8.6.1: 13946 - version "8.11.0" 13947 - resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498" 13948 - integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg== 13965 + version "8.14.1" 13966 + resolved "https://registry.yarnpkg.com/pino/-/pino-8.14.1.tgz#bb38dcda8b500dd90c1193b6c9171eb777a47ac8" 13967 + integrity sha512-8LYNv7BKWXSfS+k6oEc6occy5La+q2sPwU3q2ljTX5AZk7v+5kND2o5W794FyRaqha6DJajmkNRsWtPpFyMUdw== 13949 13968 dependencies: 13950 13969 atomic-sleep "^1.0.0" 13951 13970 fast-redact "^3.1.1" ··· 14516 14535 postcss-selector-parser "^6.0.10" 14517 14536 14518 14537 postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: 14519 - version "6.0.12" 14520 - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz#2efae5ffab3c8bfb2b7fbf0c426e3bca616c4abb" 14521 - integrity sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg== 14538 + version "6.0.13" 14539 + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" 14540 + integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== 14522 14541 dependencies: 14523 14542 cssesc "^3.0.0" 14524 14543 util-deprecate "^1.0.2" ··· 14749 14768 signal-exit "^3.0.2" 14750 14769 14751 14770 prosemirror-changeset@^2.2.0: 14752 - version "2.2.0" 14753 - resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.0.tgz#22c05da271a118be40d3e339fa2cace789b1254b" 14754 - integrity sha512-QM7ohGtkpVpwVGmFb8wqVhaz9+6IUXcIQBGZ81YNAKYuHiFJ1ShvSzab4pKqTinJhwciZbrtBEk/2WsqSt2PYg== 14771 + version "2.2.1" 14772 + resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz#dae94b63aec618fac7bb9061648e6e2a79988383" 14773 + integrity sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ== 14755 14774 dependencies: 14756 14775 prosemirror-transform "^1.0.0" 14757 14776 14758 14777 prosemirror-collab@^1.3.0: 14759 - version "1.3.0" 14760 - resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.0.tgz#601d33473bf72e6c43041a54b860c84c60b37769" 14761 - integrity sha512-+S/IJ69G2cUu2IM5b3PBekuxs94HO1CxJIWOFrLQXUaUDKL/JfBx+QcH31ldBlBXyDEUl+k3Vltfi1E1MKp2mA== 14778 + version "1.3.1" 14779 + resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33" 14780 + integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ== 14762 14781 dependencies: 14763 14782 prosemirror-state "^1.0.0" 14764 14783 14765 14784 prosemirror-commands@^1.0.0, prosemirror-commands@^1.3.1: 14766 - version "1.5.1" 14767 - resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.1.tgz#89ddfa14e144dcc7fb0938aa0e2568c7fdde306f" 14768 - integrity sha512-ga1ga/RkbzxfAvb6iEXYmrEpekn5NCwTb8w1dr/gmhSoaGcQ0VPuCzOn5qDEpC45ql2oDkKoKQbRxLJwKLpMTQ== 14785 + version "1.5.2" 14786 + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz#e94aeea52286f658cd984270de9b4c3fff580852" 14787 + integrity sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ== 14769 14788 dependencies: 14770 14789 prosemirror-model "^1.0.0" 14771 14790 prosemirror-state "^1.0.0" 14772 14791 prosemirror-transform "^1.0.0" 14773 14792 14774 14793 prosemirror-dropcursor@^1.5.0: 14775 - version "1.8.0" 14776 - resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.0.tgz#7bfa11925e0da41d1db869954fe51e1aa55158e4" 14777 - integrity sha512-TZMitR8nlp9Xh42pDYGcWopCoFPmJduoyGJ7FjYM2/7gZKnfD41TIaZN5Q1cQjm6Fm/P5vk/DpVYFhS8kDdigw== 14794 + version "1.8.1" 14795 + resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d" 14796 + integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw== 14778 14797 dependencies: 14779 14798 prosemirror-state "^1.0.0" 14780 14799 prosemirror-transform "^1.1.0" 14781 14800 prosemirror-view "^1.1.0" 14782 14801 14783 14802 prosemirror-gapcursor@^1.3.1: 14784 - version "1.3.1" 14785 - resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.1.tgz#8cfd874592e4504d63720e14ed680c7866e64554" 14786 - integrity sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA== 14803 + version "1.3.2" 14804 + resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4" 14805 + integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ== 14787 14806 dependencies: 14788 14807 prosemirror-keymap "^1.0.0" 14789 14808 prosemirror-model "^1.0.0" ··· 14791 14810 prosemirror-view "^1.0.0" 14792 14811 14793 14812 prosemirror-history@^1.0.0, prosemirror-history@^1.3.0: 14794 - version "1.3.1" 14795 - resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.1.tgz#d0dba9ed1cc2bce55a45ce9c7c8224e641f276b8" 14796 - integrity sha512-YMV/IWBZ+LZSfaNcBbPcaQUiAiJRYFyJW2aapuNzL8nhIRsI7fIO0ykJFSe802+mWeoTsVJ1jxvRWPYqaUqljQ== 14813 + version "1.3.2" 14814 + resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.2.tgz#ce6ad7ab9db83e761aee716f3040d74738311b15" 14815 + integrity sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g== 14797 14816 dependencies: 14798 14817 prosemirror-state "^1.2.2" 14799 14818 prosemirror-transform "^1.0.0" ··· 14801 14820 rope-sequence "^1.3.0" 14802 14821 14803 14822 prosemirror-inputrules@^1.2.0: 14804 - version "1.2.0" 14805 - resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz#476dde2dc244050b3aca00cf58a82adfad6749e7" 14806 - integrity sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ== 14823 + version "1.2.1" 14824 + resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.2.1.tgz#8faf3d78c16150aedac71d326a3e3947417ce557" 14825 + integrity sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ== 14807 14826 dependencies: 14808 14827 prosemirror-state "^1.0.0" 14809 14828 prosemirror-transform "^1.0.0" 14810 14829 14811 14830 prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.0: 14812 - version "1.2.1" 14813 - resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.1.tgz#3839e7db66cecddae7451f4246e73bdd8489be1d" 14814 - integrity sha512-kVK6WGC+83LZwuSJnuCb9PsADQnFZllt94qPP3Rx/vLcOUV65+IbBeH2nS5cFggPyEVJhGkGrgYFRrG250WhHQ== 14831 + version "1.2.2" 14832 + resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e" 14833 + integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ== 14815 14834 dependencies: 14816 14835 prosemirror-state "^1.0.0" 14817 14836 w3c-keyname "^2.2.0" 14818 14837 14819 14838 prosemirror-markdown@^1.10.1: 14820 - version "1.10.1" 14821 - resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.10.1.tgz#e20468201cda1916a6182686159398b242bb78ab" 14822 - integrity sha512-s7iaTLiX+qO5z8kF2NcMmy2T7mIlxzkS4Sp3vTKSYChPtbMpg6YxFkU0Y06rUg2WtKlvBu7v1bXzlGBkfjUWAA== 14839 + version "1.11.0" 14840 + resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.11.0.tgz#75f2d6f14655762b4b8a247436b87ed81e22c7ee" 14841 + integrity sha512-yP9mZqPRstjZhhf3yykCQNE3AijxARrHe4e7esV9A+gp4cnGOH4QvrKYPpXLHspNWyvJJ+0URH+iIvV5qP1I2Q== 14823 14842 dependencies: 14824 14843 markdown-it "^13.0.1" 14825 14844 prosemirror-model "^1.0.0" 14826 14845 14827 14846 prosemirror-menu@^1.2.1: 14828 - version "1.2.1" 14829 - resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.1.tgz#94d99a8547b7ba5680c20e9c497ce19846ce3b2c" 14830 - integrity sha512-sBirXxVfHalZO4f1ZS63WzewINK4182+7dOmoMeBkqYO8wqMBvBS7wQuwVOHnkMWPEh0+N0LJ856KYUN+vFkmQ== 14847 + version "1.2.2" 14848 + resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.2.tgz#c545a2de0b8cb79babc07682b1d93de0f273aa33" 14849 + integrity sha512-437HIWTq4F9cTX+kPfqZWWm+luJm95Aut/mLUy+9OMrOml0bmWDS26ceC6SNfb2/S94et1sZ186vLO7pDHzxSw== 14831 14850 dependencies: 14832 14851 crelt "^1.0.0" 14833 14852 prosemirror-commands "^1.0.0" ··· 14835 14854 prosemirror-state "^1.0.0" 14836 14855 14837 14856 prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1: 14838 - version "1.19.0" 14839 - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.0.tgz#d7ad9a65ada0bb12196f64fe0dd4fc392c841c29" 14840 - integrity sha512-/CvFGJnwc41EJSfDkQLly1cAJJJmBpZwwUJtwZPTjY2RqZJfM8HVbCreOY/jti8wTRbVyjagcylyGoeJH/g/3w== 14857 + version "1.19.1" 14858 + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.1.tgz#7e10cd9584a0a55c87ffbecc68aa9d105b9a0f53" 14859 + integrity sha512-RpV0fZfy74DEO9GPRbGcG6xN33KuqEvlLE2V0e5CXUGs3xkZsiJfx1dcYPU57+606NVYCaDN1riFXdXBQRaRcg== 14841 14860 dependencies: 14842 14861 orderedmap "^2.0.0" 14843 14862 14844 14863 prosemirror-schema-basic@^1.2.0: 14845 - version "1.2.1" 14846 - resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.1.tgz#a5a137a6399d1a829873332117d2fe8131d291d0" 14847 - integrity sha512-vYBdIHsYKSDIqYmPBC7lnwk9DsKn8PnVqK97pMYP5MLEDFqWIX75JiaJTzndBii4bRuNqhC2UfDOfM3FKhlBHg== 14864 + version "1.2.2" 14865 + resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7" 14866 + integrity sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw== 14848 14867 dependencies: 14849 14868 prosemirror-model "^1.19.0" 14850 14869 14851 14870 prosemirror-schema-list@^1.2.2: 14852 - version "1.2.2" 14853 - resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.2.tgz#bafda37b72367d39accdcaf6ddf8fb654a16e8e5" 14854 - integrity sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w== 14871 + version "1.2.3" 14872 + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.3.tgz#12e3d70cb17780980a3c28588ed7c888121d5e8d" 14873 + integrity sha512-HD8yjDOusz7JB3oBFCaMOpEN9Z9DZttLr6tcASjnvKMc0qTyX5xgAN8YiMFFEcwyhF7WZrZ2YQkAwzsn8ICVbQ== 14855 14874 dependencies: 14856 14875 prosemirror-model "^1.0.0" 14857 14876 prosemirror-state "^1.0.0" 14858 14877 prosemirror-transform "^1.0.0" 14859 14878 14860 14879 prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.1: 14861 - version "1.4.2" 14862 - resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.2.tgz#f93bd8a33a4454efab917ba9b738259d828db7e5" 14863 - integrity sha512-puuzLD2mz/oTdfgd8msFbe0A42j5eNudKAAPDB0+QJRw8cO1ygjLmhLrg9RvDpf87Dkd6D4t93qdef00KKNacQ== 14880 + version "1.4.3" 14881 + resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080" 14882 + integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q== 14864 14883 dependencies: 14865 14884 prosemirror-model "^1.0.0" 14866 14885 prosemirror-transform "^1.0.0" ··· 14888 14907 escape-string-regexp "^4.0.0" 14889 14908 14890 14909 prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.0: 14891 - version "1.7.1" 14892 - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.7.1.tgz#b516e818c3add0bdf960f4ca8ccb9d057a3ba21b" 14893 - integrity sha512-VteoifAfpt46z0yEt6Fc73A5OID9t/y2QIeR5MgxEwTuitadEunD/V0c9jQW8ziT8pbFM54uTzRLJ/nLuQjMxg== 14910 + version "1.7.2" 14911 + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.7.2.tgz#f3e57d8424afa6ab7c2b2319cc0ac58e75f7160b" 14912 + integrity sha512-b94lVUdA9NyaYRb2WuGSgb5YANiITa05dtew9eSK+KkYu64BCnU27WhJPE95gAWAnhV57CM3FabWXM23gri8Kg== 14894 14913 dependencies: 14895 14914 prosemirror-model "^1.0.0" 14896 14915 14897 14916 prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.28.2, prosemirror-view@^1.31.0: 14898 - version "1.31.1" 14899 - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.31.1.tgz#706611f134018a4dd832110911bdd908e3af92c1" 14900 - integrity sha512-9NKJdXnGV4+1qFRi16XFZxpnx6zNok9MEj/HElkqUJ1HtOyKOICffKxqoXUUCAdHrrP+yMDvdXc6wT7GGWBL3A== 14917 + version "1.31.3" 14918 + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.31.3.tgz#cfe171c4e50a577526d0235d9ec757cdddf6017d" 14919 + integrity sha512-UYDa8WxRFZm0xQLXiPJUVTl6H08Fn0IUVDootA7ZlQwzooqVWnBOXLovJyyTKgws1nprfsPhhlvWgt2jo4ZA6g== 14901 14920 dependencies: 14902 14921 prosemirror-model "^1.16.0" 14903 14922 prosemirror-state "^1.0.0" ··· 15101 15120 text-table "^0.2.0" 15102 15121 15103 15122 react-devtools-core@^4.26.1: 15104 - version "4.27.6" 15105 - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.6.tgz#e5a613014f7506801ed6c1a97bd0e6316cc9c48a" 15106 - integrity sha512-jeFNhEzcSwpiqmw+zix5IFibNEPmUodICN7ClrlRKGktzO/3FMteMb52l1NRUiz/ABSYt9hOZ9IPgVDrg5pyUw== 15123 + version "4.27.7" 15124 + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.7.tgz#458a6541483078d60a036c75bf88f54c478086ec" 15125 + integrity sha512-12N0HrhCPbD76Z7SkyJdGdXdPGouUsgV6tlEsbSpAnLDO06tjXZP+irht4wPdYwJAJRQ85DxL48eQoz7UmrSuQ== 15107 15126 dependencies: 15108 15127 shell-quote "^1.6.1" 15109 15128 ws "^7" ··· 15147 15166 integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ== 15148 15167 15149 15168 react-native-background-fetch@^4.1.8: 15150 - version "4.1.9" 15151 - resolved "https://registry.yarnpkg.com/react-native-background-fetch/-/react-native-background-fetch-4.1.9.tgz#10ebff9ca45a8868f1a72b2aa6cea40d499ece6e" 15152 - integrity sha512-sk4MCXRhGghBXu9ReabuT8U0WRzjsMt2i/nqCwR9eHi0hux+4kUh5ubpLKLByw5G8WifUv1sp6qsA7uvQERrrQ== 15169 + version "4.1.10" 15170 + resolved "https://registry.yarnpkg.com/react-native-background-fetch/-/react-native-background-fetch-4.1.10.tgz#12c7e85140af67fb05edb7cd9960e4f09a457797" 15171 + integrity sha512-Ug54vTctZuD/c06ZLk/VyvFdhw/hCVVOHYR5heyMqc6FlT/m9fVhFWyl4uH3JmPCzmWDVR3fO28CzrGpKOrusw== 15153 15172 15154 15173 react-native-codegen@^0.71.5: 15155 15174 version "0.71.5" ··· 15168 15187 dependencies: 15169 15188 dotenv "^16.0.3" 15170 15189 15190 + react-native-draggable-flatlist@^4.0.1: 15191 + version "4.0.1" 15192 + resolved "https://registry.yarnpkg.com/react-native-draggable-flatlist/-/react-native-draggable-flatlist-4.0.1.tgz#2f027d387ba4b8f3eb0907340e32cb85e6460df2" 15193 + integrity sha512-ZO1QUTNx64KZfXGXeXcBfql67l38X7kBcJ3rxUVZzPHt5r035GnGzIC0F8rqSXp6zgnwgUYMfB6zQc5PKmPL9Q== 15194 + dependencies: 15195 + "@babel/preset-typescript" "^7.17.12" 15196 + 15171 15197 react-native-drawer-layout@^3.2.0: 15172 15198 version "3.2.0" 15173 15199 resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-3.2.0.tgz#1ab05d0bed6bb684353c17c96e1d3e6c1a4e225d" ··· 15202 15228 fast-base64-decode "^1.0.0" 15203 15229 15204 15230 react-native-gradle-plugin@^0.71.17: 15205 - version "0.71.17" 15206 - resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.17.tgz#cf780a27270f0a32dca8184eff91555d7627dd00" 15207 - integrity sha512-OXXYgpISEqERwjSlaCiaQY6cTY5CH6j73gdkWpK0hedxtiWMWgH+i5TOi4hIGYitm9kQBeyDu+wim9fA8ROFJA== 15231 + version "0.71.18" 15232 + resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.18.tgz#20ef199bc85be32e45bb6cc069ec2e7dcb1a74a6" 15233 + integrity sha512-7F6bD7B8Xsn3JllxcwHhFcsl9aHIig47+3eN4IHFNqfLhZr++3ElDrcqfMzugM+niWbaMi7bJ0kAkAL8eCpdWg== 15208 15234 15209 15235 react-native-haptic-feedback@^1.14.0: 15210 15236 version "1.14.0" ··· 15259 15285 integrity sha512-sdmLElNs5PDWqmZmj4/aNH4anyxreaPm61c4ZkRiR8SO/GzLg6KjAbb0e17RmMdnBdD0AIQbS38h/l55YKN4ZA== 15260 15286 15261 15287 react-native-safe-area-context@^4.4.1: 15262 - version "4.5.2" 15263 - resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.5.2.tgz#38438c7a52ce2a6a05fc4de6cd3ee47f78a9366e" 15264 - integrity sha512-oH4/Dm7/PWOOZtFRiA4HE08lsfA948BRq8Fn7TEndYjoDXFoNdbjQRahXzCV8JGP/tv3qrVNeaDE8rmdRRUOlA== 15288 + version "4.5.3" 15289 + resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.5.3.tgz#e98eb1a73a6b3846d296545fe74760754dbaaa69" 15290 + integrity sha512-ihYeGDEBSkYH+1aWnadNhVtclhppVgd/c0tm4mj0+HV11FoiWJ8N6ocnnZnRLvM5Fxc+hUqxR9bm5AXU3rXiyA== 15265 15291 15266 15292 react-native-screens@^3.13.1: 15267 15293 version "3.20.0" ··· 15495 15521 util-deprecate "^1.0.1" 15496 15522 15497 15523 readable-stream@^4.0.0: 15498 - version "4.3.0" 15499 - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.3.0.tgz#0914d0c72db03b316c9733bb3461d64a3cc50cba" 15500 - integrity sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ== 15524 + version "4.4.0" 15525 + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.4.0.tgz#55ce132d60a988c460d75c631e9ccf6a7229b468" 15526 + integrity sha512-kDMOq0qLtxV9f/SQv522h8cxZBqNZXuXNyjyezmfAAuribMyVXziljpQ/uQhfE1XLg2/TLTW2DsnoE4VAi/krg== 15501 15527 dependencies: 15502 15528 abort-controller "^3.0.0" 15503 15529 buffer "^6.0.3" ··· 15895 15921 fsevents "~2.3.2" 15896 15922 15897 15923 rope-sequence@^1.3.0: 15898 - version "1.3.3" 15899 - resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.3.tgz#3f67fc106288b84b71532b4a5fd9d4881e4457f0" 15900 - integrity sha512-85aZYCxweiD5J8yTEbw+E6A27zSnLPNDL0WfPdw3YYodq7WjnTKo0q4dtyQ2gz23iPT8Q9CUyJtAaUNcTxRf5Q== 15924 + version "1.3.4" 15925 + resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425" 15926 + integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ== 15901 15927 15902 15928 rtl-detect@^1.0.2: 15903 15929 version "1.0.4" ··· 16104 16130 integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== 16105 16131 16106 16132 semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: 16107 - version "7.5.0" 16108 - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0" 16109 - integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA== 16133 + version "7.5.1" 16134 + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" 16135 + integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw== 16110 16136 dependencies: 16111 16137 lru-cache "^6.0.0" 16112 16138 ··· 16358 16384 integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== 16359 16385 16360 16386 slash@^5.0.0: 16361 - version "5.0.1" 16362 - resolved "https://registry.yarnpkg.com/slash/-/slash-5.0.1.tgz#c354c3a49c0d3b4da1cb0bbeb15a85c2a6defa71" 16363 - integrity sha512-ywNzUOiXwetmLvTUiCBZpLi+vxqN3i+zDqjs2HHfUSV3wN4UJxVVKWrS1JZDeiJIeBFNgB5pmioC2g0IUTL+rQ== 16387 + version "5.1.0" 16388 + resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" 16389 + integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== 16364 16390 16365 16391 slice-ansi@^2.0.0: 16366 16392 version "2.1.0" ··· 17034 17060 readable-stream "^3.1.1" 17035 17061 17036 17062 tar@^6.0.2, tar@^6.0.5: 17037 - version "6.1.13" 17038 - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" 17039 - integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== 17063 + version "6.1.15" 17064 + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.15.tgz#c9738b0b98845a3b344d334b8fa3041aaba53a69" 17065 + integrity sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A== 17040 17066 dependencies: 17041 17067 chownr "^2.0.0" 17042 17068 fs-minipass "^2.0.0" 17043 - minipass "^4.0.0" 17069 + minipass "^5.0.0" 17044 17070 minizlib "^2.1.1" 17045 17071 mkdirp "^1.0.3" 17046 17072 yallist "^4.0.0" ··· 17124 17150 supports-hyperlinks "^2.0.0" 17125 17151 17126 17152 terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.0, terser-webpack-plugin@^5.3.7: 17127 - version "5.3.7" 17128 - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz#ef760632d24991760f339fe9290deb936ad1ffc7" 17129 - integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw== 17153 + version "5.3.8" 17154 + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz#415e03d2508f7de63d59eca85c5d102838f06610" 17155 + integrity sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg== 17130 17156 dependencies: 17131 17157 "@jridgewell/trace-mapping" "^0.3.17" 17132 17158 jest-worker "^27.4.5" 17133 17159 schema-utils "^3.1.1" 17134 17160 serialize-javascript "^6.0.1" 17135 - terser "^5.16.5" 17161 + terser "^5.16.8" 17136 17162 17137 - terser@^5.0.0, terser@^5.10.0, terser@^5.15.0, terser@^5.16.5: 17138 - version "5.17.1" 17139 - resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.1.tgz#948f10830454761e2eeedc6debe45c532c83fd69" 17140 - integrity sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw== 17163 + terser@^5.0.0, terser@^5.10.0, terser@^5.15.0, terser@^5.16.8: 17164 + version "5.17.4" 17165 + resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.4.tgz#b0c2d94897dfeba43213ed5f90ed117270a2c696" 17166 + integrity sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw== 17141 17167 dependencies: 17142 17168 "@jridgewell/source-map" "^0.3.2" 17143 17169 acorn "^8.5.0" ··· 17333 17359 integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== 17334 17360 17335 17361 trace-event-lib@^1.3.1: 17336 - version "1.3.1" 17337 - resolved "https://registry.yarnpkg.com/trace-event-lib/-/trace-event-lib-1.3.1.tgz#8113146caa30778f45d0ec479d899f9eda94d594" 17338 - integrity sha512-RO/TD5E9RNqU6MhOfi/njFWKYhrzOJCpRXlEQHgXwM+6boLSrQnOZ9xbHwOXzC+Luyixc7LNNSiTsqTVeF7I1g== 17362 + version "1.4.1" 17363 + resolved "https://registry.yarnpkg.com/trace-event-lib/-/trace-event-lib-1.4.1.tgz#a749b8141650f56dcdecea760df4735f28d1ac6b" 17364 + integrity sha512-TOgFolKG8JFY+9d5EohGWMvwvteRafcyfPWWNIqcuD1W/FUvxWcy2MSCZ/beYHM63oYPHYHCd3tkbgCctHVP7w== 17339 17365 dependencies: 17340 17366 browser-process-hrtime "^1.0.0" 17341 - lodash "^4.17.21" 17342 17367 17343 17368 traverse@~0.6.6: 17344 17369 version "0.6.7" ··· 17470 17495 integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== 17471 17496 17472 17497 type-fest@^3.0.0: 17473 - version "3.9.0" 17474 - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.9.0.tgz#36a9e46e6583649f9e6098b267bc577275e9e4f4" 17475 - integrity sha512-hR8JP2e8UiH7SME5JZjsobBlEiatFoxpzCP+R3ZeCo7kAaG1jXQE5X/buLzogM6GJu8le9Y4OcfNuIQX0rZskA== 17498 + version "3.10.0" 17499 + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.10.0.tgz#d75f17a22be8816aea6315ab2739fe1c0c211863" 17500 + integrity sha512-hmAPf1datm+gt3c2mvu0sJyhFy6lTkIGf0GzyaZWxRLnabQfPUqg6tF95RPg6sLxKI7nFLGdFxBcf2/7+GXI+A== 17476 17501 17477 17502 type-is@~1.6.18: 17478 17503 version "1.6.18" ··· 17842 17867 browser-process-hrtime "^1.0.0" 17843 17868 17844 17869 w3c-keyname@^2.2.0: 17845 - version "2.2.6" 17846 - resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f" 17847 - integrity sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg== 17870 + version "2.2.7" 17871 + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.7.tgz#e29549e9ac97ac5cb2993c8222994e97922ef377" 17872 + integrity sha512-XB8aa62d4rrVfoZYQaYNy3fy+z4nrfy2ooea3/0BnBzXW0tSdZ+lRgjzBZhk0La0H6h8fVyYCxx/qkQcAIuvfg== 17848 17873 17849 17874 w3c-xmlserializer@^2.0.0: 17850 17875 version "2.0.0" ··· 17920 17945 integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== 17921 17946 17922 17947 webpack-cli@^5.0.1: 17923 - version "5.0.2" 17924 - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.0.2.tgz#2954c10ecb61c5d4dad6f68ee2d77f051741946c" 17925 - integrity sha512-4y3W5Dawri5+8dXm3+diW6Mn1Ya+Dei6eEVAdIduAmYNLzv1koKVAqsfgrrc9P2mhrYHQphx5htnGkcNwtubyQ== 17948 + version "5.1.1" 17949 + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.1.tgz#c211ac6d911e77c512978f7132f0d735d4a97ace" 17950 + integrity sha512-OLJwVMoXnXYH2ncNGU8gxVpUtm3ybvdioiTvHgUyBuyMLKiVvWy+QObzBsMtp5pH7qQoEuWgeEUQ/sU3ZJFzAw== 17926 17951 dependencies: 17927 17952 "@discoveryjs/json-ext" "^0.5.0" 17928 - "@webpack-cli/configtest" "^2.0.1" 17953 + "@webpack-cli/configtest" "^2.1.0" 17929 17954 "@webpack-cli/info" "^2.0.1" 17930 - "@webpack-cli/serve" "^2.0.2" 17955 + "@webpack-cli/serve" "^2.0.4" 17931 17956 colorette "^2.0.14" 17932 17957 commander "^10.0.1" 17933 17958 cross-spawn "^7.0.3" ··· 17950 17975 schema-utils "^4.0.0" 17951 17976 17952 17977 webpack-dev-server@^4.11.1, webpack-dev-server@^4.6.0: 17953 - version "4.13.3" 17954 - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.13.3.tgz#9feb740b8b56b886260bae1360286818a221bae8" 17955 - integrity sha512-KqqzrzMRSRy5ePz10VhjyL27K2dxqwXQLP5rAKwRJBPUahe7Z2bBWzHw37jeb8GCPKxZRO79ZdQUAPesMh/Nug== 17978 + version "4.15.0" 17979 + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.0.tgz#87ba9006eca53c551607ea0d663f4ae88be7af21" 17980 + integrity sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ== 17956 17981 dependencies: 17957 17982 "@types/bonjour" "^3.5.9" 17958 17983 "@types/connect-history-api-fallback" "^1.3.5" ··· 18023 18048 integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== 18024 18049 18025 18050 webpack@^5.64.4, webpack@^5.75.0: 18026 - version "5.81.0" 18027 - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.81.0.tgz#27a2e8466c8b4820d800a8d90f06ef98294f9956" 18028 - integrity sha512-AAjaJ9S4hYCVODKLQTgG5p5e11hiMawBwV2v8MYLE0C/6UAGLuAF4n1qa9GOwdxnicaP+5k6M5HrLmD4+gIB8Q== 18051 + version "5.83.0" 18052 + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.83.0.tgz#70b4b7c941ded3d2ad16d620c534a53dc27b0080" 18053 + integrity sha512-mdWk7amgh7hMCXzU+uTDGpIJEbkqat2RLgSDW53E1OOSE6U0gmBcWadJ6y0FdQQbGbW0lV2LT9t2iOEZc+FU7w== 18029 18054 dependencies: 18030 18055 "@types/eslint-scope" "^3.7.3" 18031 18056 "@types/estree" "^1.0.0" ··· 18036 18061 acorn-import-assertions "^1.7.6" 18037 18062 browserslist "^4.14.5" 18038 18063 chrome-trace-event "^1.0.2" 18039 - enhanced-resolve "^5.13.0" 18064 + enhanced-resolve "^5.14.0" 18040 18065 es-module-lexer "^1.2.1" 18041 18066 eslint-scope "5.1.1" 18042 18067 events "^3.2.0"