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 'main' of https://github.com/bluesky-social/social-app

+1093 -522
+28
bskyweb/cmd/bskyweb/filters.go
··· 1 + package main 2 + 3 + import ( 4 + "net/url" 5 + 6 + "github.com/flosch/pongo2/v6" 7 + ) 8 + 9 + func init() { 10 + pongo2.RegisterFilter("canonicalize_url", filterCanonicalizeURL) 11 + } 12 + 13 + func filterCanonicalizeURL(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { 14 + urlStr := in.String() 15 + 16 + parsedURL, err := url.Parse(urlStr) 17 + if err != nil { 18 + // If parsing fails, return the original URL 19 + return in, nil 20 + } 21 + 22 + // Remove query parameters and fragment 23 + parsedURL.RawQuery = "" 24 + parsedURL.Fragment = "" 25 + 26 + // Return the cleaned URL 27 + return pongo2.AsValue(parsedURL.String()), nil 28 + }
+61
bskyweb/cmd/bskyweb/filters_test.go
··· 1 + package main 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/flosch/pongo2/v6" 7 + ) 8 + 9 + func TestCanonicalizeURLFilter(t *testing.T) { 10 + tests := []struct { 11 + name string 12 + input string 13 + expected string 14 + }{ 15 + { 16 + name: "clean URL", 17 + input: "https://bsky.app/profile/user", 18 + expected: "https://bsky.app/profile/user", 19 + }, 20 + { 21 + name: "URL with query params", 22 + input: "https://bsky.app/profile/user?utm_source=test", 23 + expected: "https://bsky.app/profile/user", 24 + }, 25 + { 26 + name: "URL with multiple params", 27 + input: "https://bsky.app/profile/user?utm_source=twitter&utm_campaign=test", 28 + expected: "https://bsky.app/profile/user", 29 + }, 30 + { 31 + name: "URL with fragment", 32 + input: "https://bsky.app/profile/user#section", 33 + expected: "https://bsky.app/profile/user", 34 + }, 35 + { 36 + name: "URL with both params and fragment", 37 + input: "https://bsky.app/profile/user?param=1#section", 38 + expected: "https://bsky.app/profile/user", 39 + }, 40 + { 41 + name: "malformed URL", 42 + input: "not-a-url", 43 + expected: "not-a-url", // Should return original on error 44 + }, 45 + } 46 + 47 + for _, tt := range tests { 48 + t.Run(tt.name, func(t *testing.T) { 49 + inputValue := pongo2.AsValue(tt.input) 50 + result, err := filterCanonicalizeURL(inputValue, nil) 51 + if err != nil { 52 + t.Errorf("filterCanonicalizeURL() error = %v", err) 53 + return 54 + } 55 + 56 + if result.String() != tt.expected { 57 + t.Errorf("filterCanonicalizeURL() = %v, want %v", result.String(), tt.expected) 58 + } 59 + }) 60 + } 61 + }
+1 -1
bskyweb/templates/post.html
··· 14 14 <meta property="profile:username" content="{{ profileView.Handle }}"> 15 15 {%- if requestURI %} 16 16 <meta property="og:url" content="{{ requestURI }}"> 17 - <link rel="canonical" href="{{ requestURI }}" /> 17 + <link rel="canonical" href="{{ requestURI|canonicalize_url }}" /> 18 18 {% endif -%} 19 19 {%- if postView.Author.DisplayName %} 20 20 <meta property="og:title" content="{{ postView.Author.DisplayName }} (@{{ postView.Author.Handle }})">
+1 -1
bskyweb/templates/profile.html
··· 15 15 <meta property="profile:username" content="{{ profileView.Handle }}"> 16 16 {%- if requestURI %} 17 17 <meta property="og:url" content="{{ requestURI }}"> 18 - <link rel="canonical" href="{{ requestURI }}" /> 18 + <link rel="canonical" href="{{ requestURI|canonicalize_url }}" /> 19 19 {% endif -%} 20 20 {%- if profileView.DisplayName %} 21 21 <meta property="og:title" content="{{ profileView.DisplayName }} (@{{ profileView.Handle }})">
+1 -1
package.json
··· 21 21 "scripts": { 22 22 "prepare": "is-ci || husky install", 23 23 "postinstall": "patch-package && yarn intl:compile-if-needed", 24 - "prebuild": "expo prebuild --clean", 24 + "prebuild": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean", 25 25 "android": "expo run:android", 26 26 "android:prod": "expo run:android --variant release", 27 27 "android:profile": "BSKY_PROFILE=1 expo run:android --variant release",
+1 -1
src/Navigation.tsx
··· 55 55 import {FeedsScreen} from '#/view/screens/Feeds' 56 56 import {HomeScreen} from '#/view/screens/Home' 57 57 import {ListsScreen} from '#/view/screens/Lists' 58 - import {LogScreen} from '#/view/screens/Log' 59 58 import {ModerationBlockedAccounts} from '#/view/screens/ModerationBlockedAccounts' 60 59 import {ModerationModlistsScreen} from '#/view/screens/ModerationModlists' 61 60 import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts' ··· 74 73 import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth' 75 74 import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen' 76 75 import HashtagScreen from '#/screens/Hashtag' 76 + import {LogScreen} from '#/screens/Log' 77 77 import {MessagesScreen} from '#/screens/Messages/ChatList' 78 78 import {MessagesConversationScreen} from '#/screens/Messages/Conversation' 79 79 import {MessagesInboxScreen} from '#/screens/Messages/Inbox'
+3 -3
src/components/Dialog/index.web.tsx
··· 193 193 onInteractOutside={preventDefault} 194 194 onFocusOutside={preventDefault} 195 195 onDismiss={close} 196 - style={{display: 'flex', flexDirection: 'column'}}> 196 + style={{height: '100%', display: 'flex', flexDirection: 'column'}}> 197 197 {header} 198 198 <View style={[gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle]}> 199 199 {children} ··· 227 227 web({maxHeight: '80vh'}), 228 228 webInnerStyle, 229 229 ]} 230 - contentContainerStyle={[a.px_0, webInnerContentContainerStyle]}> 230 + contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}> 231 231 <FlatList 232 232 ref={ref} 233 - style={[gtMobile ? a.px_2xl : a.px_xl, flatten(style)]} 233 + style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, flatten(style)]} 234 234 {...props} 235 235 /> 236 236 </Inner>
+3 -2
src/components/Dialog/shared.tsx
··· 5 5 View, 6 6 type ViewStyle, 7 7 } from 'react-native' 8 - import type React from 'react' 9 8 10 9 import {atoms as a, useTheme} from '#/alf' 11 10 import {Text} from '#/components/Typography' ··· 28 27 <View 29 28 onLayout={onLayout} 30 29 style={[ 30 + a.sticky, 31 + a.top_0, 31 32 a.relative, 32 33 a.w_full, 33 34 a.py_sm, ··· 61 62 style?: StyleProp<TextStyle> 62 63 }) { 63 64 return ( 64 - <Text style={[a.text_lg, a.text_center, a.font_bold, style]}> 65 + <Text style={[a.text_lg, a.text_center, a.font_heavy, style]}> 65 66 {children} 66 67 </Text> 67 68 )
+23 -20
src/components/FeedInterstitials.tsx
··· 25 25 type ViewStyleProp, 26 26 web, 27 27 } from '#/alf' 28 - import {Button} from '#/components/Button' 28 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 29 29 import * as FeedCard from '#/components/FeedCard' 30 - import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' 30 + import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow' 31 31 import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' 32 32 import {InlineLinkText} from '#/components/Link' 33 33 import * as ProfileCard from '#/components/ProfileCard' ··· 36 36 import {ProgressGuideList} from './ProgressGuide/List' 37 37 38 38 const MOBILE_CARD_WIDTH = 165 39 + const FINAL_CARD_WIDTH = 120 39 40 40 41 function CardOuter({ 41 42 children, ··· 420 421 } 421 422 422 423 function SeeMoreSuggestedProfilesCard() { 424 + const t = useTheme() 425 + const {_} = useLingui() 423 426 const navigation = useNavigation<NavigationProp>() 424 - const {_} = useLingui() 425 427 426 428 return ( 427 429 <Button 430 + color="primary" 428 431 label={_(msg`Browse more accounts on the Explore page`)} 429 - style={[a.flex_col]} 430 - onPress={() => { 431 - navigation.navigate('SearchTab') 432 - }}> 433 - <CardOuter> 434 - <View style={[a.flex_1, a.justify_center]}> 435 - <View style={[a.flex_col, a.align_center, a.gap_md]}> 436 - <Text style={[a.leading_snug, a.text_center]}> 437 - <Trans>See more accounts you might like</Trans> 438 - </Text> 439 - 440 - <Arrow size="xl" /> 441 - </View> 442 - </View> 443 - </CardOuter> 432 + style={[ 433 + a.flex_col, 434 + a.align_center, 435 + a.gap_xs, 436 + a.p_md, 437 + a.rounded_lg, 438 + t.atoms.shadow_sm, 439 + {width: FINAL_CARD_WIDTH}, 440 + ]} 441 + onPress={() => navigation.navigate('SearchTab')}> 442 + <ButtonIcon icon={ArrowRight} size="lg" /> 443 + <ButtonText 444 + style={[a.text_md, a.font_medium, a.leading_snug, a.text_center]}> 445 + <Trans>See more</Trans> 446 + </ButtonText> 444 447 </Button> 445 448 ) 446 449 } ··· 539 542 style={[t.atoms.text_contrast_medium]}> 540 543 <Trans>Browse more suggestions</Trans> 541 544 </InlineLinkText> 542 - <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> 545 + <ArrowRight size="sm" fill={t.atoms.text_contrast_medium.color} /> 543 546 </View> 544 547 </View> 545 548 ) : ( ··· 567 570 </Trans> 568 571 </Text> 569 572 570 - <Arrow size="xl" /> 573 + <ArrowRight size="xl" /> 571 574 </View> 572 575 </View> 573 576 </CardOuter>
+1 -1
src/components/Layout/const.ts
··· 13 13 /** 14 14 * Corresponds to the width of a small square or round button 15 15 */ 16 - export const HEADER_SLOT_SIZE = 34 16 + export const HEADER_SLOT_SIZE = 33 17 17 18 18 /** 19 19 * How far to shift the center column when in the tablet breakpoint
+1 -2
src/components/MediaPreview.tsx
··· 100 100 <Image 101 101 key={thumbnail} 102 102 source={{uri: thumbnail}} 103 + alt={alt} 103 104 style={[a.flex_1, a.rounded_xs, t.atoms.bg_contrast_25]} 104 105 contentFit="cover" 105 106 accessible={true} 106 107 accessibilityIgnoresInvertColors 107 - accessibilityHint={alt} 108 - accessibilityLabel="" 109 108 /> 110 109 <MediaInsetBorder style={[a.rounded_xs]} /> 111 110 {children}
+9 -3
src/components/Post/Embed/index.tsx
··· 90 90 switch (embed.type) { 91 91 case 'images': { 92 92 return ( 93 - <ContentHider modui={rest.moderation?.ui('contentMedia')}> 93 + <ContentHider 94 + modui={rest.moderation?.ui('contentMedia')} 95 + activeStyle={[a.mt_sm]}> 94 96 <ImageEmbed embed={embed} {...rest} /> 95 97 </ContentHider> 96 98 ) 97 99 } 98 100 case 'link': { 99 101 return ( 100 - <ContentHider modui={rest.moderation?.ui('contentMedia')}> 102 + <ContentHider 103 + modui={rest.moderation?.ui('contentMedia')} 104 + activeStyle={[a.mt_sm]}> 101 105 <ExternalEmbed 102 106 link={embed.view.external} 103 107 onOpen={rest.onOpen} ··· 108 112 } 109 113 case 'video': { 110 114 return ( 111 - <ContentHider modui={rest.moderation?.ui('contentMedia')}> 115 + <ContentHider 116 + modui={rest.moderation?.ui('contentMedia')} 117 + activeStyle={[a.mt_sm]}> 112 118 <VideoEmbed embed={embed.view} /> 113 119 </ContentHider> 114 120 )
+3 -3
src/components/StarterPack/ProfileStarterPacks.tsx
··· 180 180 color="secondary" 181 181 size="small" 182 182 style={[a.self_center]} 183 - onPress={() => navigation.navigate('StarterPackWizard')}> 183 + onPress={() => navigation.navigate('StarterPackWizard', {})}> 184 184 <ButtonText> 185 185 <Trans>Create another</Trans> 186 186 </ButtonText> ··· 238 238 ], 239 239 }) 240 240 const navToWizard = useCallback(() => { 241 - navigation.navigate('StarterPackWizard') 241 + navigation.navigate('StarterPackWizard', {}) 242 242 }, [navigation]) 243 243 const wrappedNavToWizard = requireEmailVerification(navToWizard, { 244 244 instructions: [ ··· 322 322 color="secondary" 323 323 cta={_(msg`Let me choose`)} 324 324 onPress={() => { 325 - navigation.navigate('StarterPackWizard') 325 + navigation.navigate('StarterPackWizard', {}) 326 326 }} 327 327 /> 328 328 </Prompt.Actions>
+1 -6
src/components/StarterPack/Wizard/WizardEditListDialog.tsx
··· 11 11 12 12 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 13 13 import {isWeb} from '#/platform/detection' 14 - import {useSession} from '#/state/session' 15 14 import {type ListMethods} from '#/view/com/util/List' 16 15 import { 17 16 type WizardAction, ··· 48 47 }) { 49 48 const {_} = useLingui() 50 49 const t = useTheme() 51 - const {currentAccount} = useSession() 52 50 const initialNumToRender = useInitialNumToRender() 53 51 54 52 const listRef = useRef<ListMethods>(null) ··· 56 54 const getData = () => { 57 55 if (state.currentStep === 'Feeds') return state.feeds 58 56 59 - return [ 60 - profile, 61 - ...state.profiles.filter(p => p.did !== currentAccount?.did), 62 - ] 57 + return [profile, ...state.profiles.filter(p => p.did !== profile.did)] 63 58 } 64 59 65 60 const renderItem = ({item}: ListRenderItemInfo<any>) =>
+7 -4
src/components/StarterPack/Wizard/WizardListCard.tsx
··· 131 131 }) { 132 132 const {currentAccount} = useSession() 133 133 134 - const isMe = profile.did === currentAccount?.did 135 - const included = isMe || state.profiles.some(p => p.did === profile.did) 134 + // Determine the "main" profile for this starter pack - either targetDid or current account 135 + const targetProfileDid = state.targetDid || currentAccount?.did 136 + const isTarget = profile.did === targetProfileDid 137 + const included = isTarget || state.profiles.some(p => p.did === profile.did) 136 138 const disabled = 137 - isMe || (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1) 139 + isTarget || 140 + (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1) 138 141 const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar') 139 142 const displayName = profile.displayName 140 143 ? sanitizeDisplayName(profile.displayName) ··· 144 147 if (disabled) return 145 148 146 149 Keyboard.dismiss() 147 - if (profile.did === currentAccount?.did) return 150 + if (profile.did === targetProfileDid) return 148 151 149 152 if (!included) { 150 153 dispatch({type: 'AddProfile', profile})
+6 -8
src/components/dialogs/GifSelect.tsx
··· 1 - import React, { 1 + import { 2 2 useCallback, 3 3 useImperativeHandle, 4 4 useMemo, ··· 119 119 [onSelectGif], 120 120 ) 121 121 122 - const onEndReached = React.useCallback(() => { 122 + const onEndReached = useCallback(() => { 123 123 if (isFetchingNextPage || !hasNextPage || error) return 124 124 fetchNextPage() 125 125 }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) ··· 172 172 </Button> 173 173 )} 174 174 175 - <TextField.Root> 175 + <TextField.Root style={[!gtMobile && isWeb && a.flex_1]}> 176 176 <TextField.Icon icon={Search} /> 177 177 <TextField.Input 178 178 label={_(msg`Search GIFs`)} ··· 206 206 renderItem={renderItem} 207 207 numColumns={gtMobile ? 3 : 2} 208 208 columnWrapperStyle={[a.gap_sm]} 209 - contentContainerStyle={[ 210 - native([a.px_xl, {minHeight: height}]), 211 - web(a.h_full_vh), 212 - ]} 213 - style={[web(a.h_full_vh)]} 209 + contentContainerStyle={[native([a.px_xl, {minHeight: height}])]} 210 + webInnerStyle={[web({minHeight: '80vh'})]} 211 + webInnerContentContainerStyle={[web(a.pb_0)]} 214 212 ListHeaderComponent={ 215 213 <> 216 214 {listHeader}
+399
src/components/dialogs/StarterPackDialog.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import { 4 + type AppBskyGraphGetStarterPacksWithMembership, 5 + AppBskyGraphStarterpack, 6 + } from '@atproto/api' 7 + import {msg, Plural, Trans} from '@lingui/macro' 8 + import {useLingui} from '@lingui/react' 9 + import {useNavigation} from '@react-navigation/native' 10 + import {useQueryClient} from '@tanstack/react-query' 11 + 12 + import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' 13 + import {type NavigationProp} from '#/lib/routes/types' 14 + import {isWeb} from '#/platform/detection' 15 + import { 16 + invalidateActorStarterPacksWithMembershipQuery, 17 + useActorStarterPacksWithMembershipsQuery, 18 + } from '#/state/queries/actor-starter-packs' 19 + import { 20 + useListMembershipAddMutation, 21 + useListMembershipRemoveMutation, 22 + } from '#/state/queries/list-memberships' 23 + import * as Toast from '#/view/com/util/Toast' 24 + import {atoms as a, useTheme} from '#/alf' 25 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 26 + import * as Dialog from '#/components/Dialog' 27 + import {Divider} from '#/components/Divider' 28 + import {Loader} from '#/components/Loader' 29 + import {Text} from '#/components/Typography' 30 + import * as bsky from '#/types/bsky' 31 + import {AvatarStack} from '../AvatarStack' 32 + import {PlusLarge_Stroke2_Corner0_Rounded} from '../icons/Plus' 33 + import {StarterPack} from '../icons/StarterPack' 34 + import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times' 35 + 36 + type StarterPackWithMembership = 37 + AppBskyGraphGetStarterPacksWithMembership.StarterPackWithMembership 38 + 39 + export type StarterPackDialogProps = { 40 + control: Dialog.DialogControlProps 41 + targetDid: string 42 + enabled?: boolean 43 + } 44 + 45 + export function StarterPackDialog({ 46 + control, 47 + targetDid, 48 + enabled, 49 + }: StarterPackDialogProps) { 50 + const {_} = useLingui() 51 + const navigation = useNavigation<NavigationProp>() 52 + const requireEmailVerification = useRequireEmailVerification() 53 + 54 + const navToWizard = React.useCallback(() => { 55 + control.close() 56 + navigation.navigate('StarterPackWizard', { 57 + fromDialog: true, 58 + targetDid: targetDid, 59 + onSuccess: () => { 60 + setTimeout(() => { 61 + if (!control.isOpen) { 62 + control.open() 63 + } 64 + }, 0) 65 + }, 66 + }) 67 + }, [navigation, control, targetDid]) 68 + 69 + const wrappedNavToWizard = requireEmailVerification(navToWizard, { 70 + instructions: [ 71 + <Trans key="nav"> 72 + Before creating a starter pack, you must first verify your email. 73 + </Trans>, 74 + ], 75 + }) 76 + 77 + return ( 78 + <Dialog.Outer control={control}> 79 + <Dialog.Handle /> 80 + <StarterPackList 81 + control={control} 82 + onStartWizard={wrappedNavToWizard} 83 + targetDid={targetDid} 84 + enabled={enabled} 85 + /> 86 + </Dialog.Outer> 87 + ) 88 + } 89 + 90 + function Empty({onStartWizard}: {onStartWizard: () => void}) { 91 + const {_} = useLingui() 92 + const t = useTheme() 93 + 94 + isWeb 95 + return ( 96 + <View style={[a.gap_2xl, {paddingTop: isWeb ? 100 : 64}]}> 97 + <View style={[a.gap_xs, a.align_center]}> 98 + <StarterPack 99 + width={48} 100 + fill={t.atoms.border_contrast_medium.borderColor} 101 + /> 102 + <Text style={[a.text_center]}> 103 + <Trans>You have no starter packs.</Trans> 104 + </Text> 105 + </View> 106 + 107 + <View style={[a.align_center]}> 108 + <Button 109 + label={_(msg`Create starter pack`)} 110 + color="secondary_inverted" 111 + size="small" 112 + onPress={onStartWizard}> 113 + <ButtonText> 114 + <Trans comment="Text on button to create a new starter pack"> 115 + Create 116 + </Trans> 117 + </ButtonText> 118 + <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} /> 119 + </Button> 120 + </View> 121 + </View> 122 + ) 123 + } 124 + 125 + function StarterPackList({ 126 + control, 127 + onStartWizard, 128 + targetDid, 129 + enabled, 130 + }: { 131 + control: Dialog.DialogControlProps 132 + onStartWizard: () => void 133 + targetDid: string 134 + enabled?: boolean 135 + }) { 136 + const {_} = useLingui() 137 + const t = useTheme() 138 + 139 + const { 140 + data, 141 + refetch, 142 + isError, 143 + isLoading, 144 + hasNextPage, 145 + isFetchingNextPage, 146 + fetchNextPage, 147 + } = useActorStarterPacksWithMembershipsQuery({did: targetDid, enabled}) 148 + 149 + const membershipItems = 150 + data?.pages.flatMap(page => page.starterPacksWithMembership) || [] 151 + 152 + const _onRefresh = React.useCallback(async () => { 153 + try { 154 + await refetch() 155 + } catch (err) { 156 + // Error handling is optional since this is just a refresh 157 + } 158 + }, [refetch]) 159 + 160 + const _onEndReached = React.useCallback(async () => { 161 + if (isFetchingNextPage || !hasNextPage || isError) return 162 + try { 163 + await fetchNextPage() 164 + } catch (err) { 165 + // Error handling is optional since this is just pagination 166 + } 167 + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 168 + 169 + const renderItem = React.useCallback( 170 + ({item}: {item: StarterPackWithMembership}) => ( 171 + <StarterPackItem starterPackWithMembership={item} targetDid={targetDid} /> 172 + ), 173 + [targetDid], 174 + ) 175 + 176 + const onClose = React.useCallback(() => { 177 + control.close() 178 + }, [control]) 179 + 180 + const XIcon = React.useMemo(() => { 181 + return ( 182 + <TimesLarge_Stroke2_Corner0_Rounded 183 + fill={t.atoms.text_contrast_medium.color} 184 + /> 185 + ) 186 + }, [t]) 187 + 188 + const listHeader = ( 189 + <> 190 + <View 191 + style={[ 192 + {justifyContent: 'space-between', flexDirection: 'row'}, 193 + isWeb ? a.mb_2xl : a.my_lg, 194 + a.align_center, 195 + ]}> 196 + <Text style={[a.text_lg, a.font_bold]}> 197 + <Trans>Add to starter packs</Trans> 198 + </Text> 199 + <Button label={_(msg`Close`)} onPress={onClose}> 200 + <ButtonIcon icon={() => XIcon} /> 201 + </Button> 202 + </View> 203 + {membershipItems.length > 0 && ( 204 + <> 205 + <View 206 + style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}> 207 + <Text style={[a.text_md, a.font_bold]}> 208 + <Trans>New starter pack</Trans> 209 + </Text> 210 + <Button 211 + label={_(msg`Create starter pack`)} 212 + color="secondary_inverted" 213 + size="small" 214 + onPress={onStartWizard}> 215 + <ButtonText> 216 + <Trans comment="Text on button to create a new starter pack"> 217 + Create 218 + </Trans> 219 + </ButtonText> 220 + <ButtonIcon icon={PlusLarge_Stroke2_Corner0_Rounded} /> 221 + </Button> 222 + </View> 223 + <Divider /> 224 + </> 225 + )} 226 + </> 227 + ) 228 + 229 + return ( 230 + <Dialog.InnerFlatList 231 + data={isLoading ? [{}] : membershipItems} 232 + renderItem={ 233 + isLoading 234 + ? () => ( 235 + <View style={[a.align_center, a.py_2xl]}> 236 + <Loader size="xl" /> 237 + </View> 238 + ) 239 + : renderItem 240 + } 241 + keyExtractor={ 242 + isLoading 243 + ? () => 'starter_pack_dialog_loader' 244 + : (item: StarterPackWithMembership) => item.starterPack.uri 245 + } 246 + refreshing={false} 247 + onRefresh={_onRefresh} 248 + onEndReached={_onEndReached} 249 + onEndReachedThreshold={0.1} 250 + ListHeaderComponent={listHeader} 251 + ListEmptyComponent={<Empty onStartWizard={onStartWizard} />} 252 + style={isWeb ? [a.px_md, {minHeight: 500}] : [a.px_2xl, a.pt_lg]} 253 + /> 254 + ) 255 + } 256 + 257 + function StarterPackItem({ 258 + starterPackWithMembership, 259 + targetDid, 260 + }: { 261 + starterPackWithMembership: StarterPackWithMembership 262 + targetDid: string 263 + }) { 264 + const {_} = useLingui() 265 + const t = useTheme() 266 + const queryClient = useQueryClient() 267 + 268 + const starterPack = starterPackWithMembership.starterPack 269 + const isInPack = !!starterPackWithMembership.listItem 270 + 271 + const [isPendingRefresh, setIsPendingRefresh] = React.useState(false) 272 + 273 + const {mutate: addMembership} = useListMembershipAddMutation({ 274 + onSuccess: () => { 275 + Toast.show(_(msg`Added to starter pack`)) 276 + // Use a timeout to wait for the appview to update, matching the pattern 277 + // in list-memberships.ts 278 + setTimeout(() => { 279 + invalidateActorStarterPacksWithMembershipQuery({ 280 + queryClient, 281 + did: targetDid, 282 + }) 283 + setIsPendingRefresh(false) 284 + }, 1e3) 285 + }, 286 + onError: () => { 287 + Toast.show(_(msg`Failed to add to starter pack`), 'xmark') 288 + setIsPendingRefresh(false) 289 + }, 290 + }) 291 + 292 + const {mutate: removeMembership} = useListMembershipRemoveMutation({ 293 + onSuccess: () => { 294 + Toast.show(_(msg`Removed from starter pack`)) 295 + // Use a timeout to wait for the appview to update, matching the pattern 296 + // in list-memberships.ts 297 + setTimeout(() => { 298 + invalidateActorStarterPacksWithMembershipQuery({ 299 + queryClient, 300 + did: targetDid, 301 + }) 302 + setIsPendingRefresh(false) 303 + }, 1e3) 304 + }, 305 + onError: () => { 306 + Toast.show(_(msg`Failed to remove from starter pack`), 'xmark') 307 + setIsPendingRefresh(false) 308 + }, 309 + }) 310 + 311 + const handleToggleMembership = () => { 312 + if (!starterPack.list?.uri || isPendingRefresh) return 313 + 314 + const listUri = starterPack.list.uri 315 + 316 + setIsPendingRefresh(true) 317 + 318 + if (!isInPack) { 319 + addMembership({ 320 + listUri: listUri, 321 + actorDid: targetDid, 322 + }) 323 + } else { 324 + if (!starterPackWithMembership.listItem?.uri) { 325 + console.error('Cannot remove: missing membership URI') 326 + setIsPendingRefresh(false) 327 + return 328 + } 329 + removeMembership({ 330 + listUri: listUri, 331 + actorDid: targetDid, 332 + membershipUri: starterPackWithMembership.listItem.uri, 333 + }) 334 + } 335 + } 336 + 337 + const {record} = starterPack 338 + 339 + if ( 340 + !bsky.dangerousIsType<AppBskyGraphStarterpack.Record>( 341 + record, 342 + AppBskyGraphStarterpack.isRecord, 343 + ) 344 + ) { 345 + return null 346 + } 347 + 348 + return ( 349 + <View style={[a.flex_row, a.justify_between, a.align_center, a.py_md]}> 350 + <View> 351 + <Text emoji style={[a.text_md, a.font_bold]} numberOfLines={1}> 352 + {record.name} 353 + </Text> 354 + 355 + <View style={[a.flex_row, a.align_center, a.mt_xs]}> 356 + {starterPack.listItemsSample && 357 + starterPack.listItemsSample.length > 0 && ( 358 + <> 359 + <AvatarStack 360 + size={32} 361 + profiles={starterPack.listItemsSample 362 + ?.slice(0, 4) 363 + .map(p => p.subject)} 364 + /> 365 + 366 + {starterPack.list?.listItemCount && 367 + starterPack.list.listItemCount > 4 && ( 368 + <Text 369 + style={[ 370 + a.text_sm, 371 + t.atoms.text_contrast_medium, 372 + a.ml_xs, 373 + ]}> 374 + <Trans> 375 + <Plural 376 + value={starterPack.list.listItemCount - 4} 377 + other="+# more" 378 + /> 379 + </Trans> 380 + </Text> 381 + )} 382 + </> 383 + )} 384 + </View> 385 + </View> 386 + 387 + <Button 388 + label={isInPack ? _(msg`Remove`) : _(msg`Add`)} 389 + color={isInPack ? 'secondary' : 'primary'} 390 + size="tiny" 391 + disabled={isPendingRefresh} 392 + onPress={handleToggleMembership}> 393 + <ButtonText> 394 + {isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>} 395 + </ButtonText> 396 + </Button> 397 + </View> 398 + ) 399 + }
+20 -20
src/components/dms/ConvoMenu.tsx
··· 1 1 import React, {useCallback} from 'react' 2 - import {Keyboard, Pressable, View} from 'react-native' 3 - import {ChatBskyConvoDefs, ModerationCause} from '@atproto/api' 2 + import {Keyboard, View} from 'react-native' 3 + import {type ChatBskyConvoDefs, type ModerationCause} from '@atproto/api' 4 4 import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 import {useNavigation} from '@react-navigation/native' 7 7 8 - import {NavigationProp} from '#/lib/routes/types' 9 - import {Shadow} from '#/state/cache/types' 8 + import {type NavigationProp} from '#/lib/routes/types' 9 + import {type Shadow} from '#/state/cache/types' 10 10 import { 11 11 useConvoQuery, 12 12 useMarkAsReadMutation, ··· 14 14 import {useMuteConvo} from '#/state/queries/messages/mute-conversation' 15 15 import {useProfileBlockMutationQueue} from '#/state/queries/profile' 16 16 import * as Toast from '#/view/com/util/Toast' 17 - import {atoms as a, useTheme, ViewStyleProp} from '#/alf' 17 + import {type ViewStyleProp} from '#/alf' 18 + import {atoms as a} from '#/alf' 19 + import {Button, ButtonIcon} from '#/components/Button' 18 20 import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog' 19 21 import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' 20 22 import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt' 23 + import {ReportDialog} from '#/components/dms/ReportDialog' 21 24 import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft' 25 + import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble' 22 26 import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' 23 27 import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' 24 28 import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' ··· 30 34 import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 31 35 import * as Menu from '#/components/Menu' 32 36 import * as Prompt from '#/components/Prompt' 33 - import * as bsky from '#/types/bsky' 34 - import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble' 35 - import {ReportDialog} from './ReportDialog' 37 + import type * as bsky from '#/types/bsky' 36 38 37 39 let ConvoMenu = ({ 38 40 convo, ··· 59 61 style?: ViewStyleProp['style'] 60 62 }): React.ReactNode => { 61 63 const {_} = useLingui() 62 - const t = useTheme() 63 64 64 65 const leaveConvoControl = Prompt.usePromptControl() 65 66 const reportControl = Prompt.usePromptControl() ··· 73 74 {!hideTrigger && ( 74 75 <View style={[style]}> 75 76 <Menu.Trigger label={_(msg`Chat settings`)}> 76 - {({props, state}) => ( 77 - <Pressable 77 + {({props}) => ( 78 + <Button 79 + label={props.accessibilityLabel} 78 80 {...props} 79 81 onPress={() => { 80 82 Keyboard.dismiss() 81 83 props.onPress() 82 84 }} 83 - style={[ 84 - a.p_sm, 85 - a.rounded_full, 86 - (state.hovered || state.pressed) && t.atoms.bg_contrast_25, 87 - // make sure pfp is in the middle 88 - {marginLeft: -10}, 89 - ]}> 90 - <DotsHorizontal size="md" style={t.atoms.text} /> 91 - </Pressable> 85 + size="small" 86 + color="secondary" 87 + shape="round" 88 + variant="ghost" 89 + style={[a.bg_transparent]}> 90 + <ButtonIcon icon={DotsHorizontal} size="md" /> 91 + </Button> 92 92 )} 93 93 </Menu.Trigger> 94 94 </View>
+67 -104
src/components/dms/MessagesListHeader.tsx
··· 1 - import React, {useCallback} from 'react' 2 - import {TouchableOpacity, View} from 'react-native' 1 + import {useMemo} from 'react' 2 + import {View} from 'react-native' 3 3 import { 4 4 type AppBskyActorDefs, 5 5 type ModerationCause, 6 6 type ModerationDecision, 7 7 } from '@atproto/api' 8 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 9 8 import {msg} from '@lingui/macro' 10 9 import {useLingui} from '@lingui/react' 11 - import {useNavigation} from '@react-navigation/native' 12 10 13 - import {BACK_HITSLOP} from '#/lib/constants' 14 11 import {makeProfileLink} from '#/lib/routes/links' 15 - import {type NavigationProp} from '#/lib/routes/types' 16 12 import {sanitizeDisplayName} from '#/lib/strings/display-names' 17 13 import {isWeb} from '#/platform/detection' 18 14 import {type Shadow} from '#/state/cache/profile-shadow' 19 15 import {isConvoActive, useConvo} from '#/state/messages/convo' 20 16 import {type ConvoItem} from '#/state/messages/convo/types' 21 17 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 22 - import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 18 + import {atoms as a, useTheme, web} from '#/alf' 23 19 import {ConvoMenu} from '#/components/dms/ConvoMenu' 24 20 import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' 21 + import * as Layout from '#/components/Layout' 25 22 import {Link} from '#/components/Link' 26 23 import {PostAlerts} from '#/components/moderation/PostAlerts' 27 24 import {Text} from '#/components/Typography' 28 25 import {useSimpleVerificationState} from '#/components/verification' 29 26 import {VerificationCheck} from '#/components/verification/VerificationCheck' 30 27 31 - const PFP_SIZE = isWeb ? 40 : 34 28 + const PFP_SIZE = isWeb ? 40 : Layout.HEADER_SLOT_SIZE 32 29 33 - export let MessagesListHeader = ({ 30 + export function MessagesListHeader({ 34 31 profile, 35 32 moderation, 36 33 }: { 37 34 profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed> 38 35 moderation?: ModerationDecision 39 - }): React.ReactNode => { 36 + }) { 40 37 const t = useTheme() 41 - const {_} = useLingui() 42 - const {gtTablet} = useBreakpoints() 43 - const navigation = useNavigation<NavigationProp>() 44 38 45 - const blockInfo = React.useMemo(() => { 39 + const blockInfo = useMemo(() => { 46 40 if (!moderation) return 47 41 const modui = moderation.ui('profileView') 48 42 const blocks = modui.alerts.filter(alert => alert.type === 'blocking') ··· 54 48 } 55 49 }, [moderation]) 56 50 57 - const onPressBack = useCallback(() => { 58 - if (navigation.canGoBack()) { 59 - navigation.goBack() 60 - } else { 61 - navigation.navigate('Messages', {}) 62 - } 63 - }, [navigation]) 64 - 65 51 return ( 66 - <View 67 - style={[ 68 - t.atoms.bg, 69 - t.atoms.border_contrast_low, 70 - a.border_b, 71 - a.flex_row, 72 - a.align_start, 73 - a.gap_sm, 74 - gtTablet ? a.pl_lg : a.pl_xl, 75 - a.pr_lg, 76 - a.py_sm, 77 - ]}> 78 - <TouchableOpacity 79 - testID="conversationHeaderBackBtn" 80 - onPress={onPressBack} 81 - hitSlop={BACK_HITSLOP} 82 - style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}} 83 - accessibilityRole="button" 84 - accessibilityLabel={_(msg`Back`)} 85 - accessibilityHint=""> 86 - <FontAwesomeIcon 87 - size={18} 88 - icon="angle-left" 89 - style={{ 90 - marginTop: 6, 91 - }} 92 - color={t.atoms.text.color} 93 - /> 94 - </TouchableOpacity> 95 - 96 - {profile && moderation && blockInfo ? ( 97 - <HeaderReady 98 - profile={profile} 99 - moderation={moderation} 100 - blockInfo={blockInfo} 101 - /> 102 - ) : ( 103 - <> 104 - <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}> 105 - <View 106 - style={[ 107 - {width: PFP_SIZE, height: PFP_SIZE}, 108 - a.rounded_full, 109 - t.atoms.bg_contrast_25, 110 - ]} 111 - /> 112 - <View style={a.gap_xs}> 52 + <Layout.Header.Outer> 53 + <View style={[a.w_full, a.flex_row, a.gap_xs, a.align_start]}> 54 + <View style={[{minHeight: PFP_SIZE}, a.justify_center]}> 55 + <Layout.Header.BackButton /> 56 + </View> 57 + {profile && moderation && blockInfo ? ( 58 + <HeaderReady 59 + profile={profile} 60 + moderation={moderation} 61 + blockInfo={blockInfo} 62 + /> 63 + ) : ( 64 + <> 65 + <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}> 113 66 <View 114 67 style={[ 115 - {width: 120, height: 16}, 116 - a.rounded_xs, 68 + {width: PFP_SIZE, height: PFP_SIZE}, 69 + a.rounded_full, 117 70 t.atoms.bg_contrast_25, 118 - a.mt_xs, 119 71 ]} 120 72 /> 121 - <View 122 - style={[ 123 - {width: 175, height: 12}, 124 - a.rounded_xs, 125 - t.atoms.bg_contrast_25, 126 - ]} 127 - /> 73 + <View style={a.gap_xs}> 74 + <View 75 + style={[ 76 + {width: 120, height: 16}, 77 + a.rounded_xs, 78 + t.atoms.bg_contrast_25, 79 + a.mt_xs, 80 + ]} 81 + /> 82 + <View 83 + style={[ 84 + {width: 175, height: 12}, 85 + a.rounded_xs, 86 + t.atoms.bg_contrast_25, 87 + ]} 88 + /> 89 + </View> 128 90 </View> 129 - </View> 130 91 131 - <View style={{width: 30}} /> 132 - </> 133 - )} 134 - </View> 92 + <Layout.Header.Slot /> 93 + </> 94 + )} 95 + </View> 96 + </Layout.Header.Outer> 135 97 ) 136 98 } 137 - MessagesListHeader = React.memo(MessagesListHeader) 138 99 139 100 function HeaderReady({ 140 101 profile, ··· 181 142 label={_(msg`View ${displayName}'s profile`)} 182 143 style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]} 183 144 to={makeProfileLink(profile)}> 184 - <View style={[a.pt_2xs]}> 185 - <PreviewableUserAvatar 186 - size={PFP_SIZE} 187 - profile={profile} 188 - moderation={moderation.ui('avatar')} 189 - disableHoverCard={moderation.blocked} 190 - /> 191 - </View> 192 - <View style={a.flex_1}> 145 + <PreviewableUserAvatar 146 + size={PFP_SIZE} 147 + profile={profile} 148 + moderation={moderation.ui('avatar')} 149 + disableHoverCard={moderation.blocked} 150 + /> 151 + <View style={[a.flex_1]}> 193 152 <View style={[a.flex_row, a.align_center]}> 194 153 <Text 195 154 emoji ··· 215 174 <Text 216 175 style={[ 217 176 t.atoms.text_contrast_medium, 218 - a.text_sm, 177 + a.text_xs, 219 178 web([a.leading_normal, {marginTop: -2}]), 220 179 ]} 221 180 numberOfLines={1}> ··· 235 194 </View> 236 195 </Link> 237 196 238 - {isConvoActive(convoState) && ( 239 - <ConvoMenu 240 - convo={convoState.convo} 241 - profile={profile} 242 - currentScreen="conversation" 243 - blockInfo={blockInfo} 244 - latestReportableMessage={latestReportableMessage} 245 - /> 246 - )} 197 + <View style={[{minHeight: PFP_SIZE}, a.justify_center]}> 198 + <Layout.Header.Slot> 199 + {isConvoActive(convoState) && ( 200 + <ConvoMenu 201 + convo={convoState.convo} 202 + profile={profile} 203 + currentScreen="conversation" 204 + blockInfo={blockInfo} 205 + latestReportableMessage={latestReportableMessage} 206 + /> 207 + )} 208 + </Layout.Header.Slot> 209 + </View> 247 210 </View> 248 211 249 212 <View
+12 -3
src/components/forms/TextField.tsx
··· 48 48 }) 49 49 Context.displayName = 'TextFieldContext' 50 50 51 - export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}> 51 + export type RootProps = React.PropsWithChildren< 52 + {isInvalid?: boolean} & TextStyleProp 53 + > 52 54 53 - export function Root({children, isInvalid = false}: RootProps) { 55 + export function Root({children, isInvalid = false, style}: RootProps) { 54 56 const inputRef = useRef<TextInput>(null) 55 57 const { 56 58 state: hovered, ··· 85 87 return ( 86 88 <Context.Provider value={context}> 87 89 <View 88 - style={[a.flex_row, a.align_center, a.relative, a.w_full, a.px_md]} 90 + style={[ 91 + a.flex_row, 92 + a.align_center, 93 + a.relative, 94 + a.w_full, 95 + a.px_md, 96 + style, 97 + ]} 89 98 {...web({ 90 99 onClick: () => inputRef.current?.focus(), 91 100 onMouseOver: onHoverIn,
+8 -15
src/lib/generate-starterpack.ts
··· 1 1 import { 2 - $Typed, 3 - AppBskyActorDefs, 4 - AppBskyGraphGetStarterPack, 5 - BskyAgent, 6 - ComAtprotoRepoApplyWrites, 7 - Facet, 2 + type $Typed, 3 + type AppBskyActorDefs, 4 + type AppBskyGraphGetStarterPack, 5 + type BskyAgent, 6 + type ComAtprotoRepoApplyWrites, 7 + type Facet, 8 8 } from '@atproto/api' 9 9 import {msg} from '@lingui/macro' 10 10 import {useLingui} from '@lingui/react' ··· 15 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 16 import {enforceLen} from '#/lib/strings/helpers' 17 17 import {useAgent} from '#/state/session' 18 - import * as bsky from '#/types/bsky' 18 + import type * as bsky from '#/types/bsky' 19 19 20 20 export const createStarterPackList = async ({ 21 21 name, ··· 46 46 if (!list) throw new Error('List creation failed') 47 47 await agent.com.atproto.repo.applyWrites({ 48 48 repo: agent.session!.did, 49 - writes: [ 50 - createListItem({did: agent.session!.did, listUri: list.uri}), 51 - ].concat( 52 - profiles 53 - // Ensure we don't have ourselves in this list twice 54 - .filter(p => p.did !== agent.session!.did) 55 - .map(p => createListItem({did: p.did, listUri: list.uri})), 56 - ), 49 + writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})), 57 50 }) 58 51 59 52 return list
+5 -1
src/lib/routes/types.ts
··· 79 79 Start: {name: string; rkey: string} 80 80 StarterPack: {name: string; rkey: string; new?: boolean} 81 81 StarterPackShort: {code: string} 82 - StarterPackWizard: undefined 82 + StarterPackWizard: { 83 + fromDialog?: boolean 84 + targetDid?: string 85 + onSuccess?: () => void 86 + } 83 87 StarterPackEdit: {rkey?: string} 84 88 VideoFeed: VideoFeedSourceContext 85 89 }
-1
src/lib/statsig/gates.ts
··· 5 5 | 'debug_subscriptions' 6 6 | 'disable_onboarding_policy_update_notice' 7 7 | 'explore_show_suggested_feeds' 8 - | 'handle_suggestions' 9 8 | 'old_postonboarding' 10 9 | 'onboarding_add_video_feed' 11 10 | 'post_follow_profile_suggested_accounts'
+4
src/lib/strings/helpers.ts
··· 84 84 return query 85 85 } 86 86 87 + // replace “smart quotes” with normal ones 88 + // iOS keyboard will add fancy unicode quotes, but only normal ones work 89 + query = query.replaceAll(/[“”]/g, '"') 90 + 87 91 // We don't want to replace substrings that are being "quoted" because those 88 92 // are exact string matches, so what we'll do here is to split them apart 89 93
+72 -72
src/locale/locales/en/messages.po
··· 145 145 msgid "{0} is not a valid URL" 146 146 msgstr "" 147 147 148 - #: src/screens/Signup/StepHandle/index.tsx:189 148 + #: src/screens/Signup/StepHandle/index.tsx:186 149 149 msgid "{0} is not available" 150 150 msgstr "" 151 151 ··· 691 691 msgid "Add another account" 692 692 msgstr "" 693 693 694 - #: src/view/com/composer/Composer.tsx:793 694 + #: src/view/com/composer/Composer.tsx:788 695 695 msgid "Add another post" 696 696 msgstr "" 697 697 ··· 727 727 msgid "Add muted words and tags" 728 728 msgstr "" 729 729 730 - #: src/view/com/composer/Composer.tsx:1426 730 + #: src/view/com/composer/Composer.tsx:1421 731 731 msgid "Add new post" 732 732 msgstr "" 733 733 ··· 922 922 msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below." 923 923 msgstr "" 924 924 925 - #: src/components/dialogs/GifSelect.tsx:266 925 + #: src/components/dialogs/GifSelect.tsx:264 926 926 msgid "An error has occurred" 927 927 msgstr "" 928 928 ··· 1170 1170 msgid "Are you sure you want to remove this from your feeds?" 1171 1171 msgstr "" 1172 1172 1173 - #: src/view/com/composer/Composer.tsx:741 1173 + #: src/view/com/composer/Composer.tsx:737 1174 1174 msgid "Are you sure you'd like to discard this draft?" 1175 1175 msgstr "" 1176 1176 1177 - #: src/view/com/composer/Composer.tsx:932 1177 + #: src/view/com/composer/Composer.tsx:927 1178 1178 msgid "Are you sure you'd like to discard this post?" 1179 1179 msgstr "" 1180 1180 ··· 1219 1219 msgid "Available" 1220 1220 msgstr "" 1221 1221 1222 - #: src/components/dms/MessagesListHeader.tsx:84 1223 1222 #: src/components/moderation/LabelsOnMeDialog.tsx:315 1224 1223 #: src/components/moderation/LabelsOnMeDialog.tsx:316 1225 1224 #: src/screens/Login/ChooseAccountForm.tsx:90 ··· 1343 1342 msgid "Block User" 1344 1343 msgstr "" 1345 1344 1346 - #: src/components/Post/Embed/index.tsx:180 1345 + #: src/components/Post/Embed/index.tsx:186 1347 1346 msgid "Blocked" 1348 1347 msgstr "" 1349 1348 ··· 1567 1566 #: src/screens/Settings/Settings.tsx:289 1568 1567 #: src/screens/Takendown.tsx:99 1569 1568 #: src/screens/Takendown.tsx:102 1570 - #: src/view/com/composer/Composer.tsx:987 1571 - #: src/view/com/composer/Composer.tsx:998 1569 + #: src/view/com/composer/Composer.tsx:982 1570 + #: src/view/com/composer/Composer.tsx:993 1572 1571 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 1573 1572 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 1574 1573 #: src/view/com/modals/ChangePassword.tsx:279 ··· 1712 1711 msgid "Chat requests" 1713 1712 msgstr "" 1714 1713 1715 - #: src/components/dms/ConvoMenu.tsx:75 1714 + #: src/components/dms/ConvoMenu.tsx:76 1716 1715 #: src/Navigation.tsx:553 1717 1716 #: src/screens/Messages/ChatList.tsx:367 1718 1717 msgid "Chat settings" ··· 1848 1847 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:184 1849 1848 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:237 1850 1849 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:243 1851 - #: src/components/dialogs/GifSelect.tsx:282 1850 + #: src/components/dialogs/GifSelect.tsx:280 1852 1851 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:158 1853 1852 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:167 1854 1853 #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:178 ··· 1891 1890 1892 1891 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:224 1893 1892 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:230 1894 - #: src/components/dialogs/GifSelect.tsx:276 1893 + #: src/components/dialogs/GifSelect.tsx:274 1895 1894 #: src/components/verification/VerificationsDialog.tsx:136 1896 1895 #: src/components/verification/VerifierDialog.tsx:136 1897 1896 msgid "Close dialog" ··· 1932 1931 msgid "Closes password update alert" 1933 1932 msgstr "" 1934 1933 1935 - #: src/view/com/composer/Composer.tsx:995 1934 + #: src/view/com/composer/Composer.tsx:990 1936 1935 msgid "Closes post composer and discards post draft" 1937 1936 msgstr "" 1938 1937 ··· 1990 1989 msgid "Compose new post" 1991 1990 msgstr "" 1992 1991 1993 - #: src/view/com/composer/Composer.tsx:896 1992 + #: src/view/com/composer/Composer.tsx:891 1994 1993 msgid "Compose posts up to {0, plural, other {# characters}} in length" 1995 1994 msgstr "" 1996 1995 ··· 1998 1997 msgid "Compose reply" 1999 1998 msgstr "" 2000 1999 2001 - #: src/view/com/composer/Composer.tsx:1820 2000 + #: src/view/com/composer/Composer.tsx:1815 2002 2001 msgid "Compressing video..." 2003 2002 msgstr "" 2004 2003 ··· 2528 2527 2529 2528 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:678 2530 2529 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:680 2531 - #: src/view/com/composer/Composer.tsx:906 2530 + #: src/view/com/composer/Composer.tsx:901 2532 2531 msgid "Delete post" 2533 2532 msgstr "" 2534 2533 ··· 2549 2548 msgid "Delete this post?" 2550 2549 msgstr "" 2551 2550 2552 - #: src/components/Post/Embed/index.tsx:173 2551 + #: src/components/Post/Embed/index.tsx:179 2553 2552 msgid "Deleted" 2554 2553 msgstr "" 2555 2554 2556 - #: src/components/dms/MessagesListHeader.tsx:160 2555 + #: src/components/dms/MessagesListHeader.tsx:121 2557 2556 #: src/screens/Messages/components/ChatListItem.tsx:128 2558 2557 msgid "Deleted Account" 2559 2558 msgstr "" ··· 2644 2643 msgstr "" 2645 2644 2646 2645 #: src/screens/Profile/Header/EditProfileDialog.tsx:89 2647 - #: src/view/com/composer/Composer.tsx:743 2648 - #: src/view/com/composer/Composer.tsx:939 2646 + #: src/view/com/composer/Composer.tsx:739 2647 + #: src/view/com/composer/Composer.tsx:934 2649 2648 msgid "Discard" 2650 2649 msgstr "" 2651 2650 ··· 2653 2652 msgid "Discard changes?" 2654 2653 msgstr "" 2655 2654 2656 - #: src/view/com/composer/Composer.tsx:740 2655 + #: src/view/com/composer/Composer.tsx:736 2657 2656 msgid "Discard draft?" 2658 2657 msgstr "" 2659 2658 2660 - #: src/view/com/composer/Composer.tsx:931 2659 + #: src/view/com/composer/Composer.tsx:926 2661 2660 msgid "Discard post?" 2662 2661 msgstr "" 2663 2662 ··· 2683 2682 msgid "Dismiss" 2684 2683 msgstr "" 2685 2684 2686 - #: src/view/com/composer/Composer.tsx:1744 2685 + #: src/view/com/composer/Composer.tsx:1739 2687 2686 msgid "Dismiss error" 2688 2687 msgstr "" 2689 2688 ··· 3129 3128 msgid "Entertainment" 3130 3129 msgstr "" 3131 3130 3132 - #: src/view/com/composer/Composer.tsx:1829 3131 + #: src/view/com/composer/Composer.tsx:1824 3133 3132 #: src/view/com/util/error/ErrorScreen.tsx:42 3134 3133 msgid "Error" 3135 3134 msgstr "" ··· 3372 3371 msgid "Failed to load feeds preferences" 3373 3372 msgstr "" 3374 3373 3375 - #: src/components/dialogs/GifSelect.tsx:226 3374 + #: src/components/dialogs/GifSelect.tsx:224 3376 3375 msgid "Failed to load GIFs" 3377 3376 msgstr "" 3378 3377 ··· 3899 3898 msgid "Getting started" 3900 3899 msgstr "" 3901 3900 3902 - #: src/components/MediaPreview.tsx:116 3901 + #: src/components/MediaPreview.tsx:114 3903 3902 msgid "GIF" 3904 3903 msgstr "" 3905 3904 ··· 4464 4463 msgid "It's just you right now! Add more people to your starter pack by searching above." 4465 4464 msgstr "" 4466 4465 4467 - #: src/view/com/composer/Composer.tsx:1763 4466 + #: src/view/com/composer/Composer.tsx:1758 4468 4467 msgid "Job ID: {0}" 4469 4468 msgstr "" 4470 4469 ··· 5441 5440 msgid "News" 5442 5441 msgstr "" 5443 5442 5444 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:42 5445 - msgctxt "Name of app icon variant" 5446 - msgid "Next" 5447 - msgstr "" 5448 - 5449 5443 #: src/screens/Login/ForgotPasswordForm.tsx:137 5450 5444 #: src/screens/Login/ForgotPasswordForm.tsx:143 5451 5445 #: src/screens/Login/LoginForm.tsx:343 ··· 5464 5458 msgid "Next" 5465 5459 msgstr "" 5466 5460 5461 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:42 5462 + msgctxt "Name of app icon variant" 5463 + msgid "Next" 5464 + msgstr "" 5465 + 5467 5466 #: src/view/com/lightbox/Lightbox.web.tsx:170 5468 5467 msgid "Next image" 5469 5468 msgstr "" ··· 5481 5480 msgid "No expiry set" 5482 5481 msgstr "" 5483 5482 5484 - #: src/components/dialogs/GifSelect.tsx:232 5483 + #: src/components/dialogs/GifSelect.tsx:230 5485 5484 msgid "No featured GIFs found. There may be an issue with Tenor." 5486 5485 msgstr "" 5487 5486 ··· 5571 5570 msgid "No results." 5572 5571 msgstr "" 5573 5572 5574 - #: src/components/dialogs/GifSelect.tsx:230 5573 + #: src/components/dialogs/GifSelect.tsx:228 5575 5574 msgid "No search results found for \"{search}\"." 5576 5575 msgstr "" 5577 5576 ··· 5698 5697 msgid "Off" 5699 5698 msgstr "" 5700 5699 5701 - #: src/components/dialogs/GifSelect.tsx:269 5700 + #: src/components/dialogs/GifSelect.tsx:267 5702 5701 #: src/view/com/util/ErrorBoundary.tsx:57 5703 5702 msgid "Oh no!" 5704 5703 msgstr "" ··· 5740 5739 msgid "Onboarding reset" 5741 5740 msgstr "" 5742 5741 5743 - #: src/view/com/composer/Composer.tsx:355 5742 + #: src/view/com/composer/Composer.tsx:354 5744 5743 msgid "One or more GIFs is missing alt text." 5745 5744 msgstr "" 5746 5745 5747 - #: src/view/com/composer/Composer.tsx:352 5746 + #: src/view/com/composer/Composer.tsx:351 5748 5747 msgid "One or more images is missing alt text." 5749 5748 msgstr "" 5750 5749 ··· 5756 5755 msgid "One or more of your selected files is too large. Maximum size is 100 MB." 5757 5756 msgstr "" 5758 5757 5759 - #: src/view/com/composer/Composer.tsx:362 5758 + #: src/view/com/composer/Composer.tsx:361 5760 5759 msgid "One or more videos is missing alt text." 5761 5760 msgstr "" 5762 5761 ··· 5813 5812 msgstr "" 5814 5813 5815 5814 #: src/screens/Messages/components/MessageInput.web.tsx:181 5816 - #: src/view/com/composer/Composer.tsx:1411 5815 + #: src/view/com/composer/Composer.tsx:1406 5817 5816 msgid "Open emoji picker" 5818 5817 msgstr "" 5819 5818 ··· 5888 5887 msgid "Opens a dialog to choose who can reply to this thread" 5889 5888 msgstr "" 5890 5889 5891 - #: src/view/screens/Log.tsx:59 5890 + #: src/screens/Log.tsx:83 5892 5891 msgid "Opens additional details for a debug entry" 5893 5892 msgstr "" 5894 5893 ··· 5922 5921 msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video." 5923 5922 msgstr "" 5924 5923 5925 - #: src/view/com/composer/Composer.tsx:1412 5924 + #: src/view/com/composer/Composer.tsx:1407 5926 5925 msgid "Opens emoji picker" 5927 5926 msgstr "" 5928 5927 ··· 6338 6337 msgid "Post" 6339 6338 msgstr "" 6340 6339 6341 - #: src/view/com/composer/Composer.tsx:1058 6340 + #: src/view/com/composer/Composer.tsx:1053 6342 6341 msgctxt "action" 6343 6342 msgid "Post" 6344 6343 msgstr "" 6345 6344 6346 - #: src/view/com/composer/Composer.tsx:1056 6345 + #: src/view/com/composer/Composer.tsx:1051 6347 6346 msgctxt "action" 6348 6347 msgid "Post All" 6349 6348 msgstr "" ··· 6518 6517 msgid "Privacy Policy" 6519 6518 msgstr "" 6520 6519 6521 - #: src/view/com/composer/Composer.tsx:1826 6520 + #: src/view/com/composer/Composer.tsx:1821 6522 6521 msgid "Processing video..." 6523 6522 msgstr "" 6524 6523 ··· 6557 6556 msgstr "" 6558 6557 6559 6558 #. Accessibility label for button to publish a single post 6560 - #: src/view/com/composer/Composer.tsx:1038 6559 + #: src/view/com/composer/Composer.tsx:1033 6561 6560 msgid "Publish post" 6562 6561 msgstr "" 6563 6562 6564 6563 #. Accessibility label for button to publish multiple posts in a thread 6565 - #: src/view/com/composer/Composer.tsx:1031 6564 + #: src/view/com/composer/Composer.tsx:1026 6566 6565 msgid "Publish posts" 6567 6566 msgstr "" 6568 6567 6569 6568 #. Accessibility label for button to publish multiple replies in a thread 6570 - #: src/view/com/composer/Composer.tsx:1016 6569 + #: src/view/com/composer/Composer.tsx:1011 6571 6570 msgid "Publish replies" 6572 6571 msgstr "" 6573 6572 6574 6573 #. Accessibility label for button to publish a single reply 6575 - #: src/view/com/composer/Composer.tsx:1023 6574 + #: src/view/com/composer/Composer.tsx:1018 6576 6575 msgid "Publish reply" 6577 6576 msgstr "" 6578 6577 ··· 6870 6869 msgid "Remove your verification for this account?" 6871 6870 msgstr "" 6872 6871 6873 - #: src/components/Post/Embed/index.tsx:208 6872 + #: src/components/Post/Embed/index.tsx:214 6874 6873 msgid "Removed by author" 6875 6874 msgstr "" 6876 6875 6877 - #: src/components/Post/Embed/index.tsx:206 6876 + #: src/components/Post/Embed/index.tsx:212 6878 6877 msgid "Removed by you" 6879 6878 msgstr "" 6880 6879 ··· 6940 6939 msgid "Replies to this post are disabled." 6941 6940 msgstr "" 6942 6941 6943 - #: src/view/com/composer/Composer.tsx:1054 6942 + #: src/view/com/composer/Composer.tsx:1049 6944 6943 msgctxt "action" 6945 6944 msgid "Reply" 6946 6945 msgstr "" ··· 7522 7521 msgid "Select GIF" 7523 7522 msgstr "" 7524 7523 7525 - #: src/components/dialogs/GifSelect.tsx:307 7524 + #: src/components/dialogs/GifSelect.tsx:305 7526 7525 msgid "Select GIF \"{0}\"" 7527 7526 msgstr "" 7528 7527 ··· 8358 8357 msgid "System" 8359 8358 msgstr "" 8360 8359 8360 + #: src/screens/Log.tsx:58 8361 8361 #: src/screens/Settings/AboutSettings.tsx:107 8362 8362 #: src/screens/Settings/AboutSettings.tsx:110 8363 8363 #: src/screens/Settings/Settings.tsx:441 ··· 8484 8484 msgid "That starter pack could not be found." 8485 8485 msgstr "" 8486 8486 8487 - #: src/screens/Signup/StepHandle/index.tsx:81 8487 + #: src/screens/Signup/StepHandle/index.tsx:78 8488 8488 msgid "That username is already taken" 8489 8489 msgstr "" 8490 8490 ··· 8606 8606 msgid "There is no time limit for account deactivation, come back any time." 8607 8607 msgstr "" 8608 8608 8609 - #: src/components/dialogs/GifSelect.tsx:227 8609 + #: src/components/dialogs/GifSelect.tsx:225 8610 8610 msgid "There was an issue connecting to Tenor." 8611 8611 msgstr "" 8612 8612 ··· 8691 8691 msgid "There was an issue. Please check your internet connection and try again." 8692 8692 msgstr "" 8693 8693 8694 - #: src/components/dialogs/GifSelect.tsx:271 8694 + #: src/components/dialogs/GifSelect.tsx:269 8695 8695 #: src/view/com/util/ErrorBoundary.tsx:59 8696 8696 msgid "There was an unexpected issue in the application. Please let us know if this happened to you!" 8697 8697 msgstr "" ··· 8869 8869 msgid "This post will be hidden from feeds and threads. This cannot be undone." 8870 8870 msgstr "" 8871 8871 8872 - #: src/view/com/composer/Composer.tsx:471 8872 + #: src/view/com/composer/Composer.tsx:470 8873 8873 msgid "This post's author has disabled quote posts." 8874 8874 msgstr "" 8875 8875 ··· 9289 9289 msgid "Unsubscribed from list" 9290 9290 msgstr "" 9291 9291 9292 - #: src/view/com/composer/Composer.tsx:834 9292 + #: src/view/com/composer/Composer.tsx:829 9293 9293 msgid "Unsupported video type: {mimeType}" 9294 9294 msgstr "" 9295 9295 ··· 9375 9375 msgid "Uploading link thumbnail..." 9376 9376 msgstr "" 9377 9377 9378 - #: src/view/com/composer/Composer.tsx:1823 9378 + #: src/view/com/composer/Composer.tsx:1818 9379 9379 msgid "Uploading video..." 9380 9380 msgstr "" 9381 9381 ··· 9466 9466 msgid "User list updated" 9467 9467 msgstr "" 9468 9468 9469 - #: src/screens/Signup/StepHandle/index.tsx:235 9469 + #: src/screens/Signup/StepHandle/index.tsx:231 9470 9470 msgid "Username cannot be longer than {MAX_SERVICE_HANDLE_LENGTH, plural, other {# characters}}" 9471 9471 msgstr "" 9472 9472 9473 - #: src/screens/Signup/StepHandle/index.tsx:219 9473 + #: src/screens/Signup/StepHandle/index.tsx:215 9474 9474 msgid "Username cannot begin or end with a hyphen" 9475 9475 msgstr "" 9476 9476 9477 - #: src/screens/Signup/StepHandle/index.tsx:223 9477 + #: src/screens/Signup/StepHandle/index.tsx:219 9478 9478 msgid "Username must only contain letters (a-z), numbers, and hyphens" 9479 9479 msgstr "" 9480 9480 ··· 9637 9637 msgid "Video settings" 9638 9638 msgstr "" 9639 9639 9640 - #: src/view/com/composer/Composer.tsx:1833 9640 + #: src/view/com/composer/Composer.tsx:1828 9641 9641 msgid "Video uploaded" 9642 9642 msgstr "" 9643 9643 ··· 9665 9665 msgid "View {0}'s profile" 9666 9666 msgstr "" 9667 9667 9668 - #: src/components/dms/MessagesListHeader.tsx:181 9668 + #: src/components/dms/MessagesListHeader.tsx:142 9669 9669 msgid "View {displayName}'s profile" 9670 9670 msgstr "" 9671 9671 ··· 9677 9677 msgid "View blogpost for more details" 9678 9678 msgstr "" 9679 9679 9680 - #: src/view/screens/Log.tsx:57 9680 + #: src/screens/Log.tsx:81 9681 9681 msgid "View debug entry" 9682 9682 msgstr "" 9683 9683 ··· 9926 9926 msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." 9927 9927 msgstr "" 9928 9928 9929 - #: src/view/com/composer/Composer.tsx:468 9929 + #: src/view/com/composer/Composer.tsx:467 9930 9930 msgid "We're sorry! The post you are replying to has been deleted." 9931 9931 msgstr "" 9932 9932 ··· 9977 9977 9978 9978 #: src/view/com/auth/SplashScreen.tsx:38 9979 9979 #: src/view/com/auth/SplashScreen.web.tsx:99 9980 - #: src/view/com/composer/Composer.tsx:794 9980 + #: src/view/com/composer/Composer.tsx:789 9981 9981 msgid "What's up?" 9982 9982 msgstr "" 9983 9983 ··· 10059 10059 msgid "Write a message" 10060 10060 msgstr "" 10061 10061 10062 - #: src/view/com/composer/Composer.tsx:894 10062 + #: src/view/com/composer/Composer.tsx:889 10063 10063 msgid "Write post" 10064 10064 msgstr "" 10065 10065 10066 - #: src/view/com/composer/Composer.tsx:792 10066 + #: src/view/com/composer/Composer.tsx:787 10067 10067 #: src/view/com/post-thread/PostThreadComposePrompt.tsx:90 10068 10068 msgid "Write your reply" 10069 10069 msgstr "" ··· 10635 10635 msgid "Your password must be at least 8 characters long." 10636 10636 msgstr "" 10637 10637 10638 - #: src/view/com/composer/Composer.tsx:530 10638 + #: src/view/com/composer/Composer.tsx:529 10639 10639 msgid "Your post has been published" 10640 10640 msgstr "" 10641 10641 10642 - #: src/view/com/composer/Composer.tsx:527 10642 + #: src/view/com/composer/Composer.tsx:526 10643 10643 msgid "Your posts have been published" 10644 10644 msgstr "" 10645 10645 ··· 10655 10655 msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." 10656 10656 msgstr "" 10657 10657 10658 - #: src/view/com/composer/Composer.tsx:529 10658 + #: src/view/com/composer/Composer.tsx:528 10659 10659 msgid "Your reply has been published" 10660 10660 msgstr "" 10661 10661
+128
src/screens/Log.tsx
··· 1 + import {useCallback, useState} from 'react' 2 + import {LayoutAnimation, View} from 'react-native' 3 + import {Pressable} from 'react-native' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + import {useFocusEffect} from '@react-navigation/native' 7 + 8 + import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 9 + import { 10 + type CommonNavigatorParams, 11 + type NativeStackScreenProps, 12 + } from '#/lib/routes/types' 13 + import {getEntries} from '#/logger/logDump' 14 + import {useTickEveryMinute} from '#/state/shell' 15 + import {useSetMinimalShellMode} from '#/state/shell' 16 + import {atoms as a, useTheme} from '#/alf' 17 + import { 18 + ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon, 19 + ChevronTop_Stroke2_Corner0_Rounded as ChevronTopIcon, 20 + } from '#/components/icons/Chevron' 21 + import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' 22 + import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 23 + import * as Layout from '#/components/Layout' 24 + import {Text} from '#/components/Typography' 25 + 26 + export function LogScreen({}: NativeStackScreenProps< 27 + CommonNavigatorParams, 28 + 'Log' 29 + >) { 30 + const t = useTheme() 31 + const {_} = useLingui() 32 + const setMinimalShellMode = useSetMinimalShellMode() 33 + const [expanded, setExpanded] = useState<string[]>([]) 34 + const timeAgo = useGetTimeAgo() 35 + const tick = useTickEveryMinute() 36 + 37 + useFocusEffect( 38 + useCallback(() => { 39 + setMinimalShellMode(false) 40 + }, [setMinimalShellMode]), 41 + ) 42 + 43 + const toggler = (id: string) => () => { 44 + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 45 + if (expanded.includes(id)) { 46 + setExpanded(expanded.filter(v => v !== id)) 47 + } else { 48 + setExpanded([...expanded, id]) 49 + } 50 + } 51 + 52 + return ( 53 + <Layout.Screen> 54 + <Layout.Header.Outer> 55 + <Layout.Header.BackButton /> 56 + <Layout.Header.Content> 57 + <Layout.Header.TitleText> 58 + <Trans>System log</Trans> 59 + </Layout.Header.TitleText> 60 + </Layout.Header.Content> 61 + <Layout.Header.Slot /> 62 + </Layout.Header.Outer> 63 + <Layout.Content> 64 + {getEntries() 65 + .slice(0) 66 + .map(entry => { 67 + return ( 68 + <View key={`entry-${entry.id}`}> 69 + <Pressable 70 + style={[ 71 + a.flex_row, 72 + a.align_center, 73 + a.py_md, 74 + a.px_sm, 75 + a.border_b, 76 + t.atoms.border_contrast_low, 77 + t.atoms.bg, 78 + a.gap_sm, 79 + ]} 80 + onPress={toggler(entry.id)} 81 + accessibilityLabel={_(msg`View debug entry`)} 82 + accessibilityHint={_( 83 + msg`Opens additional details for a debug entry`, 84 + )}> 85 + {entry.level === 'warn' || entry.level === 'error' ? ( 86 + <WarningIcon size="sm" fill={t.palette.negative_500} /> 87 + ) : ( 88 + <CircleInfoIcon size="sm" /> 89 + )} 90 + <Text style={[a.flex_1]}>{String(entry.message)}</Text> 91 + {entry.metadata && 92 + Object.keys(entry.metadata).length > 0 && 93 + (expanded.includes(entry.id) ? ( 94 + <ChevronTopIcon 95 + size="sm" 96 + style={[t.atoms.text_contrast_low]} 97 + /> 98 + ) : ( 99 + <ChevronBottomIcon 100 + size="sm" 101 + style={[t.atoms.text_contrast_low]} 102 + /> 103 + ))} 104 + <Text style={[{minWidth: 40}, t.atoms.text_contrast_medium]}> 105 + {timeAgo(entry.timestamp, tick)} 106 + </Text> 107 + </Pressable> 108 + {expanded.includes(entry.id) && ( 109 + <View 110 + style={[ 111 + t.atoms.bg_contrast_25, 112 + a.rounded_xs, 113 + a.p_sm, 114 + a.border_b, 115 + t.atoms.border_contrast_low, 116 + ]}> 117 + <View style={[a.px_sm, a.py_xs]}> 118 + <Text>{JSON.stringify(entry.metadata, null, 2)}</Text> 119 + </View> 120 + </View> 121 + )} 122 + </View> 123 + ) 124 + })} 125 + </Layout.Content> 126 + </Layout.Screen> 127 + ) 128 + }
+1 -5
src/screens/Signup/StepHandle/index.tsx
··· 9 9 import {msg, Plural, Trans} from '@lingui/macro' 10 10 import {useLingui} from '@lingui/react' 11 11 12 - import {useGate} from '#/lib/statsig/statsig' 13 12 import { 14 13 createFullHandle, 15 14 MAX_SERVICE_HANDLE_LENGTH, ··· 28 27 import {At_Stroke2_Corner0_Rounded as AtIcon} from '#/components/icons/At' 29 28 import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 30 29 import {Text} from '#/components/Typography' 31 - import {IS_INTERNAL} from '#/env' 32 30 import {BackNextButtons} from '../BackNextButtons' 33 31 import {HandleSuggestions} from './HandleSuggestions' 34 32 35 33 export function StepHandle() { 36 34 const {_} = useLingui() 37 35 const t = useTheme() 38 - const gate = useGate() 39 36 const {state, dispatch} = useSignupContext() 40 37 const [draftValue, setDraftValue] = useState(state.handle) 41 38 const isNextLoading = useThrottledValue(state.isLoading, 500) ··· 193 190 </RequirementText> 194 191 </Requirement> 195 192 {isHandleAvailable.suggestions && 196 - isHandleAvailable.suggestions.length > 0 && 197 - (gate('handle_suggestions') || IS_INTERNAL) && ( 193 + isHandleAvailable.suggestions.length > 0 && ( 198 194 <HandleSuggestions 199 195 suggestions={isHandleAvailable.suggestions} 200 196 onSelect={suggestion => {
+9 -8
src/screens/StarterPack/Wizard/State.tsx
··· 7 7 import {msg, plural} from '@lingui/macro' 8 8 9 9 import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' 10 - import {useSession} from '#/state/session' 11 10 import * as Toast from '#/view/com/util/Toast' 12 11 import * as bsky from '#/types/bsky' 13 12 ··· 37 36 processing: boolean 38 37 error?: string 39 38 transitionDirection: 'Backward' | 'Forward' 39 + targetDid?: string 40 40 } 41 41 42 42 type TStateContext = [State, (action: Action) => void] ··· 118 118 export function Provider({ 119 119 starterPack, 120 120 listItems, 121 + targetProfile, 121 122 children, 122 123 }: { 123 124 starterPack?: AppBskyGraphDefs.StarterPackView 124 125 listItems?: AppBskyGraphDefs.ListItemView[] 126 + targetProfile: bsky.profile.AnyProfileView 125 127 children: React.ReactNode 126 128 }) { 127 - const {currentAccount} = useSession() 128 - 129 129 const createInitialState = (): State => { 130 + const targetDid = targetProfile?.did 131 + 130 132 if ( 131 133 starterPack && 132 134 bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord) ··· 136 138 currentStep: 'Details', 137 139 name: starterPack.record.name, 138 140 description: starterPack.record.description, 139 - profiles: 140 - listItems 141 - ?.map(i => i.subject) 142 - .filter(p => p.did !== currentAccount?.did) ?? [], 141 + profiles: listItems?.map(i => i.subject) ?? [], 143 142 feeds: starterPack.feeds ?? [], 144 143 processing: false, 145 144 transitionDirection: 'Forward', 145 + targetDid, 146 146 } 147 147 } 148 148 149 149 return { 150 150 canNext: true, 151 151 currentStep: 'Details', 152 - profiles: [], 152 + profiles: [targetProfile], 153 153 feeds: [], 154 154 processing: false, 155 155 transitionDirection: 'Forward', 156 + targetDid, 156 157 } 157 158 } 158 159
+72 -36
src/screens/StarterPack/Wizard/index.tsx
··· 68 68 CommonNavigatorParams, 69 69 'StarterPackEdit' | 'StarterPackWizard' 70 70 >) { 71 - const {rkey} = route.params ?? {} 71 + const params = route.params ?? {} 72 + const rkey = 'rkey' in params ? params.rkey : undefined 73 + const fromDialog = 'fromDialog' in params ? params.fromDialog : false 74 + const targetDid = 'targetDid' in params ? params.targetDid : undefined 75 + const onSuccess = 'onSuccess' in params ? params.onSuccess : undefined 72 76 const {currentAccount} = useSession() 73 77 const moderationOpts = useModerationOpts() 74 78 75 79 const {_} = useLingui() 80 + 81 + // Use targetDid if provided (from dialog), otherwise use current account 82 + const profileDid = targetDid || currentAccount!.did 76 83 77 84 const { 78 85 data: starterPack, ··· 91 98 data: profile, 92 99 isLoading: isLoadingProfile, 93 100 isError: isErrorProfile, 94 - } = useProfileQuery({did: currentAccount?.did}) 101 + } = useProfileQuery({did: profileDid}) 95 102 96 103 const isEdit = Boolean(rkey) 97 104 const isReady = ··· 127 134 <Layout.Screen 128 135 testID="starterPackWizardScreen" 129 136 style={web([{minHeight: 0}, a.flex_1])}> 130 - <Provider starterPack={starterPack} listItems={listItems}> 137 + <Provider 138 + starterPack={starterPack} 139 + listItems={listItems} 140 + targetProfile={profile}> 131 141 <WizardInner 132 142 currentStarterPack={starterPack} 133 143 currentListItems={listItems} 134 144 profile={profile} 135 145 moderationOpts={moderationOpts} 146 + fromDialog={fromDialog} 147 + onSuccess={onSuccess} 136 148 /> 137 149 </Provider> 138 150 </Layout.Screen> ··· 144 156 currentListItems, 145 157 profile, 146 158 moderationOpts, 159 + fromDialog, 160 + onSuccess, 147 161 }: { 148 162 currentStarterPack?: AppBskyGraphDefs.StarterPackView 149 163 currentListItems?: AppBskyGraphDefs.ListItemView[] 150 164 profile: AppBskyActorDefs.ProfileViewDetailed 151 165 moderationOpts: ModerationOpts 166 + fromDialog?: boolean 167 + onSuccess?: () => void 152 168 }) { 153 169 const navigation = useNavigation<NavigationProp>() 154 170 const {_} = useLingui() 155 171 const setMinimalShellMode = useSetMinimalShellMode() 156 172 const [state, dispatch] = useWizardState() 157 173 const {currentAccount} = useSession() 174 + 158 175 const {data: currentProfile} = useProfileQuery({ 159 176 did: currentAccount?.did, 160 177 staleTime: 0, ··· 213 230 }) 214 231 Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)]) 215 232 dispatch({type: 'SetProcessing', processing: false}) 216 - navigation.replace('StarterPack', { 217 - name: currentAccount!.handle, 218 - rkey, 219 - new: true, 220 - }) 233 + 234 + if (fromDialog) { 235 + navigation.goBack() 236 + onSuccess?.() 237 + } else { 238 + navigation.replace('StarterPack', { 239 + name: profile!.handle, 240 + rkey, 241 + new: true, 242 + }) 243 + } 221 244 } 222 245 223 246 const onSuccessEdit = () => { ··· 285 308 ) 286 309 } 287 310 288 - const items = 289 - state.currentStep === 'Profiles' 290 - ? [profile, ...state.profiles] 291 - : state.feeds 311 + const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds 292 312 293 313 const isEditEnabled = 294 314 (state.currentStep === 'Profiles' && items.length > 1) || ··· 340 360 </Container> 341 361 342 362 {state.currentStep !== 'Details' && ( 343 - <Footer 344 - onNext={onNext} 345 - nextBtnText={currUiStrings.nextBtn} 346 - profile={profile} 347 - /> 363 + <Footer onNext={onNext} nextBtnText={currUiStrings.nextBtn} /> 348 364 )} 349 365 <WizardEditListDialog 350 366 control={editDialogControl} ··· 392 408 function Footer({ 393 409 onNext, 394 410 nextBtnText, 395 - profile, 396 411 }: { 397 412 onNext: () => void 398 413 nextBtnText: string 399 - profile: AppBskyActorDefs.ProfileViewDetailed 400 414 }) { 401 415 const t = useTheme() 402 416 const [state] = useWizardState() 403 417 const {bottom: bottomInset} = useSafeAreaInsets() 404 - 405 - const items = 406 - state.currentStep === 'Profiles' 407 - ? [profile, ...state.profiles] 408 - : state.feeds 418 + const {currentAccount} = useSession() 419 + const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds 409 420 410 421 const minimumItems = state.currentStep === 'Profiles' ? 8 : 0 411 422 ··· 471 482 <Text style={[a.text_center, textStyles]}> 472 483 { 473 484 items.length < 2 ? ( 474 - <Trans> 475 - It's just you right now! Add more people to your starter pack 476 - by searching above. 477 - </Trans> 485 + currentAccount?.did === items[0].did ? ( 486 + <Trans> 487 + It's just you right now! Add more people to your starter 488 + pack by searching above. 489 + </Trans> 490 + ) : ( 491 + <Trans> 492 + It's just{' '} 493 + <Text style={[a.font_bold, textStyles]} emoji> 494 + {getName(items[0])}{' '} 495 + </Text> 496 + right now! Add more people to your starter pack by searching 497 + above. 498 + </Trans> 499 + ) 478 500 ) : items.length === 2 ? ( 479 - <Trans> 480 - <Text style={[a.font_bold, textStyles]}>You</Text> and 481 - <Text> </Text> 482 - <Text style={[a.font_bold, textStyles]} emoji> 483 - {getName(items[1] /* [0] is self, skip it */)}{' '} 484 - </Text> 485 - are included in your starter pack 486 - </Trans> 501 + currentAccount?.did === items[0].did ? ( 502 + <Trans> 503 + <Text style={[a.font_bold, textStyles]}>You</Text> and 504 + <Text> </Text> 505 + <Text style={[a.font_bold, textStyles]} emoji> 506 + {getName(items[1] /* [0] is self, skip it */)}{' '} 507 + </Text> 508 + are included in your starter pack 509 + </Trans> 510 + ) : ( 511 + <Trans> 512 + <Text style={[a.font_bold, textStyles]}> 513 + {getName(items[0])} 514 + </Text>{' '} 515 + and 516 + <Text> </Text> 517 + <Text style={[a.font_bold, textStyles]} emoji> 518 + {getName(items[1] /* [0] is self, skip it */)}{' '} 519 + </Text> 520 + are included in your starter pack 521 + </Trans> 522 + ) 487 523 ) : items.length > 2 ? ( 488 524 <Trans context="profiles"> 489 525 <Text style={[a.font_bold, textStyles]} emoji>
+53 -4
src/state/queries/actor-starter-packs.ts
··· 1 - import {AppBskyGraphGetActorStarterPacks} from '@atproto/api' 2 1 import { 3 - InfiniteData, 4 - QueryClient, 5 - QueryKey, 2 + type AppBskyGraphGetActorStarterPacks, 3 + type AppBskyGraphGetStarterPacksWithMembership, 4 + } from '@atproto/api' 5 + import { 6 + type InfiniteData, 7 + type QueryClient, 8 + type QueryKey, 6 9 useInfiniteQuery, 7 10 } from '@tanstack/react-query' 8 11 9 12 import {useAgent} from '#/state/session' 10 13 11 14 export const RQKEY_ROOT = 'actor-starter-packs' 15 + export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership' 12 16 export const RQKEY = (did?: string) => [RQKEY_ROOT, did] 17 + export const RQKEY_WITH_MEMBERSHIP = (did?: string) => [ 18 + RQKEY_WITH_MEMBERSHIP_ROOT, 19 + did, 20 + ] 13 21 14 22 export function useActorStarterPacksQuery({ 15 23 did, ··· 42 50 }) 43 51 } 44 52 53 + export function useActorStarterPacksWithMembershipsQuery({ 54 + did, 55 + enabled = true, 56 + }: { 57 + did?: string 58 + enabled?: boolean 59 + }) { 60 + const agent = useAgent() 61 + 62 + return useInfiniteQuery< 63 + AppBskyGraphGetStarterPacksWithMembership.OutputSchema, 64 + Error, 65 + InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>, 66 + QueryKey, 67 + string | undefined 68 + >({ 69 + queryKey: RQKEY_WITH_MEMBERSHIP(did), 70 + queryFn: async ({pageParam}: {pageParam?: string}) => { 71 + const res = await agent.app.bsky.graph.getStarterPacksWithMembership({ 72 + actor: did!, 73 + limit: 10, 74 + cursor: pageParam, 75 + }) 76 + return res.data 77 + }, 78 + enabled: Boolean(did) && enabled, 79 + initialPageParam: undefined, 80 + getNextPageParam: lastPage => lastPage.cursor, 81 + }) 82 + } 83 + 45 84 export async function invalidateActorStarterPacksQuery({ 46 85 queryClient, 47 86 did, ··· 51 90 }) { 52 91 await queryClient.invalidateQueries({queryKey: RQKEY(did)}) 53 92 } 93 + 94 + export async function invalidateActorStarterPacksWithMembershipQuery({ 95 + queryClient, 96 + did, 97 + }: { 98 + queryClient: QueryClient 99 + did: string 100 + }) { 101 + await queryClient.invalidateQueries({queryKey: RQKEY_WITH_MEMBERSHIP(did)}) 102 + }
+76 -81
src/view/com/composer/Composer.tsx
··· 40 40 ZoomIn, 41 41 ZoomOut, 42 42 } from 'react-native-reanimated' 43 - import {RootSiblingParent} from 'react-native-root-siblings' 44 43 import {useSafeAreaInsets} from 'react-native-safe-area-context' 45 44 import {type ImagePickerAsset} from 'expo-image-picker' 46 45 import { ··· 663 662 const isWebFooterSticky = !isNative && thread.posts.length > 1 664 663 return ( 665 664 <BottomSheetPortalProvider> 666 - <RootSiblingParent> 667 - <KeyboardAvoidingView 668 - testID="composePostView" 669 - behavior={isIOS ? 'padding' : 'height'} 670 - keyboardVerticalOffset={keyboardVerticalOffset} 671 - style={a.flex_1}> 672 - <View 673 - style={[a.flex_1, viewStyles]} 674 - aria-modal 675 - accessibilityViewIsModal> 676 - <RootSiblingParent> 677 - <ComposerTopBar 678 - canPost={canPost} 679 - isReply={!!replyTo} 680 - isPublishQueued={publishOnUpload} 681 - isPublishing={isPublishing} 682 - isThread={thread.posts.length > 1} 683 - publishingStage={publishingStage} 684 - topBarAnimatedStyle={topBarAnimatedStyle} 685 - onCancel={onPressCancel} 686 - onPublish={onPressPublish}> 687 - {missingAltError && <AltTextReminder error={missingAltError} />} 688 - <ErrorBanner 689 - error={error} 690 - videoState={erroredVideo} 691 - clearError={() => setError('')} 692 - clearVideo={ 693 - erroredVideoPostId 694 - ? () => clearVideo(erroredVideoPostId) 695 - : () => {} 696 - } 697 - /> 698 - </ComposerTopBar> 665 + <KeyboardAvoidingView 666 + testID="composePostView" 667 + behavior={isIOS ? 'padding' : 'height'} 668 + keyboardVerticalOffset={keyboardVerticalOffset} 669 + style={a.flex_1}> 670 + <View 671 + style={[a.flex_1, viewStyles]} 672 + aria-modal 673 + accessibilityViewIsModal> 674 + <ComposerTopBar 675 + canPost={canPost} 676 + isReply={!!replyTo} 677 + isPublishQueued={publishOnUpload} 678 + isPublishing={isPublishing} 679 + isThread={thread.posts.length > 1} 680 + publishingStage={publishingStage} 681 + topBarAnimatedStyle={topBarAnimatedStyle} 682 + onCancel={onPressCancel} 683 + onPublish={onPressPublish}> 684 + {missingAltError && <AltTextReminder error={missingAltError} />} 685 + <ErrorBanner 686 + error={error} 687 + videoState={erroredVideo} 688 + clearError={() => setError('')} 689 + clearVideo={ 690 + erroredVideoPostId 691 + ? () => clearVideo(erroredVideoPostId) 692 + : () => {} 693 + } 694 + /> 695 + </ComposerTopBar> 699 696 700 - <Animated.ScrollView 701 - ref={scrollViewRef} 702 - layout={native(LinearTransition)} 703 - onScroll={scrollHandler} 704 - contentContainerStyle={a.flex_grow} 705 - style={a.flex_1} 706 - keyboardShouldPersistTaps="always" 707 - onContentSizeChange={onScrollViewContentSizeChange} 708 - onLayout={onScrollViewLayout}> 709 - {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined} 710 - {thread.posts.map((post, index) => ( 711 - <React.Fragment key={post.id}> 712 - <ComposerPost 713 - post={post} 714 - dispatch={composerDispatch} 715 - textInput={post.id === activePost.id ? textInput : null} 716 - isFirstPost={index === 0} 717 - isLastPost={index === thread.posts.length - 1} 718 - isPartOfThread={thread.posts.length > 1} 719 - isReply={index > 0 || !!replyTo} 720 - isActive={post.id === activePost.id} 721 - canRemovePost={thread.posts.length > 1} 722 - canRemoveQuote={index > 0 || !initQuote} 723 - onSelectVideo={selectVideo} 724 - onClearVideo={clearVideo} 725 - onPublish={onComposerPostPublish} 726 - onError={setError} 727 - /> 728 - {isWebFooterSticky && post.id === activePost.id && ( 729 - <View style={styles.stickyFooterWeb}>{footer}</View> 730 - )} 731 - </React.Fragment> 732 - ))} 733 - </Animated.ScrollView> 734 - {!isWebFooterSticky && footer} 735 - </RootSiblingParent> 736 - </View> 697 + <Animated.ScrollView 698 + ref={scrollViewRef} 699 + layout={native(LinearTransition)} 700 + onScroll={scrollHandler} 701 + contentContainerStyle={a.flex_grow} 702 + style={a.flex_1} 703 + keyboardShouldPersistTaps="always" 704 + onContentSizeChange={onScrollViewContentSizeChange} 705 + onLayout={onScrollViewLayout}> 706 + {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined} 707 + {thread.posts.map((post, index) => ( 708 + <React.Fragment key={post.id}> 709 + <ComposerPost 710 + post={post} 711 + dispatch={composerDispatch} 712 + textInput={post.id === activePost.id ? textInput : null} 713 + isFirstPost={index === 0} 714 + isLastPost={index === thread.posts.length - 1} 715 + isPartOfThread={thread.posts.length > 1} 716 + isReply={index > 0 || !!replyTo} 717 + isActive={post.id === activePost.id} 718 + canRemovePost={thread.posts.length > 1} 719 + canRemoveQuote={index > 0 || !initQuote} 720 + onSelectVideo={selectVideo} 721 + onClearVideo={clearVideo} 722 + onPublish={onComposerPostPublish} 723 + onError={setError} 724 + /> 725 + {isWebFooterSticky && post.id === activePost.id && ( 726 + <View style={styles.stickyFooterWeb}>{footer}</View> 727 + )} 728 + </React.Fragment> 729 + ))} 730 + </Animated.ScrollView> 731 + {!isWebFooterSticky && footer} 732 + </View> 737 733 738 - <Prompt.Basic 739 - control={discardPromptControl} 740 - title={_(msg`Discard draft?`)} 741 - description={_(msg`Are you sure you'd like to discard this draft?`)} 742 - onConfirm={onClose} 743 - confirmButtonCta={_(msg`Discard`)} 744 - confirmButtonColor="negative" 745 - /> 746 - </KeyboardAvoidingView> 747 - </RootSiblingParent> 734 + <Prompt.Basic 735 + control={discardPromptControl} 736 + title={_(msg`Discard draft?`)} 737 + description={_(msg`Are you sure you'd like to discard this draft?`)} 738 + onConfirm={onClose} 739 + confirmButtonCta={_(msg`Discard`)} 740 + confirmButtonColor="negative" 741 + /> 742 + </KeyboardAvoidingView> 748 743 </BottomSheetPortalProvider> 749 744 ) 750 745 }
+17
src/view/com/profile/ProfileMenu.tsx
··· 32 32 import * as Toast from '#/view/com/util/Toast' 33 33 import {Button, ButtonIcon} from '#/components/Button' 34 34 import {useDialogControl} from '#/components/Dialog' 35 + import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog' 35 36 import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' 36 37 import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' 37 38 import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck' ··· 50 51 } from '#/components/icons/Person' 51 52 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 52 53 import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 54 + import {StarterPack} from '#/components/icons/StarterPack' 53 55 import {EditLiveDialog} from '#/components/live/EditLiveDialog' 54 56 import {GoLiveDialog} from '#/components/live/GoLiveDialog' 55 57 import * as Menu from '#/components/Menu' ··· 97 99 const blockPromptControl = Prompt.usePromptControl() 98 100 const loggedOutWarningPromptControl = Prompt.usePromptControl() 99 101 const goLiveDialogControl = useDialogControl() 102 + const addToStarterPacksDialogControl = useDialogControl() 100 103 101 104 const showLoggedOutWarning = React.useMemo(() => { 102 105 return ( ··· 310 313 </> 311 314 )} 312 315 <Menu.Item 316 + testID="profileHeaderDropdownStarterPackAddRemoveBtn" 317 + label={_(msg`Add to starter packs`)} 318 + onPress={addToStarterPacksDialogControl.open}> 319 + <Menu.ItemText> 320 + <Trans>Add to starter packs</Trans> 321 + </Menu.ItemText> 322 + <Menu.ItemIcon icon={StarterPack} /> 323 + </Menu.Item> 324 + <Menu.Item 313 325 testID="profileHeaderDropdownListAddRemoveBtn" 314 326 label={_(msg`Add to lists`)} 315 327 onPress={onPressAddRemoveLists}> ··· 473 485 ) : null} 474 486 </Menu.Outer> 475 487 </Menu.Root> 488 + 489 + <StarterPackDialog 490 + control={addToStarterPacksDialogControl} 491 + targetDid={profile.did} 492 + /> 476 493 477 494 <ReportDialog 478 495 control={reportDialogControl}
-116
src/view/screens/Log.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 - import {msg} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 - import {useFocusEffect} from '@react-navigation/native' 7 - 8 - import {usePalette} from '#/lib/hooks/usePalette' 9 - import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 10 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 11 - import {s} from '#/lib/styles' 12 - import {getEntries} from '#/logger/logDump' 13 - import {useTickEveryMinute} from '#/state/shell' 14 - import {useSetMinimalShellMode} from '#/state/shell' 15 - import {Text} from '#/view/com/util/text/Text' 16 - import {ViewHeader} from '#/view/com/util/ViewHeader' 17 - import {ScrollView} from '#/view/com/util/Views' 18 - import * as Layout from '#/components/Layout' 19 - 20 - export function LogScreen({}: NativeStackScreenProps< 21 - CommonNavigatorParams, 22 - 'Log' 23 - >) { 24 - const pal = usePalette('default') 25 - const {_} = useLingui() 26 - const setMinimalShellMode = useSetMinimalShellMode() 27 - const [expanded, setExpanded] = React.useState<string[]>([]) 28 - const timeAgo = useGetTimeAgo() 29 - const tick = useTickEveryMinute() 30 - 31 - useFocusEffect( 32 - React.useCallback(() => { 33 - setMinimalShellMode(false) 34 - }, [setMinimalShellMode]), 35 - ) 36 - 37 - const toggler = (id: string) => () => { 38 - if (expanded.includes(id)) { 39 - setExpanded(expanded.filter(v => v !== id)) 40 - } else { 41 - setExpanded([...expanded, id]) 42 - } 43 - } 44 - 45 - return ( 46 - <Layout.Screen> 47 - <ViewHeader title="Log" /> 48 - <ScrollView style={s.flex1}> 49 - {getEntries() 50 - .slice(0) 51 - .map(entry => { 52 - return ( 53 - <View key={`entry-${entry.id}`}> 54 - <TouchableOpacity 55 - style={[styles.entry, pal.border, pal.view]} 56 - onPress={toggler(entry.id)} 57 - accessibilityLabel={_(msg`View debug entry`)} 58 - accessibilityHint={_( 59 - msg`Opens additional details for a debug entry`, 60 - )}> 61 - {entry.level === 'debug' ? ( 62 - <FontAwesomeIcon icon="info" /> 63 - ) : ( 64 - <FontAwesomeIcon icon="exclamation" style={s.red3} /> 65 - )} 66 - <Text type="sm" style={[styles.summary, pal.text]}> 67 - {String(entry.message)} 68 - </Text> 69 - {entry.metadata && Object.keys(entry.metadata).length ? ( 70 - <FontAwesomeIcon 71 - icon={ 72 - expanded.includes(entry.id) ? 'angle-up' : 'angle-down' 73 - } 74 - style={s.mr5} 75 - /> 76 - ) : undefined} 77 - <Text type="sm" style={[styles.ts, pal.textLight]}> 78 - {timeAgo(entry.timestamp, tick)} 79 - </Text> 80 - </TouchableOpacity> 81 - {expanded.includes(entry.id) ? ( 82 - <View style={[pal.view, s.pl10, s.pr10, s.pb10]}> 83 - <View style={[pal.btn, styles.details]}> 84 - <Text type="mono" style={pal.text}> 85 - {JSON.stringify(entry.metadata, null, 2)} 86 - </Text> 87 - </View> 88 - </View> 89 - ) : undefined} 90 - </View> 91 - ) 92 - })} 93 - <View style={s.footerSpacer} /> 94 - </ScrollView> 95 - </Layout.Screen> 96 - ) 97 - } 98 - 99 - const styles = StyleSheet.create({ 100 - entry: { 101 - flexDirection: 'row', 102 - borderTopWidth: 1, 103 - paddingVertical: 10, 104 - paddingHorizontal: 6, 105 - }, 106 - summary: { 107 - flex: 1, 108 - }, 109 - ts: { 110 - width: 40, 111 - }, 112 - details: { 113 - paddingVertical: 10, 114 - paddingHorizontal: 6, 115 - }, 116 - })