Bluesky app fork with some witchin' additions 💫

Merge with bluesky-social/social-app again

3rd attempt to get dumb germ dm button and various upstream refactoring stuff in (I hope for more dm buttons in the future) also removing .github folder again since this is a .tangled household

xan.lol 5efb0d1a 711c6982

verified
+1355 -644
-54
.github/workflows/claude.yml
··· 1 - name: Claude Code 2 - 3 - on: 4 - issue_comment: 5 - types: [created] 6 - pull_request_review_comment: 7 - types: [created] 8 - issues: 9 - types: [opened, assigned] 10 - pull_request_review: 11 - types: [submitted] 12 - 13 - jobs: 14 - claude: 15 - if: | 16 - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 - runs-on: ubuntu-latest 21 - permissions: 22 - contents: read 23 - pull-requests: read 24 - issues: read 25 - id-token: write 26 - actions: read # Required for Claude to read CI results on PRs 27 - steps: 28 - - name: Checkout repository 29 - uses: actions/checkout@v4 30 - with: 31 - fetch-depth: 1 32 - 33 - - name: Run Claude Code 34 - id: claude 35 - uses: anthropics/claude-code-action@v1 36 - with: 37 - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 38 - 39 - # This is an optional setting that allows Claude to read CI results on PRs 40 - additional_permissions: | 41 - actions: read 42 - 43 - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. 44 - # prompt: 'Update the pull request description to include a summary of changes.' 45 - 46 - # Optional: Add claude_args to customize behavior and configuration 47 - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md 48 - # or https://code.claude.com/docs/en/cli-reference for available options 49 - # claude_args: '--allowed-tools Bash(gh pr:*)' 50 - 51 - # NOTE(sfn): we can add a custom system prompt here 52 - 53 - claude_args: | 54 - --model claude-opus-4-5-20251101
+1
assets/icons/person_filled_corner2_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12.233 2a4.433 4.433 0 1 0 0 8.867 4.433 4.433 0 0 0 0-8.867Zm0 10.133c-3.888 0-6.863 2.263-8.071 5.435-.346.906-.11 1.8.44 2.436.535.619 1.36.996 2.25.996h10.762c.89 0 1.716-.377 2.25-.996.55-.636.786-1.53.441-2.436-1.208-3.173-4.184-5.435-8.072-5.435Z"/></svg>
assets/images/germ_logo.webp

This is a binary file and will not be displayed.

+4 -4
bskyweb/templates/base.html
··· 148 148 </head> 149 149 <body> 150 150 {%- block body_all %} 151 + <div id="splash"> 152 + <!-- Witchsky SVG --> 153 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#ED5345" d="M374.473 57.7173C367.666 50.7995 357.119 49.1209 348.441 53.1659C347.173 53.7567 342.223 56.0864 334.796 59.8613C326.32 64.1696 314.568 70.3869 301.394 78.0596C275.444 93.1728 242.399 114.83 218.408 139.477C185.983 172.786 158.719 225.503 140.029 267.661C130.506 289.144 122.878 308.661 117.629 322.81C116.301 326.389 115.124 329.63 114.104 332.478C87.1783 336.42 64.534 341.641 47.5078 348.101C37.6493 351.84 28.3222 356.491 21.0573 362.538C13.8818 368.511 6.00003 378.262 6.00003 391.822C6.00014 403.222 11.8738 411.777 17.4566 417.235C23.0009 422.655 29.9593 426.793 36.871 430.062C50.8097 436.653 69.5275 441.988 90.8362 446.249C133.828 454.846 192.21 460 256.001 460C319.79 460 378.172 454.846 421.164 446.249C442.472 441.988 461.19 436.653 475.129 430.062C482.041 426.793 488.999 422.655 494.543 417.235C500.039 411.862 505.817 403.489 505.996 392.353L506 391.822L505.995 391.188C505.754 377.959 498.012 368.417 490.945 362.534C483.679 356.485 474.35 351.835 464.491 348.095C446.749 341.366 422.906 335.982 394.476 331.987C393.6 330.57 392.633 328.995 391.595 327.273C386.477 318.777 379.633 306.842 372.737 293.115C358.503 264.781 345.757 232.098 344.756 206.636C343.87 184.121 351.638 154.087 360.819 127.789C365.27 115.041 369.795 103.877 373.207 95.9072C374.909 91.9309 376.325 88.7712 377.302 86.6328C377.79 85.5645 378.167 84.7524 378.416 84.2224C378.54 83.9579 378.632 83.7635 378.69 83.643C378.718 83.5829 378.739 83.5411 378.75 83.5181C378.753 83.5108 378.756 83.5049 378.757 83.5015C382.909 74.8634 381.196 64.5488 374.473 57.7173Z"/></svg> 154 + </div> 151 155 <div id="root"> 152 - <div id="splash"> 153 - <!-- Bluesky SVG --> 154 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 320"><path fill="#0085ff" d="M180 142c-16.3-31.7-60.7-90.8-102-120C38.5-5.9 23.4-1 13.5 3.4 2.1 8.6 0 26.2 0 36.5c0 10.4 5.7 84.8 9.4 97.2 12.2 41 55.7 55 95.7 50.5-58.7 8.6-110.8 30-42.4 106.1 75.1 77.9 103-16.7 117.3-64.6 14.3 48 30.8 139 116 64.6 64-64.6 17.6-97.5-41.1-106.1 40 4.4 83.5-9.5 95.7-50.5 3.7-12.4 9.4-86.8 9.4-97.2 0-10.3-2-27.9-13.5-33C336.5-1 321.5-6 282 22c-41.3 29.2-85.7 88.3-102 120Z"/></svg><!-- TODO: Xan: Replace with Witchsky logo --> 155 - </div> 156 156 </div> 157 157 158 158 <noscript>
+2 -2
package.json
··· 73 73 "icons:optimize": "svgo -f ./assets/icons" 74 74 }, 75 75 "dependencies": { 76 - "@atproto/api": "^0.18.20", 76 + "@atproto/api": "^0.18.21", 77 77 "@bitdrift/react-native": "^0.6.8", 78 78 "@braintree/sanitize-url": "^6.0.2", 79 79 "@bsky.app/alf": "^0.1.6", ··· 230 230 "zod": "^3.20.2" 231 231 }, 232 232 "devDependencies": { 233 - "@atproto/dev-env": "^0.3.208", 233 + "@atproto/dev-env": "^0.3.209", 234 234 "@babel/core": "^7.26.0", 235 235 "@babel/preset-env": "^7.26.0", 236 236 "@babel/runtime": "^7.26.0",
+61 -62
src/App.web.tsx
··· 2 2 import '#/view/icons' 3 3 import './style.css' 4 4 5 - import React, {useEffect, useState} from 'react' 5 + import {Fragment, useEffect, useState} from 'react' 6 6 import {SafeAreaProvider} from 'react-native-safe-area-context' 7 7 import {msg} from '@lingui/macro' 8 8 import {useLingui} from '@lingui/react' ··· 81 81 prefetchLiveEvents() 82 82 83 83 function InnerApp() { 84 - const [isReady, setIsReady] = React.useState(false) 84 + const [isReady, setIsReady] = useState(false) 85 85 const {currentAccount} = useSession() 86 86 const {resumeSession} = useSessionApi() 87 87 const theme = useColorModeTheme() ··· 116 116 }) 117 117 }, [_]) 118 118 119 - // wait for session to resume 120 - if (!isReady || !hasCheckedReferrer) return <Splash isReady /> 121 - 122 119 return ( 123 120 <Alf theme={theme}> 124 121 <ThemeProvider theme={theme}> 125 122 <ContextMenuProvider> 126 - <VideoVolumeProvider> 127 - <ActiveVideoProvider> 128 - <React.Fragment 129 - // Resets the entire tree below when it changes: 130 - key={currentAccount?.did}> 131 - <AnalyticsFeaturesContext> 132 - <QueryProvider currentDid={currentAccount?.did}> 133 - <PolicyUpdateOverlayProvider> 134 - <LiveEventsProvider> 135 - <AgeAssuranceV2Provider> 136 - <ComposerProvider> 137 - <MessagesProvider> 138 - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 139 - <LabelDefsProvider> 140 - <ModerationOptsProvider> 141 - <LoggedOutViewProvider> 142 - <SelectedFeedProvider> 143 - <HiddenRepliesProvider> 144 - <HomeBadgeProvider> 145 - <UnreadNotifsProvider> 146 - <BackgroundNotificationPreferencesProvider> 147 - <MutedThreadsProvider> 148 - <SafeAreaProvider> 149 - <ProgressGuideProvider> 150 - <ServiceConfigProvider> 151 - <EmailVerificationProvider> 152 - <HideBottomBarBorderProvider> 153 - <IntentDialogProvider> 154 - <Shell /> 155 - <ToastOutlet /> 156 - </IntentDialogProvider> 157 - </HideBottomBarBorderProvider> 158 - </EmailVerificationProvider> 159 - </ServiceConfigProvider> 160 - </ProgressGuideProvider> 161 - </SafeAreaProvider> 162 - </MutedThreadsProvider> 163 - </BackgroundNotificationPreferencesProvider> 164 - </UnreadNotifsProvider> 165 - </HomeBadgeProvider> 166 - </HiddenRepliesProvider> 167 - </SelectedFeedProvider> 168 - </LoggedOutViewProvider> 169 - </ModerationOptsProvider> 170 - </LabelDefsProvider> 171 - </MessagesProvider> 172 - </ComposerProvider> 173 - </AgeAssuranceV2Provider> 174 - </LiveEventsProvider> 175 - </PolicyUpdateOverlayProvider> 176 - </QueryProvider> 177 - </AnalyticsFeaturesContext> 178 - </React.Fragment> 179 - </ActiveVideoProvider> 180 - </VideoVolumeProvider> 123 + <Splash isReady={isReady && hasCheckedReferrer}> 124 + <VideoVolumeProvider> 125 + <ActiveVideoProvider> 126 + <Fragment 127 + // Resets the entire tree below when it changes: 128 + key={currentAccount?.did}> 129 + <AnalyticsFeaturesContext> 130 + <QueryProvider currentDid={currentAccount?.did}> 131 + <PolicyUpdateOverlayProvider> 132 + <LiveEventsProvider> 133 + <AgeAssuranceV2Provider> 134 + <ComposerProvider> 135 + <MessagesProvider> 136 + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} 137 + <LabelDefsProvider> 138 + <ModerationOptsProvider> 139 + <LoggedOutViewProvider> 140 + <SelectedFeedProvider> 141 + <HiddenRepliesProvider> 142 + <HomeBadgeProvider> 143 + <UnreadNotifsProvider> 144 + <BackgroundNotificationPreferencesProvider> 145 + <MutedThreadsProvider> 146 + <SafeAreaProvider> 147 + <ProgressGuideProvider> 148 + <ServiceConfigProvider> 149 + <EmailVerificationProvider> 150 + <HideBottomBarBorderProvider> 151 + <IntentDialogProvider> 152 + <Shell /> 153 + <ToastOutlet /> 154 + </IntentDialogProvider> 155 + </HideBottomBarBorderProvider> 156 + </EmailVerificationProvider> 157 + </ServiceConfigProvider> 158 + </ProgressGuideProvider> 159 + </SafeAreaProvider> 160 + </MutedThreadsProvider> 161 + </BackgroundNotificationPreferencesProvider> 162 + </UnreadNotifsProvider> 163 + </HomeBadgeProvider> 164 + </HiddenRepliesProvider> 165 + </SelectedFeedProvider> 166 + </LoggedOutViewProvider> 167 + </ModerationOptsProvider> 168 + </LabelDefsProvider> 169 + </MessagesProvider> 170 + </ComposerProvider> 171 + </AgeAssuranceV2Provider> 172 + </LiveEventsProvider> 173 + </PolicyUpdateOverlayProvider> 174 + </QueryProvider> 175 + </AnalyticsFeaturesContext> 176 + </Fragment> 177 + </ActiveVideoProvider> 178 + </VideoVolumeProvider> 179 + </Splash> 181 180 </ContextMenuProvider> 182 181 </ThemeProvider> 183 182 </Alf> ··· 187 186 function App() { 188 187 const [isReady, setReady] = useState(false) 189 188 190 - React.useEffect(() => { 189 + useEffect(() => { 191 190 Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() => 192 191 setReady(true), 193 192 ) 194 193 }, []) 195 194 196 195 if (!isReady) { 197 - return <Splash isReady /> 196 + return null 198 197 } 199 198 200 199 /*
+82 -14
src/Splash.web.tsx
··· 4 4 * the app is ready to go. 5 5 */ 6 6 7 - import {View} from 'react-native' 7 + import {useEffect, useRef, useState} from 'react' 8 8 import Svg, {Path} from 'react-native-svg' 9 9 10 - import {atoms as a} from '#/alf' 10 + import {atoms as a, flatten} from '#/alf' 11 11 12 12 const size = 125 13 13 const ratio = 512 / 512 14 14 15 - export function Splash() { 15 + export function Splash({ 16 + isReady, 17 + children, 18 + }: React.PropsWithChildren<{ 19 + isReady: boolean 20 + }>) { 21 + const [isAnimationComplete, setIsAnimationComplete] = useState(false) 22 + const splashRef = useRef<HTMLDivElement>(null) 23 + 24 + // hide the static one that's baked into the HTML - gets replaced by our React version below 25 + useEffect(() => { 26 + // double rAF ensures that the React version gets painted first 27 + requestAnimationFrame(() => { 28 + requestAnimationFrame(() => { 29 + const splash = document.getElementById('splash') 30 + if (splash) { 31 + splash.remove() 32 + } 33 + }) 34 + }) 35 + }, []) 36 + 37 + // when ready, we fade/scale out 38 + useEffect(() => { 39 + if (!isReady) return 40 + 41 + const reduceMotion = window.matchMedia( 42 + '(prefers-reduced-motion: reduce)', 43 + ).matches 44 + const node = splashRef.current 45 + if (!node || reduceMotion) { 46 + setIsAnimationComplete(true) 47 + return 48 + } 49 + 50 + const animation = node.animate( 51 + [ 52 + {opacity: 1, transform: 'scale(1)'}, 53 + {opacity: 0, transform: 'scale(1.5)'}, 54 + ], 55 + { 56 + duration: 300, 57 + easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 58 + fill: 'forwards', 59 + }, 60 + ) 61 + animation.onfinish = () => setIsAnimationComplete(true) 62 + 63 + return () => { 64 + animation.cancel() 65 + } 66 + }, [isReady]) 67 + 16 68 return ( 17 - <View style={[a.fixed, a.inset_0, a.align_center, a.justify_center]}> 18 - <Svg 19 - fill="none" 20 - viewBox="0 0 512 512" 21 - style={[a.relative, {width: size, height: size * ratio, top: -50}]}> 22 - <Path 23 - fill="#ED5345" 24 - d="M374.473 57.7173C367.666 50.7995 357.119 49.1209 348.441 53.1659C347.173 53.7567 342.223 56.0864 334.796 59.8613C326.32 64.1696 314.568 70.3869 301.394 78.0596C275.444 93.1728 242.399 114.83 218.408 139.477C185.983 172.786 158.719 225.503 140.029 267.661C130.506 289.144 122.878 308.661 117.629 322.81C116.301 326.389 115.124 329.63 114.104 332.478C87.1783 336.42 64.534 341.641 47.5078 348.101C37.6493 351.84 28.3222 356.491 21.0573 362.538C13.8818 368.511 6.00003 378.262 6.00003 391.822C6.00014 403.222 11.8738 411.777 17.4566 417.235C23.0009 422.655 29.9593 426.793 36.871 430.062C50.8097 436.653 69.5275 441.988 90.8362 446.249C133.828 454.846 192.21 460 256.001 460C319.79 460 378.172 454.846 421.164 446.249C442.472 441.988 461.19 436.653 475.129 430.062C482.041 426.793 488.999 422.655 494.543 417.235C500.039 411.862 505.817 403.489 505.996 392.353L506 391.822L505.995 391.188C505.754 377.959 498.012 368.417 490.945 362.534C483.679 356.485 474.35 351.835 464.491 348.095C446.749 341.366 422.906 335.982 394.476 331.987C393.6 330.57 392.633 328.995 391.595 327.273C386.477 318.777 379.633 306.842 372.737 293.115C358.503 264.781 345.757 232.098 344.756 206.636C343.87 184.121 351.638 154.087 360.819 127.789C365.27 115.041 369.795 103.877 373.207 95.9072C374.909 91.9309 376.325 88.7712 377.302 86.6328C377.79 85.5645 378.167 84.7524 378.416 84.2224C378.54 83.9579 378.632 83.7635 378.69 83.643C378.718 83.5829 378.739 83.5411 378.75 83.5181C378.753 83.5108 378.756 83.5049 378.757 83.5015C382.909 74.8634 381.196 64.5488 374.473 57.7173Z" 25 - /> 26 - </Svg> 27 - </View> 69 + <> 70 + {isReady && children} 71 + 72 + {!isAnimationComplete && ( 73 + <div 74 + ref={splashRef} 75 + style={flatten([ 76 + a.fixed, 77 + a.inset_0, 78 + a.flex, 79 + a.align_center, 80 + a.justify_center, 81 + // to compensate for the `top: -50px` below 82 + {transformOrigin: 'center calc(50% - 50px)'}, 83 + ])}> 84 + <Svg 85 + fill="none" 86 + viewBox="0 0 512 512" 87 + style={[a.relative, {width: size, height: size * ratio, top: -50}]}> 88 + <Path 89 + fill="#ED5345" 90 + d="M374.473 57.7173C367.666 50.7995 357.119 49.1209 348.441 53.1659C347.173 53.7567 342.223 56.0864 334.796 59.8613C326.32 64.1696 314.568 70.3869 301.394 78.0596C275.444 93.1728 242.399 114.83 218.408 139.477C185.983 172.786 158.719 225.503 140.029 267.661C130.506 289.144 122.878 308.661 117.629 322.81C116.301 326.389 115.124 329.63 114.104 332.478C87.1783 336.42 64.534 341.641 47.5078 348.101C37.6493 351.84 28.3222 356.491 21.0573 362.538C13.8818 368.511 6.00003 378.262 6.00003 391.822C6.00014 403.222 11.8738 411.777 17.4566 417.235C23.0009 422.655 29.9593 426.793 36.871 430.062C50.8097 436.653 69.5275 441.988 90.8362 446.249C133.828 454.846 192.21 460 256.001 460C319.79 460 378.172 454.846 421.164 446.249C442.472 441.988 461.19 436.653 475.129 430.062C482.041 426.793 488.999 422.655 494.543 417.235C500.039 411.862 505.817 403.489 505.996 392.353L506 391.822L505.995 391.188C505.754 377.959 498.012 368.417 490.945 362.534C483.679 356.485 474.35 351.835 464.491 348.095C446.749 341.366 422.906 335.982 394.476 331.987C393.6 330.57 392.633 328.995 391.595 327.273C386.477 318.777 379.633 306.842 372.737 293.115C358.503 264.781 345.757 232.098 344.756 206.636C343.87 184.121 351.638 154.087 360.819 127.789C365.27 115.041 369.795 103.877 373.207 95.9072C374.909 91.9309 376.325 88.7712 377.302 86.6328C377.79 85.5645 378.167 84.7524 378.416 84.2224C378.54 83.9579 378.632 83.7635 378.69 83.643C378.718 83.5829 378.739 83.5411 378.75 83.5181C378.753 83.5108 378.756 83.5049 378.757 83.5015C382.909 74.8634 381.196 64.5488 374.473 57.7173Z" 91 + /> 92 + </Svg> 93 + </div> 94 + )} 95 + </> 28 96 ) 29 97 }
+5
src/analytics/metrics/types.ts
··· 876 876 'liveEvents:unhideAllFeedBanners': { 877 877 context: LiveEventFeedMetricContext 878 878 } 879 + 880 + 'profile:associated:germ:click-to-chat': {} 881 + 'profile:associated:germ:click-self-info': {} 882 + 'profile:associated:germ:self-disconnect': {} 883 + 'profile:associated:germ:self-reconnect': {} 879 884 }
+4 -2
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
··· 59 59 <BlueskyVideoView 60 60 url={embed.playlist} 61 61 autoplay={!autoplayDisabled && !isWithinMessage} 62 - beginMuted={isGif || autoplayDisabled ? false : muted} 62 + beginMuted={isGif || (autoplayDisabled ? false : muted)} 63 63 style={[a.rounded_sm]} 64 64 onActiveChange={e => { 65 65 setIsActive(e.nativeEvent.isActive) ··· 68 68 setIsLoading(e.nativeEvent.isLoading) 69 69 }} 70 70 onMutedChange={e => { 71 - setMuted(e.nativeEvent.isMuted) 71 + if (!isGif) { 72 + setMuted(e.nativeEvent.isMuted) 73 + } 72 74 }} 73 75 onStatusChange={e => { 74 76 setStatus(e.nativeEvent.status)
+79 -63
src/components/ProgressGuide/List.tsx
··· 1 - import {type StyleProp, View, type ViewStyle} from 'react-native' 1 + import {useState} from 'react' 2 + import { 3 + type LayoutChangeEvent, 4 + type StyleProp, 5 + View, 6 + type ViewStyle, 7 + } from 'react-native' 2 8 import {msg, Trans} from '@lingui/macro' 3 9 import {useLingui} from '@lingui/react' 4 10 ··· 11 17 import {UserAvatar} from '#/view/com/util/UserAvatar' 12 18 import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf' 13 19 import {Button, ButtonIcon} from '#/components/Button' 14 - import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' 20 + import {Person_Filled_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' 15 21 import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' 16 22 import {Text} from '#/components/Typography' 17 23 import type * as bsky from '#/types/bsky' ··· 51 57 a.flex_col, 52 58 a.gap_md, 53 59 a.rounded_md, 54 - t.atoms.bg_contrast_25, 60 + t.atoms.bg_contrast_50, 55 61 a.p_lg, 56 62 style, 57 63 ]}> ··· 81 87 a.justify_between, 82 88 a.gap_sm, 83 89 ] 84 - : a.flex_col, 85 - !inlineLayout && a.gap_md, 90 + : [a.flex_col, a.gap_md], 86 91 ]}> 87 92 <StackedAvatars follows={follows?.pages?.[0]?.follows} /> 88 93 <FollowDialog guide={guide} showArrow={inlineLayout} /> ··· 112 117 113 118 function StackedAvatars({follows}: {follows?: bsky.profile.AnyProfileView[]}) { 114 119 const t = useTheme() 115 - const {centerColumnOffset} = useLayoutBreakpoints() 120 + const [containerWidth, setContainerWidth] = useState(0) 116 121 117 - // Smaller avatars for narrower viewport 118 - const avatarSize = centerColumnOffset ? 30 : 37 119 - const overlap = centerColumnOffset ? 9 : 11 120 - const iconSize = centerColumnOffset ? 14 : 18 122 + const onLayout = (e: LayoutChangeEvent) => { 123 + setContainerWidth(e.nativeEvent.layout.width) 124 + } 121 125 122 - // Use actual follows count, not the guide's event counter 126 + // Overlap ratio (22% of avatar size) 127 + const overlapRatio = 0.22 128 + 129 + // Calculate avatar size to fill container width 130 + // Formula: containerWidth = avatarSize * count - overlap * (count - 1) 131 + // Where overlap = avatarSize * overlapRatio 132 + const visiblePortions = TOTAL_AVATARS - overlapRatio * (TOTAL_AVATARS - 1) 133 + const avatarSize = containerWidth > 0 ? containerWidth / visiblePortions : 0 134 + const overlap = avatarSize * overlapRatio 135 + const iconSize = avatarSize * 0.5 136 + 123 137 const followedAvatars = follows?.slice(0, TOTAL_AVATARS) ?? [] 124 138 const remainingSlots = TOTAL_AVATARS - followedAvatars.length 125 139 126 - // Total width calculation: first avatar + (remaining * visible portion) 127 - const totalWidth = avatarSize + (TOTAL_AVATARS - 1) * (avatarSize - overlap) 128 - 129 140 return ( 130 - <View style={[a.flex_row, a.self_start, {width: totalWidth}]}> 131 - {/* Show followed user avatars */} 132 - {followedAvatars.map((follow, i) => ( 133 - <View 134 - key={follow.did} 135 - style={[ 136 - a.rounded_full, 137 - { 138 - marginLeft: i === 0 ? 0 : -overlap, 139 - zIndex: TOTAL_AVATARS - i, 140 - borderWidth: 2, 141 - borderColor: t.atoms.bg_contrast_25.backgroundColor, 142 - }, 143 - ]}> 144 - <UserAvatar 145 - type="user" 146 - size={avatarSize - 4} 147 - avatar={follow.avatar} 148 - /> 149 - </View> 150 - ))} 151 - {/* Show placeholder avatars for remaining slots */} 152 - {Array(remainingSlots) 153 - .fill(0) 154 - .map((_, i) => ( 155 - <View 156 - key={`placeholder-${i}`} 157 - style={[ 158 - a.align_center, 159 - a.justify_center, 160 - a.rounded_full, 161 - t.atoms.bg_contrast_100, 162 - { 163 - width: avatarSize, 164 - height: avatarSize, 165 - marginLeft: 166 - followedAvatars.length === 0 && i === 0 ? 0 : -overlap, 167 - zIndex: TOTAL_AVATARS - followedAvatars.length - i, 168 - borderWidth: 2, 169 - borderColor: t.atoms.bg_contrast_25.backgroundColor, 170 - }, 171 - ]}> 172 - <PersonIcon 173 - width={iconSize} 174 - height={iconSize} 175 - fill={t.atoms.text_contrast_low.color} 176 - /> 177 - </View> 178 - ))} 141 + <View style={[a.flex_row, a.flex_1]} onLayout={onLayout}> 142 + {containerWidth > 0 && ( 143 + <> 144 + {/* Show followed user avatars */} 145 + {followedAvatars.map((follow, i) => ( 146 + <View 147 + key={follow.did} 148 + style={[ 149 + a.rounded_full, 150 + a.border, 151 + t.atoms.border_contrast_low, 152 + { 153 + marginLeft: i === 0 ? 0 : -overlap, 154 + zIndex: TOTAL_AVATARS - i, 155 + }, 156 + ]}> 157 + <UserAvatar 158 + type="user" 159 + size={avatarSize - 2} 160 + avatar={follow.avatar} 161 + noBorder 162 + /> 163 + </View> 164 + ))} 165 + {/* Show placeholder avatars for remaining slots */} 166 + {Array(remainingSlots) 167 + .fill(0) 168 + .map((_, i) => ( 169 + <View 170 + key={`placeholder-${i}`} 171 + style={[ 172 + a.align_center, 173 + a.justify_center, 174 + a.rounded_full, 175 + t.atoms.bg_contrast_300, 176 + a.border, 177 + t.atoms.border_contrast_low, 178 + { 179 + width: avatarSize, 180 + height: avatarSize, 181 + marginLeft: 182 + followedAvatars.length === 0 && i === 0 ? 0 : -overlap, 183 + zIndex: TOTAL_AVATARS - followedAvatars.length - i, 184 + }, 185 + ]}> 186 + <PersonIcon 187 + width={iconSize} 188 + height={iconSize} 189 + fill={t.atoms.bg_contrast_50.backgroundColor} 190 + /> 191 + </View> 192 + ))} 193 + </> 194 + )} 179 195 </View> 180 196 ) 181 197 }
+17 -2
src/components/dialogs/LinkWarning.tsx
··· 22 22 webOptions={{alignCenter: true}} 23 23 onClose={linkWarningDialogControl.clear}> 24 24 <Dialog.Handle /> 25 - <InAppBrowserConsentInner link={linkWarningDialogControl.value} /> 25 + <LinkWarningDialogInner link={linkWarningDialogControl.value} /> 26 + </Dialog.Outer> 27 + ) 28 + } 29 + 30 + export function CustomLinkWarningDialog({ 31 + control, 32 + link, 33 + }: { 34 + control: Dialog.DialogControlProps 35 + link?: {href: string; displayText: string; share?: boolean} 36 + }) { 37 + return ( 38 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 39 + <Dialog.Handle /> 40 + <LinkWarningDialogInner link={link} /> 26 41 </Dialog.Outer> 27 42 ) 28 43 } 29 44 30 - function InAppBrowserConsentInner({ 45 + function LinkWarningDialogInner({ 31 46 link, 32 47 }: { 33 48 link?: {href: string; displayText: string; share?: boolean}
+56 -58
src/components/dialogs/StarterPackDialog.tsx
··· 1 - import {useCallback, useState} from 'react' 1 + import {useCallback} from 'react' 2 2 import {View} from 'react-native' 3 3 import { 4 4 type AppBskyGraphGetStarterPacksWithMembership, ··· 7 7 import {msg, Plural, Trans} from '@lingui/macro' 8 8 import {useLingui} from '@lingui/react' 9 9 import {useNavigation} from '@react-navigation/native' 10 - import {useQueryClient} from '@tanstack/react-query' 11 10 12 11 import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' 13 12 import {type NavigationProp} from '#/lib/routes/types' 13 + import {isNetworkError} from '#/lib/strings/errors' 14 + import {logger} from '#/logger' 14 15 import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 15 - import { 16 - invalidateActorStarterPacksWithMembershipQuery, 17 - useActorStarterPacksWithMembershipsQuery, 18 - } from '#/state/queries/actor-starter-packs' 16 + import {useActorStarterPacksWithMembershipsQuery} from '#/state/queries/actor-starter-packs' 19 17 import { 20 18 useListMembershipAddMutation, 21 19 useListMembershipRemoveMutation, 22 20 } from '#/state/queries/list-memberships' 23 - import * as Toast from '#/view/com/util/Toast' 24 - import {atoms as a, useTheme} from '#/alf' 21 + import {useProfileQuery} from '#/state/queries/profile' 22 + import {atoms as a, native, platform, useTheme} from '#/alf' 25 23 import {AvatarStack} from '#/components/AvatarStack' 26 24 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 27 25 import * as Dialog from '#/components/Dialog' ··· 30 28 import {StarterPack} from '#/components/icons/StarterPack' 31 29 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 32 30 import {Loader} from '#/components/Loader' 31 + import * as Toast from '#/components/Toast' 33 32 import {Text} from '#/components/Typography' 34 33 import {useAnalytics} from '#/analytics' 35 34 import {IS_WEB} from '#/env' ··· 132 131 }) { 133 132 const control = Dialog.useDialogContext() 134 133 const {_} = useLingui() 134 + const {data: subject} = useProfileQuery({did: targetDid}) 135 135 136 136 const enableSquareButtons = useEnableSquareButtons() 137 137 ··· 158 158 159 159 const renderItem = useCallback( 160 160 ({item}: {item: StarterPackWithMembership}) => ( 161 - <StarterPackItem starterPackWithMembership={item} targetDid={targetDid} /> 161 + <StarterPackItem 162 + starterPackWithMembership={item} 163 + targetDid={targetDid} 164 + subject={subject} 165 + /> 162 166 ), 163 - [targetDid], 167 + [targetDid, subject], 164 168 ) 165 169 166 170 const onClose = useCallback(() => { ··· 171 175 <> 172 176 <View 173 177 style={[ 174 - {justifyContent: 'space-between', flexDirection: 'row'}, 175 - IS_WEB ? a.mb_2xl : a.my_lg, 178 + a.justify_between, 176 179 a.align_center, 180 + a.flex_row, 181 + a.pb_lg, 182 + native(a.pt_lg), 177 183 ]}> 178 184 <Text style={[a.text_lg, a.font_semi_bold]}> 179 185 <Trans>Add to starter packs</Trans> ··· 184 190 variant="ghost" 185 191 color="secondary" 186 192 size="small" 187 - shape={enableSquareButtons ? 'square' : 'round'}> 193 + shape={enableSquareButtons ? 'square' : 'round'} 194 + style={{margin: -8}}> 188 195 <ButtonIcon icon={XIcon} /> 189 196 </Button> 190 197 </View> ··· 235 242 onEndReachedThreshold={0.1} 236 243 ListHeaderComponent={listHeader} 237 244 ListEmptyComponent={<Empty onStartWizard={onStartWizard} />} 238 - style={IS_WEB ? [a.px_md, {minHeight: 500}] : [a.px_2xl, a.pt_lg]} 245 + style={platform({ 246 + web: [a.px_2xl, {minHeight: 500}], 247 + native: [a.px_2xl, a.pt_lg], 248 + })} 239 249 /> 240 250 ) 241 251 } ··· 243 253 function StarterPackItem({ 244 254 starterPackWithMembership, 245 255 targetDid, 256 + subject, 246 257 }: { 247 258 starterPackWithMembership: StarterPackWithMembership 248 259 targetDid: string 260 + subject?: bsky.profile.AnyProfileView 249 261 }) { 250 262 const t = useTheme() 251 263 const ax = useAnalytics() 252 264 const {_} = useLingui() 253 - const queryClient = useQueryClient() 254 265 255 266 const starterPack = starterPackWithMembership.starterPack 256 267 const isInPack = !!starterPackWithMembership.listItem 257 268 258 - const [isPendingRefresh, setIsPendingRefresh] = useState(false) 269 + const {mutate: addMembership, isPending: isPendingAdd} = 270 + useListMembershipAddMutation({ 271 + subject, 272 + onSuccess: () => { 273 + Toast.show(_(msg`Added to starter pack`)) 274 + }, 275 + onError: err => { 276 + if (!isNetworkError(err)) { 277 + logger.error('Failed to add to starter pack', {safeMessage: err}) 278 + } 279 + Toast.show(_(msg`Failed to add to starter pack`), {type: 'error'}) 280 + }, 281 + }) 259 282 260 - const {mutate: addMembership} = useListMembershipAddMutation({ 261 - onSuccess: () => { 262 - Toast.show(_(msg`Added to starter pack`)) 263 - // Use a timeout to wait for the appview to update, matching the pattern 264 - // in list-memberships.ts 265 - setTimeout(() => { 266 - invalidateActorStarterPacksWithMembershipQuery({ 267 - queryClient, 268 - did: targetDid, 269 - }) 270 - setIsPendingRefresh(false) 271 - }, 1e3) 272 - }, 273 - onError: () => { 274 - Toast.show(_(msg`Failed to add to starter pack`), 'xmark') 275 - setIsPendingRefresh(false) 276 - }, 277 - }) 283 + const {mutate: removeMembership, isPending: isPendingRemove} = 284 + useListMembershipRemoveMutation({ 285 + onSuccess: () => { 286 + Toast.show(_(msg`Removed from starter pack`)) 287 + }, 288 + onError: err => { 289 + if (!isNetworkError(err)) { 290 + logger.error('Failed to remove from starter pack', {safeMessage: err}) 291 + } 292 + Toast.show(_(msg`Failed to remove from starter pack`), {type: 'error'}) 293 + }, 294 + }) 278 295 279 - const {mutate: removeMembership} = useListMembershipRemoveMutation({ 280 - onSuccess: () => { 281 - Toast.show(_(msg`Removed from starter pack`)) 282 - // Use a timeout to wait for the appview to update, matching the pattern 283 - // in list-memberships.ts 284 - setTimeout(() => { 285 - invalidateActorStarterPacksWithMembershipQuery({ 286 - queryClient, 287 - did: targetDid, 288 - }) 289 - setIsPendingRefresh(false) 290 - }, 1e3) 291 - }, 292 - onError: () => { 293 - Toast.show(_(msg`Failed to remove from starter pack`), 'xmark') 294 - setIsPendingRefresh(false) 295 - }, 296 - }) 296 + const isPending = isPendingAdd || isPendingRemove 297 297 298 298 const handleToggleMembership = () => { 299 - if (!starterPack.list?.uri || isPendingRefresh) return 299 + if (!starterPack.list?.uri || isPending) return 300 300 301 301 const listUri = starterPack.list.uri 302 302 const starterPackUri = starterPack.uri 303 303 304 - setIsPendingRefresh(true) 305 - 306 304 if (!isInPack) { 307 305 addMembership({ 308 306 listUri: listUri, ··· 312 310 } else { 313 311 if (!starterPackWithMembership.listItem?.uri) { 314 312 console.error('Cannot remove: missing membership URI') 315 - setIsPendingRefresh(false) 316 313 return 317 314 } 318 315 removeMembership({ ··· 347 344 starterPack.listItemsSample.length > 0 && ( 348 345 <> 349 346 <AvatarStack 350 - size={32} 347 + size={24} 351 348 profiles={starterPack.listItemsSample 352 349 ?.slice(0, 4) 353 350 .map(p => p.subject)} ··· 378 375 label={isInPack ? _(msg`Remove`) : _(msg`Add`)} 379 376 color={isInPack ? 'secondary' : 'primary_subtle'} 380 377 size="tiny" 381 - disabled={isPendingRefresh} 378 + disabled={isPending} 382 379 onPress={handleToggleMembership}> 380 + {isPending && <ButtonIcon icon={Loader} />} 383 381 <ButtonText> 384 382 {isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>} 385 383 </ButtonText>
+4
src/components/icons/Person.tsx
··· 36 36 export const PersonGroup_Stroke2_Corner2_Rounded = createSinglePathSVG({ 37 37 path: 'M8 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM4 7a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm13-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Zm7.301 9.7c-.836-2.6-2.88-3.503-4.575-3.111a1 1 0 0 1-.451-1.949c2.815-.651 5.81.966 6.93 4.448a2.49 2.49 0 0 1-.506 2.43A2.92 2.92 0 0 1 20 20h-2a1 1 0 1 1 0-2h2a.92.92 0 0 0 .69-.295.49.49 0 0 0 .112-.505ZM8 14c-1.865 0-3.878 1.274-4.681 4.151a.57.57 0 0 0 .132.55c.15.171.4.299.695.299h7.708a.93.93 0 0 0 .695-.299.57.57 0 0 0 .132-.55C11.878 15.274 9.865 14 8 14Zm0-2c2.87 0 5.594 1.98 6.607 5.613.53 1.9-1.09 3.387-2.753 3.387H4.146c-1.663 0-3.283-1.487-2.753-3.387C2.406 13.981 5.129 12 8 12Z', 38 38 }) 39 + 40 + export const Person_Filled_Corner2_Rounded = createSinglePathSVG({ 41 + path: 'M12.233 2a4.433 4.433 0 1 0 0 8.867 4.433 4.433 0 0 0 0-8.867ZM12.233 12.133c-3.888 0-6.863 2.263-8.071 5.435-.346.906-.11 1.8.44 2.436.535.619 1.36.996 2.25.996h10.762c.89 0 1.716-.377 2.25-.996.55-.636.786-1.53.441-2.436-1.208-3.173-4.184-5.435-8.072-5.435Z', 42 + })
+2
src/lib/constants.ts
··· 51 51 52 52 export const MAX_GRAPHEME_LENGTH = 300 53 53 54 + export const MAX_DRAFT_GRAPHEME_LENGTH = 1000 55 + 54 56 export const MAX_DM_GRAPHEME_LENGTH = 1000 55 57 56 58 // Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html
+12
src/lib/strings/errors.ts
··· 30 30 if (str.includes('Bad token scope') || str.includes('Bad token method')) { 31 31 return t`This feature is not available while using an App Password. Please sign in with your main password.` 32 32 } 33 + if (str.includes('Account has been suspended')) { 34 + return t`Account has been suspended` 35 + } 36 + if (str.includes('Account is deactivated')) { 37 + return t`Account is deactivated` 38 + } 39 + if (str.includes('Profile not found')) { 40 + return t`Profile not found` 41 + } 42 + if (str.includes('Unable to resolve handle')) { 43 + return t`Unable to resolve handle` 44 + } 33 45 if (str.startsWith('Error: ')) { 34 46 return str.slice('Error: '.length) 35 47 }
+184 -118
src/locale/locales/en/messages.po
··· 132 132 msgid "{0, plural, other {# people have}} joined Bluesky via this starter pack!" 133 133 msgstr "" 134 134 135 - #: src/components/dialogs/StarterPackDialog.tsx:361 135 + #: src/components/dialogs/StarterPackDialog.tsx:358 136 136 msgid "{0, plural, other {+# more}}" 137 137 msgstr "" 138 138 ··· 643 643 msgid "Account followed" 644 644 msgstr "" 645 645 646 + #: src/lib/strings/errors.ts:34 647 + msgid "Account has been suspended" 648 + msgstr "" 649 + 650 + #: src/lib/strings/errors.ts:37 651 + msgid "Account is deactivated" 652 + msgstr "" 653 + 646 654 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 647 655 #: src/view/com/profile/ProfileMenu.tsx:158 648 656 msgctxt "toast" ··· 670 678 msgid "Account removed from quick access" 671 679 msgstr "" 672 680 673 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:84 674 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:290 681 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:85 682 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:295 675 683 #: src/view/com/profile/ProfileMenu.tsx:172 676 684 msgctxt "toast" 677 685 msgid "Account unblocked" ··· 704 712 705 713 #: src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx:169 706 714 #: src/components/dialogs/MutedWords.tsx:333 707 - #: src/components/dialogs/StarterPackDialog.tsx:375 708 - #: src/components/dialogs/StarterPackDialog.tsx:381 715 + #: src/components/dialogs/StarterPackDialog.tsx:372 716 + #: src/components/dialogs/StarterPackDialog.tsx:379 709 717 #: src/view/com/modals/UserAddRemoveLists.tsx:235 710 718 msgid "Add" 711 719 msgstr "" ··· 835 843 msgid "Add this feed to your feeds" 836 844 msgstr "" 837 845 838 - #: src/view/com/profile/ProfileMenu.tsx:343 839 - #: src/view/com/profile/ProfileMenu.tsx:346 846 + #: src/view/com/profile/ProfileMenu.tsx:345 847 + #: src/view/com/profile/ProfileMenu.tsx:348 840 848 msgid "Add to lists" 841 849 msgstr "" 842 850 ··· 844 852 msgid "Add to saved posts" 845 853 msgstr "" 846 854 847 - #: src/components/dialogs/StarterPackDialog.tsx:176 848 - #: src/view/com/profile/ProfileMenu.tsx:334 849 - #: src/view/com/profile/ProfileMenu.tsx:337 855 + #: src/components/dialogs/StarterPackDialog.tsx:182 856 + #: src/view/com/profile/ProfileMenu.tsx:335 857 + #: src/view/com/profile/ProfileMenu.tsx:338 850 858 msgid "Add to starter packs" 851 859 msgstr "" 852 860 ··· 863 871 msgid "Added to list" 864 872 msgstr "" 865 873 866 - #: src/components/dialogs/StarterPackDialog.tsx:259 874 + #: src/components/dialogs/StarterPackDialog.tsx:270 867 875 msgid "Added to starter pack" 868 876 msgstr "" 869 877 ··· 1435 1443 msgid "Before creating a post or replying, you must first verify your email." 1436 1444 msgstr "" 1437 1445 1438 - #: src/components/dialogs/StarterPackDialog.tsx:71 1446 + #: src/components/dialogs/StarterPackDialog.tsx:70 1439 1447 #: src/components/StarterPack/ProfileStarterPacks.tsx:263 1440 1448 #: src/components/StarterPack/ProfileStarterPacks.tsx:273 1441 1449 #: src/view/screens/Profile.tsx:351 ··· 1482 1490 msgstr "" 1483 1491 1484 1492 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:821 1485 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 1486 - #: src/view/com/profile/ProfileMenu.tsx:551 1493 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:195 1494 + #: src/view/com/profile/ProfileMenu.tsx:553 1487 1495 msgid "Block" 1488 1496 msgstr "" 1489 1497 ··· 1493 1501 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:708 1494 1502 #: src/screens/Messages/components/RequestButtons.tsx:144 1495 1503 #: src/screens/Messages/components/RequestButtons.tsx:146 1496 - #: src/view/com/profile/ProfileMenu.tsx:457 1497 - #: src/view/com/profile/ProfileMenu.tsx:464 1504 + #: src/view/com/profile/ProfileMenu.tsx:459 1505 + #: src/view/com/profile/ProfileMenu.tsx:466 1498 1506 msgid "Block account" 1499 1507 msgstr "" 1500 1508 1501 1509 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:816 1502 - #: src/view/com/profile/ProfileMenu.tsx:534 1510 + #: src/view/com/profile/ProfileMenu.tsx:536 1503 1511 msgid "Block Account?" 1504 1512 msgstr "" 1505 1513 ··· 1551 1559 msgstr "" 1552 1560 1553 1561 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:818 1554 - #: src/view/com/profile/ProfileMenu.tsx:546 1562 + #: src/view/com/profile/ProfileMenu.tsx:548 1555 1563 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." 1556 1564 msgstr "" 1557 1565 ··· 1567 1575 msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." 1568 1576 msgstr "" 1569 1577 1570 - #: src/view/com/profile/ProfileMenu.tsx:543 1578 + #: src/view/com/profile/ProfileMenu.tsx:545 1571 1579 msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." 1572 1580 msgstr "" 1573 1581 ··· 1601 1609 msgid "Bluesky is an open network where you can choose your own provider. If you're new here, we recommend sticking with the default Bluesky Social option." 1602 1610 msgstr "" 1603 1611 1604 - #: src/components/ProgressGuide/List.tsx:103 1612 + #: src/components/ProgressGuide/List.tsx:108 1605 1613 msgid "Bluesky is better with friends!" 1606 1614 msgstr "" 1607 1615 ··· 2104 2112 #: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:190 2105 2113 #: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:198 2106 2114 #: src/components/dialogs/SearchablePeopleList.tsx:295 2107 - #: src/components/dialogs/StarterPackDialog.tsx:179 2115 + #: src/components/dialogs/StarterPackDialog.tsx:185 2108 2116 #: src/components/dms/AfterReportDialog.tsx:93 2109 2117 #: src/components/dms/AfterReportDialog.tsx:98 2110 2118 #: src/components/dms/AfterReportDialog.tsx:208 ··· 2477 2485 msgid "Copy App Password" 2478 2486 msgstr "" 2479 2487 2480 - #: src/view/com/profile/ProfileMenu.tsx:494 2481 - #: src/view/com/profile/ProfileMenu.tsx:497 2488 + #: src/view/com/profile/ProfileMenu.tsx:496 2489 + #: src/view/com/profile/ProfileMenu.tsx:499 2482 2490 msgid "Copy at:// URI" 2483 2491 msgstr "" 2484 2492 ··· 2493 2501 msgstr "" 2494 2502 2495 2503 #: src/screens/Settings/components/ChangeHandleDialog.tsx:502 2496 - #: src/view/com/profile/ProfileMenu.tsx:503 2497 - #: src/view/com/profile/ProfileMenu.tsx:506 2504 + #: src/view/com/profile/ProfileMenu.tsx:505 2505 + #: src/view/com/profile/ProfileMenu.tsx:508 2498 2506 msgid "Copy DID" 2499 2507 msgstr "" 2500 2508 ··· 2594 2602 msgstr "" 2595 2603 2596 2604 #: src/screens/ProfileList/components/ErrorScreen.tsx:26 2597 - #: src/screens/ProfileList/index.tsx:79 2598 - #: src/screens/ProfileList/index.tsx:101 2605 + #: src/screens/ProfileList/index.tsx:80 2606 + #: src/screens/ProfileList/index.tsx:102 2599 2607 msgid "Could not load list" 2600 2608 msgstr "" 2601 2609 ··· 2629 2637 2630 2638 #. Text on button to create a new starter pack 2631 2639 #. Text on button to create a new starter pack 2632 - #: src/components/dialogs/StarterPackDialog.tsx:112 2633 - #: src/components/dialogs/StarterPackDialog.tsx:201 2640 + #: src/components/dialogs/StarterPackDialog.tsx:111 2641 + #: src/components/dialogs/StarterPackDialog.tsx:208 2634 2642 #: src/components/StarterPack/ProfileStarterPacks.tsx:328 2635 2643 msgid "Create" 2636 2644 msgstr "" ··· 2711 2719 msgid "Create report for {0}" 2712 2720 msgstr "" 2713 2721 2714 - #: src/components/dialogs/StarterPackDialog.tsx:107 2715 - #: src/components/dialogs/StarterPackDialog.tsx:196 2722 + #: src/components/dialogs/StarterPackDialog.tsx:106 2723 + #: src/components/dialogs/StarterPackDialog.tsx:203 2716 2724 msgid "Create starter pack" 2717 2725 msgstr "" 2718 2726 ··· 3025 3033 msgid "Discard post?" 3026 3034 msgstr "" 3027 3035 3036 + #: src/screens/Profile/components/GermButton.tsx:260 3037 + #: src/screens/Profile/components/GermButton.tsx:267 3038 + msgid "Disconnect Germ DM" 3039 + msgstr "" 3040 + 3028 3041 #: src/screens/Settings/components/PwiOptOut.tsx:87 3029 3042 #: src/screens/Settings/components/PwiOptOut.tsx:91 3030 3043 msgid "Discourage apps from showing my account to logged-out users" ··· 3052 3065 msgid "Dismiss error" 3053 3066 msgstr "" 3054 3067 3055 - #: src/components/ProgressGuide/List.tsx:67 3068 + #: src/components/ProgressGuide/List.tsx:73 3056 3069 msgid "Dismiss getting started guide" 3057 3070 msgstr "" 3058 3071 ··· 3279 3292 msgid "Edit list details" 3280 3293 msgstr "" 3281 3294 3282 - #: src/view/com/profile/ProfileMenu.tsx:357 3283 - #: src/view/com/profile/ProfileMenu.tsx:377 3295 + #: src/view/com/profile/ProfileMenu.tsx:359 3296 + #: src/view/com/profile/ProfileMenu.tsx:379 3284 3297 msgid "Edit live status" 3285 3298 msgstr "" 3286 3299 ··· 3309 3322 #: src/screens/Profile/Header/EditProfileDialog.tsx:268 3310 3323 #: src/screens/Profile/Header/EditProfileDialog.tsx:274 3311 3324 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:295 3312 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:324 3325 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:329 3313 3326 msgid "Edit profile" 3314 3327 msgstr "" 3315 3328 3316 3329 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:298 3317 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:326 3330 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:331 3318 3331 msgid "Edit Profile" 3319 3332 msgstr "" 3320 3333 ··· 3524 3537 msgid "Enter your username and password" 3525 3538 msgstr "" 3526 3539 3527 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:146 3540 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:148 3528 3541 msgid "Enters full screen" 3529 3542 msgstr "" 3530 3543 ··· 3710 3723 msgid "Failed to add emoji reaction" 3711 3724 msgstr "" 3712 3725 3713 - #: src/components/dialogs/StarterPackDialog.tsx:271 3726 + #: src/components/dialogs/StarterPackDialog.tsx:276 3714 3727 msgid "Failed to add to starter pack" 3715 3728 msgstr "" 3716 3729 ··· 3747 3760 3748 3761 #: src/screens/StarterPack/StarterPackScreen.tsx:732 3749 3762 msgid "Failed to delete starter pack" 3763 + msgstr "" 3764 + 3765 + #: src/screens/Profile/components/GermButton.tsx:194 3766 + msgid "Failed to disconnect Germ DM. Error: {0}" 3750 3767 msgstr "" 3751 3768 3752 3769 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:133 ··· 3823 3840 msgid "Failed to pin post" 3824 3841 msgstr "" 3825 3842 3843 + #: src/screens/Profile/components/GermButton.tsx:162 3844 + msgid "Failed to reconnect Germ DM. Error: {0}" 3845 + msgstr "" 3846 + 3826 3847 #: src/screens/Settings/FindContactsSettings.tsx:487 3827 3848 msgid "Failed to remove data due to a network error, please check your internet connection." 3828 3849 msgstr "" ··· 3836 3857 msgid "Failed to remove emoji reaction" 3837 3858 msgstr "" 3838 3859 3839 - #: src/components/dialogs/StarterPackDialog.tsx:290 3860 + #: src/components/dialogs/StarterPackDialog.tsx:289 3840 3861 msgid "Failed to remove from starter pack" 3841 3862 msgstr "" 3842 3863 ··· 4126 4147 #: src/components/ProfileHoverCard/index.web.tsx:497 4127 4148 #: src/components/ProfileHoverCard/index.web.tsx:508 4128 4149 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:152 4129 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:381 4150 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:386 4130 4151 #: src/screens/VideoFeed/index.tsx:874 4131 4152 #: src/view/com/notifications/NotificationFeedItem.tsx:841 4132 4153 #: src/view/com/notifications/NotificationFeedItem.tsx:848 ··· 4134 4155 msgstr "" 4135 4156 4136 4157 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:139 4137 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:369 4158 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:374 4138 4159 msgid "Follow {0}" 4139 4160 msgstr "" 4140 4161 ··· 4146 4167 msgid "Follow 10 accounts" 4147 4168 msgstr "" 4148 4169 4149 - #: src/components/ProgressGuide/List.tsx:60 4170 + #: src/components/ProgressGuide/List.tsx:66 4150 4171 msgid "Follow 10 people to get started" 4151 4172 msgstr "" 4152 4173 4153 - #: src/components/ProgressGuide/List.tsx:102 4174 + #: src/components/ProgressGuide/List.tsx:107 4154 4175 msgid "Follow 7 accounts" 4155 4176 msgstr "" 4156 4177 ··· 4178 4199 #. User is not following this account, click to follow back 4179 4200 #: src/components/ProfileCard.tsx:553 4180 4201 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:150 4181 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:379 4202 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:384 4182 4203 #: src/view/com/notifications/NotificationFeedItem.tsx:841 4183 4204 #: src/view/com/notifications/NotificationFeedItem.tsx:848 4184 4205 msgid "Follow back" ··· 4223 4244 #: src/components/ProfileHoverCard/index.web.tsx:496 4224 4245 #: src/components/ProfileHoverCard/index.web.tsx:507 4225 4246 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:155 4226 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:377 4247 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:382 4227 4248 #: src/screens/VideoFeed/index.tsx:872 4228 4249 #: src/view/com/notifications/NotificationFeedItem.tsx:819 4229 4250 #: src/view/com/notifications/NotificationFeedItem.tsx:836 ··· 4237 4258 msgstr "" 4238 4259 4239 4260 #: src/components/ProfileCard.tsx:509 4240 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:244 4261 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:249 4241 4262 #: src/view/com/notifications/NotificationFeedItem.tsx:772 4242 4263 msgid "Following {0}" 4243 4264 msgstr "" ··· 4339 4360 msgid "Generate a starter pack" 4340 4361 msgstr "" 4341 4362 4363 + #: src/screens/Profile/components/GermButton.tsx:85 4364 + #: src/screens/Profile/components/GermButton.tsx:223 4365 + msgid "Germ DM" 4366 + msgstr "" 4367 + 4368 + #: src/screens/Profile/components/GermButton.tsx:181 4369 + msgid "Germ DM disconnected" 4370 + msgstr "" 4371 + 4372 + #: src/screens/Profile/components/GermButton.tsx:232 4373 + #: src/screens/Profile/components/GermButton.tsx:237 4374 + msgid "Germ DM Link" 4375 + msgstr "" 4376 + 4377 + #: src/screens/Profile/components/GermButton.tsx:158 4378 + msgid "Germ DM reconnected" 4379 + msgstr "" 4380 + 4342 4381 #: src/view/shell/Drawer.tsx:374 4343 4382 msgid "Get help" 4344 4383 msgstr "" ··· 4421 4460 msgid "Glorification of violence" 4422 4461 msgstr "" 4423 4462 4424 - #: src/components/dialogs/LinkWarning.tsx:111 4425 - #: src/components/dialogs/LinkWarning.tsx:117 4463 + #: src/components/dialogs/LinkWarning.tsx:126 4464 + #: src/components/dialogs/LinkWarning.tsx:132 4426 4465 #: src/components/Layout/Header/index.tsx:128 4427 4466 #: src/components/moderation/ScreenHider.tsx:160 4428 4467 #: src/components/moderation/ScreenHider.tsx:169 ··· 4471 4510 msgid "Go Home" 4472 4511 msgstr "" 4473 4512 4474 - #: src/view/com/profile/ProfileMenu.tsx:358 4475 - #: src/view/com/profile/ProfileMenu.tsx:379 4513 + #: src/view/com/profile/ProfileMenu.tsx:360 4514 + #: src/view/com/profile/ProfileMenu.tsx:381 4476 4515 msgid "Go live" 4477 4516 msgstr "" 4478 4517 ··· 4483 4522 msgid "Go Live" 4484 4523 msgstr "" 4485 4524 4486 - #: src/view/com/profile/ProfileMenu.tsx:355 4487 - #: src/view/com/profile/ProfileMenu.tsx:375 4525 + #: src/view/com/profile/ProfileMenu.tsx:357 4526 + #: src/view/com/profile/ProfileMenu.tsx:377 4488 4527 msgid "Go live (disabled)" 4489 4528 msgstr "" 4490 4529 ··· 4527 4566 4528 4567 #: src/components/live/GoLiveDisabledDialog.tsx:104 4529 4568 msgid "Going live is currently disabled for your account" 4569 + msgstr "" 4570 + 4571 + #: src/screens/Profile/components/GermButton.tsx:251 4572 + #: src/screens/Profile/components/GermButton.tsx:256 4573 + msgid "Got it" 4530 4574 msgstr "" 4531 4575 4532 4576 #: src/lib/moderation/useGlobalLabelStrings.ts:46 ··· 5284 5328 msgid "Learn more about what is public on Bluesky." 5285 5329 msgstr "" 5286 5330 5331 + #: src/screens/Profile/components/GermButton.tsx:210 5332 + msgid "Learn more about your Germ DM link" 5333 + msgstr "" 5334 + 5287 5335 #: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:89 5288 5336 msgid "Learn more in your <0>account settings.</0>" 5289 5337 msgstr "" ··· 5314 5362 msgid "Leave them all unselected to see any language." 5315 5363 msgstr "" 5316 5364 5317 - #: src/components/dialogs/LinkWarning.tsx:67 5318 - #: src/components/dialogs/LinkWarning.tsx:75 5365 + #: src/components/dialogs/LinkWarning.tsx:82 5366 + #: src/components/dialogs/LinkWarning.tsx:90 5319 5367 msgid "Leaving Bluesky" 5320 5368 msgstr "" 5321 5369 ··· 5356 5404 msgid "Like ({0, plural, one {# like} other {# likes}})" 5357 5405 msgstr "" 5358 5406 5359 - #: src/components/ProgressGuide/List.tsx:96 5407 + #: src/components/ProgressGuide/List.tsx:101 5360 5408 msgid "Like 10 posts" 5361 5409 msgstr "" 5362 5410 ··· 5473 5521 msgid "List has been hidden" 5474 5522 msgstr "" 5475 5523 5476 - #: src/screens/ProfileList/index.tsx:172 5524 + #: src/screens/ProfileList/index.tsx:176 5477 5525 msgid "List Hidden" 5478 5526 msgstr "" 5479 5527 ··· 5643 5691 msgid "Make one for me" 5644 5692 msgstr "" 5645 5693 5646 - #: src/components/dialogs/LinkWarning.tsx:85 5694 + #: src/components/dialogs/LinkWarning.tsx:100 5647 5695 msgid "Make sure this is where you intend to go!" 5648 5696 msgstr "" 5649 5697 ··· 5870 5918 msgid "Music" 5871 5919 msgstr "" 5872 5920 5873 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:167 5921 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:169 5874 5922 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:97 5875 5923 msgctxt "video" 5876 5924 msgid "Mute" ··· 5883 5931 5884 5932 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:689 5885 5933 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:695 5886 - #: src/view/com/profile/ProfileMenu.tsx:436 5887 - #: src/view/com/profile/ProfileMenu.tsx:443 5934 + #: src/view/com/profile/ProfileMenu.tsx:438 5935 + #: src/view/com/profile/ProfileMenu.tsx:445 5888 5936 msgid "Mute account" 5889 5937 msgstr "" 5890 5938 ··· 6014 6062 msgstr "" 6015 6063 6016 6064 #: src/screens/Search/modules/ExploreTrendingTopics.tsx:193 6017 - #: src/view/com/profile/ProfileMenu.tsx:391 6065 + #: src/view/com/profile/ProfileMenu.tsx:393 6018 6066 msgid "New" 6019 6067 msgstr "" 6020 6068 ··· 6081 6129 msgstr "" 6082 6130 6083 6131 #: src/screens/Profile/ProfileFeed/index.tsx:250 6084 - #: src/screens/ProfileList/index.tsx:246 6085 - #: src/screens/ProfileList/index.tsx:284 6132 + #: src/screens/ProfileList/index.tsx:250 6133 + #: src/screens/ProfileList/index.tsx:299 6086 6134 #: src/view/screens/Feeds.tsx:552 6087 6135 #: src/view/screens/Notifications.tsx:166 6088 6136 #: src/view/screens/Profile.tsx:594 ··· 6107 6155 msgid "New posts from {firstAuthorName} and {additionalAuthorsCount, plural, one {{formattedAuthorsCount} other} other {{formattedAuthorsCount} others}}" 6108 6156 msgstr "" 6109 6157 6110 - #: src/components/dialogs/StarterPackDialog.tsx:193 6158 + #: src/components/dialogs/StarterPackDialog.tsx:200 6111 6159 msgid "New starter pack" 6112 6160 msgstr "" 6113 6161 ··· 6211 6259 msgstr "" 6212 6260 6213 6261 #: src/components/ProfileCard.tsx:531 6214 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:269 6262 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:274 6215 6263 #: src/view/com/notifications/NotificationFeedItem.tsx:792 6216 6264 msgid "No longer following {0}" 6217 6265 msgstr "" ··· 6373 6421 msgid "Not ready to hit post? Keep your best ideas in Drafts until the timing is just right." 6374 6422 msgstr "" 6375 6423 6376 - #: src/view/com/profile/ProfileMenu.tsx:558 6424 + #: src/view/com/profile/ProfileMenu.tsx:560 6377 6425 msgid "Note about sharing" 6378 6426 msgstr "" 6379 6427 ··· 6586 6634 msgid "Open full emoji list" 6587 6635 msgstr "" 6588 6636 6637 + #: src/screens/Profile/components/GermButton.tsx:74 6638 + msgid "Open Germ DM" 6639 + msgstr "" 6640 + 6589 6641 #: src/components/Post/Embed/ExternalEmbed/index.tsx:79 6590 6642 msgid "Open link to {niceUrl}" 6591 6643 msgstr "" ··· 6703 6755 msgid "Opens image picker" 6704 6756 msgstr "" 6705 6757 6706 - #: src/components/dialogs/LinkWarning.tsx:97 6758 + #: src/components/dialogs/LinkWarning.tsx:112 6707 6759 msgid "Opens link {0}" 6708 6760 msgstr "" 6709 6761 ··· 6846 6898 msgid "Password updated!" 6847 6899 msgstr "" 6848 6900 6849 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:151 6901 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153 6850 6902 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:385 6851 6903 msgid "Pause" 6852 6904 msgstr "" ··· 6859 6911 msgid "Pause video" 6860 6912 msgstr "" 6861 6913 6862 - #: src/screens/ProfileList/index.tsx:166 6914 + #: src/screens/ProfileList/index.tsx:168 6863 6915 #: src/screens/Search/SearchResults.tsx:72 6864 6916 #: src/screens/StarterPack/StarterPackScreen.tsx:192 6865 6917 msgid "People" ··· 6960 7012 msgid "Pinned to your feeds" 6961 7013 msgstr "" 6962 7014 6963 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:151 7015 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153 6964 7016 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:386 6965 7017 msgid "Play" 6966 7018 msgstr "" ··· 6986 7038 msgid "Plays or pauses the GIF" 6987 7039 msgstr "" 6988 7040 6989 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:152 7041 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:154 6990 7042 msgid "Plays or pauses the video" 6991 7043 msgstr "" 6992 7044 ··· 7256 7308 7257 7309 #: src/components/activity-notifications/SubscribeProfileDialog.tsx:250 7258 7310 #: src/components/activity-notifications/SubscribeProfileDialog.tsx:262 7259 - #: src/screens/ProfileList/index.tsx:166 7311 + #: src/screens/ProfileList/index.tsx:168 7260 7312 #: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:216 7261 7313 #: src/screens/StarterPack/StarterPackScreen.tsx:194 7262 7314 #: src/view/screens/Profile.tsx:234 ··· 7275 7327 msgid "Posts, Replies" 7276 7328 msgstr "" 7277 7329 7278 - #: src/components/dialogs/LinkWarning.tsx:73 7330 + #: src/components/dialogs/LinkWarning.tsx:88 7279 7331 msgid "Potentially misleading link" 7280 7332 msgstr "" 7281 7333 7282 - #: src/components/dialogs/LinkWarning.tsx:66 7334 + #: src/components/dialogs/LinkWarning.tsx:81 7283 7335 msgid "Potentially misleading link warning" 7284 7336 msgstr "" 7285 7337 ··· 7378 7430 #: src/view/shell/Drawer.tsx:79 7379 7431 #: src/view/shell/Drawer.tsx:598 7380 7432 msgid "Profile" 7433 + msgstr "" 7434 + 7435 + #: src/lib/strings/errors.ts:40 7436 + msgid "Profile not found" 7381 7437 msgstr "" 7382 7438 7383 7439 #: src/screens/Profile/Header/EditProfileDialog.tsx:189 ··· 7601 7657 7602 7658 #: src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx:171 7603 7659 #: src/components/dialogs/MutedWords.tsx:443 7604 - #: src/components/dialogs/StarterPackDialog.tsx:375 7605 - #: src/components/dialogs/StarterPackDialog.tsx:381 7660 + #: src/components/dialogs/StarterPackDialog.tsx:372 7661 + #: src/components/dialogs/StarterPackDialog.tsx:379 7606 7662 #: src/components/FeedCard.tsx:375 7607 7663 #: src/components/StarterPack/Wizard/WizardListCard.tsx:105 7608 7664 #: src/components/StarterPack/Wizard/WizardListCard.tsx:112 ··· 7726 7782 7727 7783 #: src/components/verification/VerificationRemovePrompt.tsx:46 7728 7784 #: src/components/verification/VerificationsDialog.tsx:249 7729 - #: src/view/com/profile/ProfileMenu.tsx:409 7730 - #: src/view/com/profile/ProfileMenu.tsx:412 7785 + #: src/view/com/profile/ProfileMenu.tsx:411 7786 + #: src/view/com/profile/ProfileMenu.tsx:414 7731 7787 msgid "Remove verification" 7732 7788 msgstr "" 7733 7789 ··· 7757 7813 msgid "Removed from saved posts" 7758 7814 msgstr "" 7759 7815 7760 - #: src/components/dialogs/StarterPackDialog.tsx:278 7816 + #: src/components/dialogs/StarterPackDialog.tsx:283 7761 7817 msgid "Removed from starter pack" 7762 7818 msgstr "" 7763 7819 ··· 7862 7918 msgid "Report" 7863 7919 msgstr "" 7864 7920 7865 - #: src/view/com/profile/ProfileMenu.tsx:476 7866 - #: src/view/com/profile/ProfileMenu.tsx:479 7921 + #: src/view/com/profile/ProfileMenu.tsx:478 7922 + #: src/view/com/profile/ProfileMenu.tsx:481 7867 7923 msgid "Report account" 7868 7924 msgstr "" 7869 7925 ··· 8758 8814 msgid "Share a fun fact!" 8759 8815 msgstr "" 8760 8816 8761 - #: src/view/com/profile/ProfileMenu.tsx:563 8817 + #: src/view/com/profile/ProfileMenu.tsx:565 8762 8818 msgid "Share anyway" 8763 8819 msgstr "" 8764 8820 ··· 8767 8823 msgid "Share author DID" 8768 8824 msgstr "" 8769 8825 8770 - #: src/components/dialogs/LinkWarning.tsx:96 8771 - #: src/components/dialogs/LinkWarning.tsx:104 8826 + #: src/components/dialogs/LinkWarning.tsx:111 8827 + #: src/components/dialogs/LinkWarning.tsx:119 8772 8828 #: src/components/StarterPack/ShareDialog.tsx:114 8773 8829 #: src/components/StarterPack/ShareDialog.tsx:123 8774 8830 msgid "Share link" ··· 9437 9493 msgid "Task complete - 10 likes!" 9438 9494 msgstr "" 9439 9495 9440 - #: src/components/ProgressGuide/List.tsx:97 9496 + #: src/components/ProgressGuide/List.tsx:102 9441 9497 msgid "Teach our algorithm what you like" 9442 9498 msgstr "" 9443 9499 ··· 9522 9578 msgid "That's everything!" 9523 9579 msgstr "" 9524 9580 9525 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:186 9526 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:394 9527 - #: src/view/com/profile/ProfileMenu.tsx:539 9581 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:191 9582 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:399 9583 + #: src/view/com/profile/ProfileMenu.tsx:541 9528 9584 msgid "The account will be able to interact with you after unblocking." 9529 9585 msgstr "" 9530 9586 ··· 9707 9763 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:450 9708 9764 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:116 9709 9765 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:127 9710 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:88 9711 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:253 9712 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:279 9713 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:294 9766 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89 9767 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:258 9768 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:284 9769 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:299 9714 9770 #: src/view/com/profile/ProfileMenu.tsx:152 9715 9771 #: src/view/com/profile/ProfileMenu.tsx:162 9716 9772 #: src/view/com/profile/ProfileMenu.tsx:176 ··· 9787 9843 #: src/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated.tsx:27 9788 9844 #: src/screens/PostThread/components/ThreadItemPostNoUnauthenticated.tsx:47 9789 9845 msgid "This author has chosen to make their posts visible only to people who are signed in." 9846 + msgstr "" 9847 + 9848 + #: src/screens/Profile/components/GermButton.tsx:242 9849 + msgid "This button lets others open the Germ DM app to send you a message. You can manage its visibility from the Germ DM app, or you can disconnect your Bluesky account from Germ DM altogether by clicking the button below." 9790 9850 msgstr "" 9791 9851 9792 9852 #: src/screens/Messages/components/MessageListError.tsx:18 ··· 9897 9957 msgid "This labeler hasn't declared what labels it publishes, and may not be active." 9898 9958 msgstr "" 9899 9959 9900 - #: src/components/dialogs/LinkWarning.tsx:79 9960 + #: src/components/dialogs/LinkWarning.tsx:94 9901 9961 msgid "This link is taking you to the following website:" 9902 9962 msgstr "" 9903 9963 ··· 9941 10001 msgid "This post's author has disabled quote posts." 9942 10002 msgstr "" 9943 10003 9944 - #: src/view/com/profile/ProfileMenu.tsx:560 10004 + #: src/view/com/profile/ProfileMenu.tsx:562 9945 10005 msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't signed in." 9946 10006 msgstr "" 9947 10007 ··· 10070 10130 msgid "Toggle to enable or disable adult content" 10071 10131 msgstr "" 10072 10132 10073 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:169 10133 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:171 10074 10134 msgid "Toggles the sound" 10075 10135 msgstr "" 10076 10136 ··· 10189 10249 msgid "Unable to delete" 10190 10250 msgstr "" 10191 10251 10252 + #: src/lib/strings/errors.ts:43 10253 + msgid "Unable to resolve handle" 10254 + msgstr "" 10255 + 10192 10256 #: src/screens/Settings/Settings.tsx:523 10193 10257 msgid "Unapply Pull Request" 10194 10258 msgstr "" ··· 10209 10273 #: src/components/dms/MessagesListBlockedFooter.tsx:104 10210 10274 #: src/components/dms/MessagesListBlockedFooter.tsx:112 10211 10275 #: src/components/dms/MessagesListBlockedFooter.tsx:119 10212 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 10213 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:337 10214 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:397 10276 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:195 10277 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:342 10278 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:402 10215 10279 #: src/screens/ProfileList/components/Header.tsx:165 10216 10280 #: src/screens/ProfileList/components/Header.tsx:172 10217 - #: src/view/com/profile/ProfileMenu.tsx:551 10281 + #: src/view/com/profile/ProfileMenu.tsx:553 10218 10282 msgid "Unblock" 10219 10283 msgstr "" 10220 10284 10221 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:341 10285 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:346 10222 10286 msgctxt "action" 10223 10287 msgid "Unblock" 10224 10288 msgstr "" 10225 10289 10226 10290 #: src/components/dms/ConvoMenu.tsx:261 10227 10291 #: src/components/dms/ConvoMenu.tsx:264 10228 - #: src/view/com/profile/ProfileMenu.tsx:456 10229 - #: src/view/com/profile/ProfileMenu.tsx:462 10292 + #: src/view/com/profile/ProfileMenu.tsx:458 10293 + #: src/view/com/profile/ProfileMenu.tsx:464 10230 10294 msgid "Unblock account" 10231 10295 msgstr "" 10232 10296 10233 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:184 10234 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:392 10235 - #: src/view/com/profile/ProfileMenu.tsx:533 10297 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:189 10298 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:397 10299 + #: src/view/com/profile/ProfileMenu.tsx:535 10236 10300 msgid "Unblock Account?" 10237 10301 msgstr "" 10238 10302 ··· 10252 10316 #: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:78 10253 10317 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:39 10254 10318 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:45 10319 + #: src/screens/Profile/components/GermButton.tsx:184 10320 + #: src/screens/Profile/components/GermButton.tsx:185 10255 10321 msgid "Undo" 10256 10322 msgstr "" 10257 10323 ··· 10265 10331 msgid "Undo repost ({0, plural, one {# repost} other {# reposts}})" 10266 10332 msgstr "" 10267 10333 10268 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:368 10334 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:373 10269 10335 msgid "Unfollow {0}" 10270 10336 msgstr "" 10271 10337 ··· 10311 10377 msgid "Unlike ({0, plural, one {# like} other {# likes}})" 10312 10378 msgstr "" 10313 10379 10314 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:166 10380 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:168 10315 10381 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96 10316 10382 msgctxt "video" 10317 10383 msgid "Unmute" ··· 10329 10395 10330 10396 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:688 10331 10397 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:694 10332 - #: src/view/com/profile/ProfileMenu.tsx:435 10333 - #: src/view/com/profile/ProfileMenu.tsx:441 10398 + #: src/view/com/profile/ProfileMenu.tsx:437 10399 + #: src/view/com/profile/ProfileMenu.tsx:443 10334 10400 msgid "Unmute account" 10335 10401 msgstr "" 10336 10402 ··· 10646 10712 10647 10713 #: src/components/verification/VerificationCreatePrompt.tsx:84 10648 10714 #: src/components/verification/VerificationCreatePrompt.tsx:86 10649 - #: src/view/com/profile/ProfileMenu.tsx:419 10650 - #: src/view/com/profile/ProfileMenu.tsx:422 10715 + #: src/view/com/profile/ProfileMenu.tsx:421 10716 + #: src/view/com/profile/ProfileMenu.tsx:424 10651 10717 msgid "Verify account" 10652 10718 msgstr "" 10653 10719 ··· 10728 10794 msgid "Version {0}" 10729 10795 msgstr "" 10730 10796 10731 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:84 10732 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:145 10797 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:86 10798 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:147 10733 10799 msgid "Video" 10734 10800 msgstr "" 10735 10801 ··· 10774 10840 msgid "Video uploaded" 10775 10841 msgstr "" 10776 10842 10777 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:84 10843 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:86 10778 10844 msgid "Video: {0}" 10779 10845 msgstr "" 10780 10846 ··· 10918 10984 msgid "Violent or threatening content" 10919 10985 msgstr "" 10920 10986 10921 - #: src/components/dialogs/LinkWarning.tsx:96 10922 - #: src/components/dialogs/LinkWarning.tsx:106 10987 + #: src/components/dialogs/LinkWarning.tsx:111 10988 + #: src/components/dialogs/LinkWarning.tsx:121 10923 10989 msgid "Visit site" 10924 10990 msgstr "" 10925 10991 ··· 11090 11156 msgid "We're sorry, but based on your device's location, you are currently located in a region that requires age assurance." 11091 11157 msgstr "" 11092 11158 11093 - #: src/screens/ProfileList/index.tsx:87 11159 + #: src/screens/ProfileList/index.tsx:88 11094 11160 msgid "We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @{handleOrDid}." 11095 11161 msgstr "" 11096 11162 ··· 11494 11560 msgid "You have no lists." 11495 11561 msgstr "" 11496 11562 11497 - #: src/components/dialogs/StarterPackDialog.tsx:101 11563 + #: src/components/dialogs/StarterPackDialog.tsx:100 11498 11564 msgid "You have no starter packs." 11499 11565 msgstr "" 11500 11566
+5
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 48 48 import {Text} from '#/components/Typography' 49 49 import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 50 50 import {IS_IOS} from '#/env' 51 + import {GermButton} from '../components/GermButton' 51 52 import {EditProfileDialog} from './EditProfileDialog' 52 53 import {ProfileHeaderHandle} from './Handle' 53 54 import {ProfileHeaderMetrics} from './Metrics' ··· 182 183 /> 183 184 </View> 184 185 ) : undefined} 186 + 187 + {profile.associated?.germ && ( 188 + <GermButton germ={profile.associated.germ} profile={profile} /> 189 + )} 185 190 186 191 {!isMe && 187 192 !disableFollowedByMetrics &&
+334
src/screens/Profile/components/GermButton.tsx
··· 1 + import {Platform, View} from 'react-native' 2 + import {Image} from 'expo-image' 3 + import { 4 + type AppBskyActorDefs, 5 + type AppBskyActorGetProfile, 6 + type AtpAgent, 7 + } from '@atproto/api' 8 + import {msg, Trans} from '@lingui/macro' 9 + import {useLingui} from '@lingui/react' 10 + import {useMutation, useQueryClient} from '@tanstack/react-query' 11 + 12 + import {until} from '#/lib/async/until' 13 + import {isNetworkError} from '#/lib/strings/errors' 14 + import {RQKEY} from '#/state/queries/profile' 15 + import {useAgent, useSession} from '#/state/session' 16 + import {atoms as a, useTheme, web} from '#/alf' 17 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 18 + import * as Dialog from '#/components/Dialog' 19 + import {CustomLinkWarningDialog} from '#/components/dialogs/LinkWarning' 20 + import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRightIcon} from '#/components/icons/Arrow' 21 + import {Link} from '#/components/Link' 22 + import {Loader} from '#/components/Loader' 23 + import * as Toast from '#/components/Toast' 24 + import {Text} from '#/components/Typography' 25 + import {useAnalytics} from '#/analytics' 26 + import type * as bsky from '#/types/bsky' 27 + 28 + export function GermButton({ 29 + germ, 30 + profile, 31 + }: { 32 + germ: AppBskyActorDefs.ProfileAssociatedGerm 33 + profile: bsky.profile.AnyProfileView 34 + }) { 35 + const t = useTheme() 36 + const ax = useAnalytics() 37 + const {_} = useLingui() 38 + const {currentAccount} = useSession() 39 + const linkWarningControl = Dialog.useDialogControl() 40 + 41 + // exclude `none` and all unknown values 42 + if ( 43 + !(germ.showButtonTo === 'everyone' || germ.showButtonTo === 'usersIFollow') 44 + ) { 45 + return null 46 + } 47 + 48 + if (currentAccount?.did === profile.did) { 49 + return <GermSelfButton did={currentAccount.did} /> 50 + } 51 + 52 + if (germ.showButtonTo === 'usersIFollow' && !profile.viewer?.followedBy) { 53 + return null 54 + } 55 + 56 + const url = constructGermUrl(germ, profile, currentAccount?.did) 57 + 58 + if (!url) { 59 + return null 60 + } 61 + 62 + return ( 63 + <> 64 + <Link 65 + to={url} 66 + onPress={evt => { 67 + ax.metric('profile:associated:germ:click-to-chat', {}) 68 + if (isCustomGermDomain(url)) { 69 + evt.preventDefault() 70 + linkWarningControl.open() 71 + return false 72 + } 73 + }} 74 + label={_(msg`Open Germ DM`)} 75 + overridePresentation={false} 76 + shouldProxy={false} 77 + style={[ 78 + t.atoms.bg_contrast_50, 79 + a.rounded_full, 80 + a.self_start, 81 + {padding: 6}, 82 + ]}> 83 + <GermLogo size="small" /> 84 + <Text style={[a.text_sm, a.font_medium, a.ml_xs]}> 85 + <Trans>Germ DM</Trans> 86 + </Text> 87 + <ArrowTopRightIcon style={[t.atoms.text, a.mx_2xs]} width={14} /> 88 + </Link> 89 + <CustomLinkWarningDialog 90 + control={linkWarningControl} 91 + link={{ 92 + href: url, 93 + displayText: '', 94 + share: false, 95 + }} 96 + /> 97 + </> 98 + ) 99 + } 100 + 101 + function GermLogo({size}: {size: 'small' | 'large'}) { 102 + return ( 103 + <Image 104 + source={require('../../../../assets/images/germ_logo.webp')} 105 + accessibilityIgnoresInvertColors={false} 106 + contentFit="cover" 107 + style={[ 108 + a.rounded_full, 109 + size === 'large' ? {width: 32, height: 32} : {width: 16, height: 16}, 110 + ]} 111 + /> 112 + ) 113 + } 114 + 115 + function GermSelfButton({did}: {did: string}) { 116 + const t = useTheme() 117 + const ax = useAnalytics() 118 + const {_} = useLingui() 119 + const selfExplanationDialogControl = Dialog.useDialogControl() 120 + const agent = useAgent() 121 + const queryClient = useQueryClient() 122 + 123 + const {mutate: deleteDeclaration, isPending} = useMutation({ 124 + mutationFn: async () => { 125 + const previousRecord = await agent.com.germnetwork.declaration 126 + .get({ 127 + repo: did, 128 + rkey: 'self', 129 + }) 130 + .then(res => res.value) 131 + .catch(() => null) 132 + 133 + await agent.com.germnetwork.declaration.delete({ 134 + repo: did, 135 + rkey: 'self', 136 + }) 137 + 138 + await whenAppViewReady(agent, did, res => !res.data.associated?.germ) 139 + 140 + return previousRecord 141 + }, 142 + onSuccess: previousRecord => { 143 + ax.metric('profile:associated:germ:self-disconnect', {}) 144 + 145 + async function undo() { 146 + if (!previousRecord) return 147 + try { 148 + await agent.com.germnetwork.declaration.put( 149 + { 150 + repo: did, 151 + rkey: 'self', 152 + }, 153 + previousRecord, 154 + ) 155 + await whenAppViewReady(agent, did, res => !!res.data.associated?.germ) 156 + await queryClient.refetchQueries({queryKey: RQKEY(did)}) 157 + 158 + Toast.show(_(msg`Germ DM reconnected`)) 159 + ax.metric('profile:associated:germ:self-reconnect', {}) 160 + } catch (e: any) { 161 + Toast.show( 162 + _(msg`Failed to reconnect Germ DM. Error: ${e?.message}`), 163 + { 164 + type: 'error', 165 + }, 166 + ) 167 + if (!isNetworkError(e)) { 168 + ax.logger.error('Failed to reconnect Germ DM link', { 169 + safeMessage: e, 170 + }) 171 + } 172 + } 173 + } 174 + 175 + selfExplanationDialogControl.close(() => { 176 + void queryClient.refetchQueries({queryKey: RQKEY(did)}) 177 + Toast.show( 178 + <Toast.Outer> 179 + <Toast.Icon /> 180 + <Toast.Text> 181 + <Trans>Germ DM disconnected</Trans> 182 + </Toast.Text> 183 + {previousRecord && ( 184 + <Toast.Action label={_(msg`Undo`)} onPress={() => void undo()}> 185 + <Trans>Undo</Trans> 186 + </Toast.Action> 187 + )} 188 + </Toast.Outer>, 189 + ) 190 + }) 191 + }, 192 + onError: error => { 193 + Toast.show( 194 + _(msg`Failed to disconnect Germ DM. Error: ${error?.message}`), 195 + { 196 + type: 'error', 197 + }, 198 + ) 199 + if (!isNetworkError(error)) { 200 + ax.logger.error('Failed to disconnect Germ DM link', { 201 + safeMessage: error, 202 + }) 203 + } 204 + }, 205 + }) 206 + 207 + return ( 208 + <> 209 + <Button 210 + label={_(msg`Learn more about your Germ DM link`)} 211 + onPress={() => { 212 + ax.metric('profile:associated:germ:click-self-info', {}) 213 + selfExplanationDialogControl.open() 214 + }} 215 + style={[ 216 + t.atoms.bg_contrast_50, 217 + a.rounded_full, 218 + a.self_start, 219 + {padding: 6, paddingRight: 10}, 220 + ]}> 221 + <GermLogo size="small" /> 222 + <Text style={[a.text_sm, a.font_medium, a.ml_xs]}> 223 + <Trans>Germ DM</Trans> 224 + </Text> 225 + </Button> 226 + 227 + <Dialog.Outer 228 + control={selfExplanationDialogControl} 229 + nativeOptions={{preventExpansion: true}}> 230 + <Dialog.Handle /> 231 + <Dialog.ScrollableInner 232 + label={_(msg`Germ DM Link`)} 233 + style={web([{maxWidth: 400, borderRadius: 36}])}> 234 + <View style={[a.flex_row, a.align_center, {gap: 6}]}> 235 + <GermLogo size="large" /> 236 + <Text style={[a.text_2xl, a.font_bold]}> 237 + <Trans>Germ DM Link</Trans> 238 + </Text> 239 + </View> 240 + 241 + <Text style={[a.text_md, a.leading_snug, a.mt_sm]}> 242 + <Trans> 243 + This button lets others open the Germ DM app to send you a 244 + message. You can manage its visibility from the Germ DM app, or 245 + you can disconnect your Bluesky account from Germ DM altogether by 246 + clicking the button below. 247 + </Trans> 248 + </Text> 249 + <View style={[a.mt_2xl, a.gap_md]}> 250 + <Button 251 + label={_(msg`Got it`)} 252 + size="large" 253 + color="primary" 254 + onPress={() => selfExplanationDialogControl.close()}> 255 + <ButtonText> 256 + <Trans>Got it</Trans> 257 + </ButtonText> 258 + </Button> 259 + <Button 260 + label={_(msg`Disconnect Germ DM`)} 261 + size="large" 262 + color="secondary" 263 + onPress={() => deleteDeclaration()} 264 + disabled={isPending}> 265 + {isPending && <ButtonIcon icon={Loader} />} 266 + <ButtonText> 267 + <Trans>Disconnect Germ DM</Trans> 268 + </ButtonText> 269 + </Button> 270 + </View> 271 + </Dialog.ScrollableInner> 272 + </Dialog.Outer> 273 + </> 274 + ) 275 + } 276 + 277 + function constructGermUrl( 278 + declaration: AppBskyActorDefs.ProfileAssociatedGerm, 279 + profile: bsky.profile.AnyProfileView, 280 + viewerDid?: string, 281 + ) { 282 + try { 283 + const urlp = new URL(declaration.messageMeUrl) 284 + 285 + if (urlp.pathname.endsWith('/')) { 286 + urlp.pathname = urlp.pathname.slice(0, -1) 287 + } 288 + 289 + urlp.pathname += `/${platform()}` 290 + 291 + if (viewerDid) { 292 + urlp.hash = `#${profile.did}+${viewerDid}` 293 + } else { 294 + urlp.hash = `#${profile.did}` 295 + } 296 + 297 + return urlp.toString() 298 + } catch { 299 + return null 300 + } 301 + } 302 + 303 + function isCustomGermDomain(url: string) { 304 + try { 305 + const urlp = new URL(url) 306 + return urlp.hostname !== 'landing.ger.mx' 307 + } catch { 308 + return false 309 + } 310 + } 311 + 312 + function platform() { 313 + switch (Platform.OS) { 314 + case 'ios': 315 + return 'iOS' 316 + case 'android': 317 + return 'android' 318 + default: 319 + return 'web' 320 + } 321 + } 322 + 323 + async function whenAppViewReady( 324 + agent: AtpAgent, 325 + actor: string, 326 + fn: (res: AppBskyActorGetProfile.Response) => boolean, 327 + ) { 328 + await until( 329 + 5, // 5 tries 330 + 1e3, // 1s delay between tries 331 + fn, 332 + () => agent.app.bsky.actor.getProfile({actor}), 333 + ) 334 + }
+24 -9
src/screens/ProfileList/index.tsx
··· 1 - import {useCallback, useMemo, useRef} from 'react' 1 + import {useCallback, useMemo, useRef, useState} from 'react' 2 2 import {View} from 'react-native' 3 3 import {useAnimatedRef} from 'react-native-reanimated' 4 4 import { ··· 35 35 import {FAB} from '#/view/com/util/fab/FAB' 36 36 import {type ListRef} from '#/view/com/util/List' 37 37 import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' 38 - import {atoms as a, platform} from '#/alf' 38 + import {atoms as a, native, platform, useTheme} from '#/alf' 39 39 import {useDialogControl} from '#/components/Dialog' 40 40 import {ListAddRemoveUsersDialog} from '#/components/dialogs/lists/ListAddRemoveUsersDialog' 41 41 import * as Layout from '#/components/Layout' 42 42 import {Loader} from '#/components/Loader' 43 43 import * as Hider from '#/components/moderation/Hider' 44 + import {IS_WEB} from '#/env' 44 45 import {AboutSection} from './AboutSection' 45 46 import {ErrorScreen} from './components/ErrorScreen' 46 47 import {Header} from './components/Header' ··· 149 150 moderationOpts: ModerationOpts 150 151 preferences: UsePreferencesQueryResponse 151 152 }) { 153 + const t = useTheme() 152 154 const {_} = useLingui() 153 155 const queryClient = useQueryClient() 154 156 const {openComposer} = useOpenComposer() ··· 164 166 const scrollElRef = useAnimatedRef() 165 167 const addUserDialogControl = useDialogControl() 166 168 const sectionTitlesCurate = [_(msg`Posts`), _(msg`People`)] 169 + // modlist only 170 + const [headerHeight, setHeaderHeight] = useState<number | null>(null) 167 171 168 172 const moderation = useMemo(() => { 169 173 return moderateUserList(list, moderationOpts) ··· 263 267 </Hider.Mask> 264 268 <Hider.Content> 265 269 <View style={[a.util_screen_outer]}> 266 - <Layout.Center>{renderHeader()}</Layout.Center> 267 - <AboutSection 268 - list={list} 269 - scrollElRef={scrollElRef as ListRef} 270 - onPressAddUser={addUserDialogControl.open} 271 - headerHeight={0} 272 - /> 270 + <Layout.Center 271 + onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)} 272 + style={[ 273 + native([a.absolute, a.z_10, t.atoms.bg]), 274 + 275 + a.border_b, 276 + t.atoms.border_contrast_low, 277 + ]}> 278 + {renderHeader()} 279 + </Layout.Center> 280 + {headerHeight !== null && ( 281 + <AboutSection 282 + list={list} 283 + scrollElRef={scrollElRef as ListRef} 284 + onPressAddUser={addUserDialogControl.open} 285 + headerHeight={IS_WEB ? 0 : headerHeight} 286 + /> 287 + )} 273 288 <FAB 274 289 testID="composeFAB" 275 290 onPress={() => openComposer({logContext: 'Fab'})}
+2 -12
src/screens/Search/modules/ExploreTrendingTopics.tsx
··· 1 1 import {useMemo} from 'react' 2 2 import {Pressable, View} from 'react-native' 3 3 import {type AppBskyUnspeccedDefs, moderateProfile} from '@atproto/api' 4 - import {msg, plural, Trans} from '@lingui/macro' 4 + import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 7 7 import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' ··· 10 10 import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery' 11 11 import {useTrendingConfig} from '#/state/service-config' 12 12 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 13 - import {formatCount} from '#/view/com/util/numeric/format' 14 13 import {atoms as a, useGutters, useTheme, type ViewStyleProp, web} from '#/alf' 15 14 import {AvatarStack} from '#/components/AvatarStack' 16 15 import {type Props as SVGIconProps} from '#/components/icons/common' ··· 66 65 onPress?: () => void 67 66 }) { 68 67 const t = useTheme() 69 - const {_, i18n} = useLingui() 68 + const {_} = useLingui() 70 69 const gutters = useGutters([0, 'base']) 71 70 72 71 const category = useCategoryDisplayName(trend?.category || 'other') ··· 75 74 (1000 * 60 * 60), 76 75 ) 77 76 const badgeType = trend.status === 'hot' ? 'hot' : age < 2 ? 'new' : age 78 - const postCount = trend.postCount 79 - ? _( 80 - plural(trend.postCount, { 81 - other: `${formatCount(i18n, trend.postCount)} posts`, 82 - }), 83 - ) 84 - : null 85 77 86 78 const actors = useModerateTrendingActors(trend.actors) 87 79 ··· 133 125 web(a.leading_snug), 134 126 ]} 135 127 numberOfLines={1}> 136 - {postCount} 137 - {postCount && category && <> &middot; </>} 138 128 {category} 139 129 </Text> 140 130 </View>
+3 -24
src/state/queries/actor-starter-packs.ts
··· 1 - import { 2 - type AppBskyGraphGetActorStarterPacks, 3 - type AppBskyGraphGetStarterPacksWithMembership, 4 - } from '@atproto/api' 5 - import { 6 - type InfiniteData, 7 - type QueryClient, 8 - type QueryKey, 9 - useInfiniteQuery, 10 - } from '@tanstack/react-query' 1 + import {type QueryClient, useInfiniteQuery} from '@tanstack/react-query' 11 2 12 3 import {useAgent} from '#/state/session' 13 4 ··· 28 19 }) { 29 20 const agent = useAgent() 30 21 31 - return useInfiniteQuery< 32 - AppBskyGraphGetActorStarterPacks.OutputSchema, 33 - Error, 34 - InfiniteData<AppBskyGraphGetActorStarterPacks.OutputSchema>, 35 - QueryKey, 36 - string | undefined 37 - >({ 22 + return useInfiniteQuery({ 38 23 queryKey: RQKEY(did), 39 24 queryFn: async ({pageParam}: {pageParam?: string}) => { 40 25 const res = await agent.app.bsky.graph.getActorStarterPacks({ ··· 59 44 }) { 60 45 const agent = useAgent() 61 46 62 - return useInfiniteQuery< 63 - AppBskyGraphGetStarterPacksWithMembership.OutputSchema, 64 - Error, 65 - InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>, 66 - QueryKey, 67 - string | undefined 68 - >({ 47 + return useInfiniteQuery({ 69 48 queryKey: RQKEY_WITH_MEMBERSHIP(did), 70 49 queryFn: async ({pageParam}: {pageParam?: string}) => { 71 50 const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
+116 -2
src/state/queries/list-memberships.ts
··· 14 14 * -prf 15 15 */ 16 16 17 - import {AtUri} from '@atproto/api' 18 - import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 17 + import { 18 + type AppBskyActorDefs, 19 + type AppBskyGraphGetStarterPacksWithMembership, 20 + AtUri, 21 + } from '@atproto/api' 22 + import { 23 + type InfiniteData, 24 + useMutation, 25 + useQuery, 26 + useQueryClient, 27 + } from '@tanstack/react-query' 19 28 20 29 import {STALE} from '#/state/queries' 21 30 import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members' 22 31 import {useAgent, useSession} from '#/state/session' 32 + import type * as bsky from '#/types/bsky' 33 + import {RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY} from './actor-starter-packs' 23 34 24 35 // sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records 25 36 const SANITY_PAGE_LIMIT = 1000 ··· 91 102 } 92 103 93 104 export function useListMembershipAddMutation({ 105 + subject, 94 106 onSuccess, 95 107 onError, 96 108 }: { 109 + /** 110 + * Needed for optimistic update of starter pack query 111 + */ 112 + subject?: bsky.profile.AnyProfileView 97 113 onSuccess?: (data: {uri: string; cid: string}) => void 98 114 onError?: (error: Error) => void 99 115 } = {}) { ··· 151 167 queryKey: LIST_MEMBERS_RQKEY(variables.listUri), 152 168 }) 153 169 }, 1e3) 170 + 171 + // update WITH_MEMBERSHIPS query 172 + 173 + if (subject) { 174 + queryClient.setQueryData< 175 + InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema> 176 + >(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => { 177 + if (!old) return old 178 + 179 + return { 180 + ...old, 181 + pages: old.pages.map(page => ({ 182 + ...page, 183 + starterPacksWithMembership: page.starterPacksWithMembership.map( 184 + spWithMembership => { 185 + if ( 186 + spWithMembership.starterPack.list && 187 + spWithMembership.starterPack.list?.uri === variables.listUri 188 + ) { 189 + return { 190 + ...spWithMembership, 191 + starterPack: { 192 + ...spWithMembership.starterPack, 193 + listItemsSample: [ 194 + { 195 + uri: data.uri, 196 + subject: subject as AppBskyActorDefs.ProfileView, 197 + }, 198 + ...(spWithMembership.starterPack.listItemsSample?.filter( 199 + item => item.subject.did !== variables.actorDid, 200 + ) ?? []), 201 + ], 202 + list: { 203 + ...spWithMembership.starterPack.list, 204 + listItemCount: 205 + (spWithMembership.starterPack.list.listItemCount ?? 206 + 0) + 1, 207 + }, 208 + }, 209 + listItem: { 210 + uri: data.uri, 211 + subject: subject as AppBskyActorDefs.ProfileView, 212 + }, 213 + } 214 + } 215 + 216 + return spWithMembership 217 + }, 218 + ), 219 + })), 220 + } 221 + }) 222 + } 223 + 154 224 onSuccess?.(data) 155 225 }, 156 226 onError, ··· 206 276 queryKey: LIST_MEMBERS_RQKEY(variables.listUri), 207 277 }) 208 278 }, 1e3) 279 + 280 + queryClient.setQueryData< 281 + InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema> 282 + >(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => { 283 + if (!old) return old 284 + 285 + return { 286 + ...old, 287 + pages: old.pages.map(page => ({ 288 + ...page, 289 + starterPacksWithMembership: page.starterPacksWithMembership.map( 290 + spWithMembership => { 291 + if ( 292 + spWithMembership.starterPack.list && 293 + spWithMembership.starterPack.list.uri === variables.listUri 294 + ) { 295 + return { 296 + ...spWithMembership, 297 + starterPack: { 298 + ...spWithMembership.starterPack, 299 + listItemsSample: 300 + spWithMembership.starterPack.listItemsSample?.filter( 301 + item => item.subject.did !== variables.actorDid, 302 + ), 303 + list: { 304 + ...spWithMembership.starterPack.list, 305 + listItemCount: Math.max( 306 + 0, 307 + (spWithMembership.starterPack.list.listItemCount ?? 308 + 1) - 1, 309 + ), 310 + }, 311 + }, 312 + listItem: undefined, 313 + } 314 + } 315 + 316 + return spWithMembership 317 + }, 318 + ), 319 + })), 320 + } 321 + }) 322 + 209 323 onSuccess?.(data) 210 324 }, 211 325 onError,
+1 -1
src/state/session/index.tsx
··· 430 430 const {signinDialogControl} = useGlobalDialogsControlContext() 431 431 432 432 return useCallback( 433 - (fn: () => void) => { 433 + (fn: () => unknown) => { 434 434 if (hasSession) { 435 435 fn() 436 436 } else {
+109 -31
src/view/com/composer/Composer.tsx
··· 45 45 import * as FileSystem from 'expo-file-system' 46 46 import {type ImagePickerAsset} from 'expo-image-picker' 47 47 import { 48 + AppBskyDraftCreateDraft, 48 49 AppBskyUnspeccedDefs, 49 50 type AppBskyUnspeccedGetPostThreadV2, 50 51 AtUri, ··· 62 63 import {retry} from '#/lib/async/retry' 63 64 import {until} from '#/lib/async/until' 64 65 import { 66 + MAX_DRAFT_GRAPHEME_LENGTH, 65 67 MAX_GRAPHEME_LENGTH, 66 68 SUPPORTED_MIME_TYPES, 67 69 type SupportedMimeTypes, ··· 277 279 ) 278 280 279 281 const thread = composerState.thread 282 + 283 + // Clear error when composer content changes, but only if all posts are 284 + // back within the character limit. 285 + const allPostsWithinLimit = thread.posts.every( 286 + post => post.richtext.graphemeLength <= MAX_DRAFT_GRAPHEME_LENGTH, 287 + ) 288 + 280 289 const activePost = thread.posts[composerState.activePostIndex] 281 290 const nextPost: PostDraft | undefined = 282 291 thread.posts[composerState.activePostIndex + 1] ··· 545 554 revokeAllMediaUrls() 546 555 }, [closeComposer, queryClient]) 547 556 557 + const getDraftSaveError = React.useCallback( 558 + (e: unknown): string => { 559 + if (e instanceof AppBskyDraftCreateDraft.DraftLimitReachedError) { 560 + return _(msg`You've reached the maximum number of drafts`) 561 + } 562 + return _(msg`Failed to save draft`) 563 + }, 564 + [_], 565 + ) 566 + 567 + const validateDraftTextOrError = React.useCallback((): boolean => { 568 + const tooLong = composerState.thread.posts.some( 569 + post => post.richtext.graphemeLength > MAX_DRAFT_GRAPHEME_LENGTH, 570 + ) 571 + if (tooLong) { 572 + setError( 573 + _( 574 + msg`One or more posts are too long to save as a draft. ${plural(MAX_DRAFT_GRAPHEME_LENGTH, {one: 'The maximum number of characters is # character.', other: 'The maximum number of characters is # characters.'})}`, 575 + ), 576 + ) 577 + return false 578 + } 579 + return true 580 + }, [composerState.thread.posts, _]) 581 + 548 582 const handleSaveDraft = React.useCallback(async () => { 583 + setError('') 584 + if (!validateDraftTextOrError()) { 585 + return 586 + } 549 587 const isNewDraft = !composerState.draftId 550 588 try { 551 589 const result = await saveDraft({ ··· 571 609 onClose() 572 610 } catch (e) { 573 611 logger.error('Failed to save draft', {error: e}) 574 - setError(_(msg`Failed to save draft`)) 612 + setError(getDraftSaveError(e)) 575 613 } 576 - }, [saveDraft, composerState, composerDispatch, onClose, _, ax]) 614 + }, [ 615 + saveDraft, 616 + composerState, 617 + composerDispatch, 618 + onClose, 619 + ax, 620 + validateDraftTextOrError, 621 + getDraftSaveError, 622 + ]) 577 623 578 624 // Save without closing - for use by DraftsButton 579 - const saveCurrentDraft = React.useCallback(async () => { 580 - const result = await saveDraft({ 581 - composerState, 582 - existingDraftId: composerState.draftId, 583 - }) 584 - composerDispatch({type: 'mark_saved', draftId: result.draftId}) 585 - }, [saveDraft, composerState, composerDispatch]) 625 + const saveCurrentDraft = React.useCallback(async (): Promise<{ 626 + success: boolean 627 + }> => { 628 + setError('') 629 + if (!validateDraftTextOrError()) { 630 + return {success: false} 631 + } 632 + try { 633 + const result = await saveDraft({ 634 + composerState, 635 + existingDraftId: composerState.draftId, 636 + }) 637 + composerDispatch({type: 'mark_saved', draftId: result.draftId}) 638 + return {success: true} 639 + } catch (e) { 640 + setError(getDraftSaveError(e)) 641 + return {success: false} 642 + } 643 + }, [ 644 + saveDraft, 645 + composerState, 646 + composerDispatch, 647 + validateDraftTextOrError, 648 + getDraftSaveError, 649 + ]) 586 650 587 651 // Handle discard action - fires metric and closes composer 588 652 const handleDiscard = React.useCallback(() => { ··· 1092 1156 isEmpty={isComposerEmpty} 1093 1157 isDirty={composerState.isDirty} 1094 1158 isEditingDraft={!!composerState.draftId} 1159 + canSaveDraft={allPostsWithinLimit} 1095 1160 textLength={thread.posts[0].richtext.text.length}> 1096 1161 {missingAltError && <AltTextReminder error={missingAltError} />} 1097 1162 <ErrorBanner ··· 1158 1223 <Prompt.Outer control={discardPromptControl}> 1159 1224 <Prompt.Content> 1160 1225 <Prompt.TitleText> 1161 - {composerState.draftId ? ( 1162 - <Trans>Save changes?</Trans> 1226 + {allPostsWithinLimit ? ( 1227 + composerState.draftId ? ( 1228 + <Trans>Save changes?</Trans> 1229 + ) : ( 1230 + <Trans>Save draft?</Trans> 1231 + ) 1163 1232 ) : ( 1164 - <Trans>Save draft?</Trans> 1233 + <Trans>Discard post?</Trans> 1165 1234 )} 1166 1235 </Prompt.TitleText> 1167 1236 <Prompt.DescriptionText> 1168 - {composerState.draftId ? ( 1169 - <Trans> 1170 - You have unsaved changes to this draft, would you like to 1171 - save them? 1172 - </Trans> 1237 + {allPostsWithinLimit ? ( 1238 + composerState.draftId ? ( 1239 + <Trans> 1240 + You have unsaved changes to this draft, would you like to 1241 + save them? 1242 + </Trans> 1243 + ) : ( 1244 + <Trans> 1245 + Would you like to save this as a draft to edit later? 1246 + </Trans> 1247 + ) 1173 1248 ) : ( 1174 - <Trans> 1175 - Would you like to save this as a draft to edit later? 1176 - </Trans> 1249 + <Trans>You can only save drafts up to 1000 characters.</Trans> 1177 1250 )} 1178 1251 </Prompt.DescriptionText> 1179 1252 </Prompt.Content> 1180 1253 <Prompt.Actions> 1181 - <Prompt.Action 1182 - cta={ 1183 - composerState.draftId 1184 - ? _(msg`Save changes`) 1185 - : _(msg`Save draft`) 1186 - } 1187 - onPress={handleSaveDraft} 1188 - color="primary" 1189 - /> 1254 + {allPostsWithinLimit && ( 1255 + <Prompt.Action 1256 + cta={ 1257 + composerState.draftId 1258 + ? _(msg`Save changes`) 1259 + : _(msg`Save draft`) 1260 + } 1261 + onPress={handleSaveDraft} 1262 + color="primary" 1263 + /> 1264 + )} 1190 1265 <Prompt.Action 1191 1266 cta={_(msg`Discard`)} 1192 1267 onPress={handleDiscard} 1193 1268 color="negative_subtle" 1194 1269 /> 1195 - <Prompt.Cancel /> 1270 + <Prompt.Cancel cta={_(msg`Keep editing`)} /> 1196 1271 </Prompt.Actions> 1197 1272 </Prompt.Outer> 1198 1273 )} ··· 1422 1497 isEmpty, 1423 1498 isDirty, 1424 1499 isEditingDraft, 1500 + canSaveDraft, 1425 1501 textLength, 1426 1502 topBarAnimatedStyle, 1427 1503 children, ··· 1435 1511 onCancel: () => void 1436 1512 onPublish: () => void 1437 1513 onSelectDraft: (draft: DraftSummary) => void 1438 - onSaveDraft: () => Promise<void> 1514 + onSaveDraft: () => Promise<{success: boolean}> 1439 1515 onDiscard: () => void 1440 1516 isEmpty: boolean 1441 1517 isDirty: boolean 1442 1518 isEditingDraft: boolean 1519 + canSaveDraft: boolean 1443 1520 textLength: number 1444 1521 topBarAnimatedStyle: StyleProp<ViewStyle> 1445 1522 children?: React.ReactNode ··· 1488 1565 isEmpty={isEmpty} 1489 1566 isDirty={isDirty} 1490 1567 isEditingDraft={isEditingDraft} 1568 + canSaveDraft={canSaveDraft} 1491 1569 textLength={textLength} 1492 1570 /> 1493 1571 )}
+1
src/view/com/composer/drafts/DraftItem.tsx
··· 98 98 {!!post.text.trim().length && ( 99 99 <RichText 100 100 style={[a.text_md, a.leading_snug, a.pointer_events_none]} 101 + numberOfLines={8} 101 102 value={post.text} 102 103 enableTags 103 104 disableMentionFacetValidation
+36 -18
src/view/com/composer/drafts/DraftsButton.tsx
··· 18 18 isEmpty, 19 19 isDirty, 20 20 isEditingDraft, 21 + canSaveDraft, 21 22 textLength, 22 23 }: { 23 24 onSelectDraft: (draft: DraftSummary) => void 24 - onSaveDraft: () => Promise<void> 25 + onSaveDraft: () => Promise<{success: boolean}> 25 26 onDiscard: () => void 26 27 isEmpty: boolean 27 28 isDirty: boolean 28 29 isEditingDraft: boolean 30 + canSaveDraft: boolean 29 31 textLength: number 30 32 }) { 31 33 const {_} = useLingui() ··· 46 48 } 47 49 48 50 const handleSaveAndOpen = async () => { 49 - await onSaveDraft() 50 - draftsDialogControl.open() 51 + const {success} = await onSaveDraft() 52 + if (success) { 53 + draftsDialogControl.open() 54 + } 51 55 } 52 56 53 57 const handleDiscardAndOpen = () => { ··· 90 94 <Prompt.Outer control={savePromptControl}> 91 95 <Prompt.Content> 92 96 <Prompt.TitleText> 93 - {isEditingDraft ? ( 94 - <Trans>Save changes?</Trans> 97 + {canSaveDraft ? ( 98 + isEditingDraft ? ( 99 + <Trans>Save changes?</Trans> 100 + ) : ( 101 + <Trans>Save draft?</Trans> 102 + ) 95 103 ) : ( 96 - <Trans>Save draft?</Trans> 104 + <Trans>Discard draft?</Trans> 97 105 )} 98 106 </Prompt.TitleText> 99 107 </Prompt.Content> 100 108 <Prompt.DescriptionText> 101 - {isEditingDraft ? ( 102 - <Trans> 103 - You have unsaved changes. Would you like to save them before 104 - viewing your drafts? 105 - </Trans> 109 + {canSaveDraft ? ( 110 + isEditingDraft ? ( 111 + <Trans> 112 + You have unsaved changes. Would you like to save them before 113 + viewing your drafts? 114 + </Trans> 115 + ) : ( 116 + <Trans> 117 + Would you like to save this as a draft before viewing your 118 + drafts? 119 + </Trans> 120 + ) 106 121 ) : ( 107 122 <Trans> 108 - Would you like to save this as a draft before viewing your drafts? 123 + You can only save drafts up to 1000 characters. Would you like to 124 + discard this post before viewing your drafts? 109 125 </Trans> 110 126 )} 111 127 </Prompt.DescriptionText> 112 128 <Prompt.Actions> 113 - <Prompt.Action 114 - cta={isEditingDraft ? _(msg`Save changes`) : _(msg`Save draft`)} 115 - onPress={handleSaveAndOpen} 116 - color="primary" 117 - /> 129 + {canSaveDraft && ( 130 + <Prompt.Action 131 + cta={isEditingDraft ? _(msg`Save changes`) : _(msg`Save draft`)} 132 + onPress={handleSaveAndOpen} 133 + color="primary" 134 + /> 135 + )} 118 136 <Prompt.Action 119 137 cta={_(msg`Discard`)} 120 138 onPress={handleDiscardAndOpen} 121 139 color="negative_subtle" 122 140 /> 123 - <Prompt.Cancel /> 141 + <Prompt.Cancel cta={_(msg`Keep editing`)} /> 124 142 </Prompt.Actions> 125 143 </Prompt.Outer> 126 144 </>
+9 -6
src/view/com/composer/drafts/DraftsListDialog.tsx
··· 1 1 import {useCallback, useEffect, useMemo} from 'react' 2 - import {View} from 'react-native' 2 + import {Keyboard, View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 6 import {useCallOnce} from '#/lib/once' 7 7 import {EmptyState} from '#/view/com/util/EmptyState' 8 - import {atoms as a, select, useBreakpoints, useTheme, web} from '#/alf' 8 + import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 9 9 import {Button, ButtonText} from '#/components/Button' 10 10 import * as Dialog from '#/components/Dialog' 11 11 import {PageX_Stroke2_Corner0_Rounded_Large as PageXIcon} from '#/components/icons/PageX' ··· 54 54 55 55 const handleSelectDraft = useCallback( 56 56 (summary: DraftSummary) => { 57 + // Dismiss keyboard immediately to prevent flicker. Without this, 58 + // the text input regains focus (showing the keyboard) after the 59 + // drafts sheet closes, then loses it again when the post component 60 + // remounts with the draft content, causing a show-hide-show cycle -sfn 61 + Keyboard.dismiss() 62 + 57 63 control.close(() => { 58 64 onSelectDraft(summary) 59 65 }) ··· 173 179 ListFooterComponent={footerComponent} 174 180 onEndReached={onEndReached} 175 181 onEndReachedThreshold={0.5} 176 - style={[ 177 - a.px_0, 178 - web({minHeight: 500}), 179 - ]} 182 + style={[a.px_0, web({minHeight: 500})]} 180 183 webInnerContentContainerStyle={[a.py_0]} 181 184 contentContainerStyle={[a.pb_xl]} 182 185 />
+11 -9
src/view/com/profile/ProfileMenu.tsx
··· 383 383 )} 384 384 </> 385 385 )} 386 - <Menu.Item 387 - testID="profileHeaderDropdownStarterPackAddRemoveBtn" 388 - label={_(msg`Add to starter packs`)} 389 - onPress={onPressAddToStarterPacks}> 390 - <Menu.ItemText> 391 - <Trans>Add to starter packs</Trans> 392 - </Menu.ItemText> 393 - <Menu.ItemIcon icon={StarterPack} /> 394 - </Menu.Item> 386 + {!isSelf && ( 387 + <Menu.Item 388 + testID="profileHeaderDropdownStarterPackAddRemoveBtn" 389 + label={_(msg`Add to starter packs`)} 390 + onPress={onPressAddToStarterPacks}> 391 + <Menu.ItemText> 392 + <Trans>Add to starter packs</Trans> 393 + </Menu.ItemText> 394 + <Menu.ItemIcon icon={StarterPack} /> 395 + </Menu.Item> 396 + )} 395 397 <Menu.Item 396 398 testID="profileHeaderDropdownListAddRemoveBtn" 397 399 label={_(msg`Add to lists`)}
+27 -27
src/view/screens/Profile.tsx
··· 1 - import React, {useCallback, useMemo} from 'react' 1 + import {useCallback, useEffect, useMemo, useRef, useState} from 'react' 2 2 import {StyleSheet} from 'react-native' 3 3 import {SafeAreaView} from 'react-native-safe-area-context' 4 4 import { ··· 79 79 data: resolvedDid, 80 80 error: resolveError, 81 81 refetch: refetchDid, 82 - isLoading: isLoadingDid, 82 + isPending: isDidPending, 83 83 } = useResolveDidQuery(name) 84 84 const { 85 85 data: profile, 86 86 error: profileError, 87 87 refetch: refetchProfile, 88 - isLoading: isLoadingProfile, 89 88 isPlaceholderData: isPlaceholderProfile, 89 + isPending: isProfilePending, 90 90 } = useProfileQuery({ 91 91 did: resolvedDid, 92 92 }) 93 93 94 - const onPressTryAgain = React.useCallback(() => { 94 + const onPressTryAgain = useCallback(() => { 95 95 if (resolveError) { 96 - refetchDid() 96 + void refetchDid() 97 97 } else { 98 - refetchProfile() 98 + void refetchProfile() 99 99 } 100 100 }, [resolveError, refetchDid, refetchProfile]) 101 101 102 102 // Apply hard-coded redirects as need 103 - React.useEffect(() => { 103 + useEffect(() => { 104 104 if (resolveError) { 105 105 if (name === 'lulaoficial.bsky.social') { 106 106 console.log('Applying redirect to lula.com.br') 107 - navigate('Profile', {name: 'lula.com.br'}) 107 + void navigate('Profile', {name: 'lula.com.br'}) 108 108 } 109 109 } 110 110 }, [name, resolveError]) 111 111 112 112 // When we open the profile, we want to reset the posts query if we are blocked. 113 - React.useEffect(() => { 113 + useEffect(() => { 114 114 if (resolvedDid && profile?.viewer?.blockedBy) { 115 115 resetProfilePostsQueries(queryClient, resolvedDid) 116 116 } 117 117 }, [queryClient, profile?.viewer?.blockedBy, resolvedDid]) 118 118 119 119 // Most pushes will happen here, since we will have only placeholder data 120 - if (isLoadingDid || isLoadingProfile) { 120 + if (isDidPending || isProfilePending) { 121 121 return ( 122 122 <Layout.Content> 123 123 <ProfileHeaderLoading /> ··· 186 186 did: profile.did, 187 187 enabled: !!profile.associated?.labeler, 188 188 }) 189 - const [currentPage, setCurrentPage] = React.useState(0) 189 + const [currentPage, setCurrentPage] = useState(0) 190 190 const {_} = useLingui() 191 191 192 - const [scrollViewTag, setScrollViewTag] = React.useState<number | null>(null) 192 + const [scrollViewTag, setScrollViewTag] = useState<number | null>(null) 193 193 194 - const postsSectionRef = React.useRef<SectionRef>(null) 195 - const repliesSectionRef = React.useRef<SectionRef>(null) 196 - const mediaSectionRef = React.useRef<SectionRef>(null) 197 - const videosSectionRef = React.useRef<SectionRef>(null) 198 - const likesSectionRef = React.useRef<SectionRef>(null) 199 - const feedsSectionRef = React.useRef<SectionRef>(null) 200 - const listsSectionRef = React.useRef<SectionRef>(null) 201 - const starterPacksSectionRef = React.useRef<SectionRef>(null) 202 - const labelsSectionRef = React.useRef<SectionRef>(null) 194 + const postsSectionRef = useRef<SectionRef>(null) 195 + const repliesSectionRef = useRef<SectionRef>(null) 196 + const mediaSectionRef = useRef<SectionRef>(null) 197 + const videosSectionRef = useRef<SectionRef>(null) 198 + const likesSectionRef = useRef<SectionRef>(null) 199 + const feedsSectionRef = useRef<SectionRef>(null) 200 + const listsSectionRef = useRef<SectionRef>(null) 201 + const starterPacksSectionRef = useRef<SectionRef>(null) 202 + const labelsSectionRef = useRef<SectionRef>(null) 203 203 204 204 useSetTitle(combinedDisplayName(profile)) 205 205 ··· 315 315 ) 316 316 317 317 useFocusEffect( 318 - React.useCallback(() => { 318 + useCallback(() => { 319 319 setMinimalShellMode(false) 320 320 return listenSoftReset(() => { 321 321 scrollSectionToTop(currentPage) ··· 601 601 602 602 function useRichText(text: string): [RichTextAPI, boolean] { 603 603 const agent = useAgent() 604 - const [prevText, setPrevText] = React.useState(text) 605 - const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text})) 606 - const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null) 604 + const [prevText, setPrevText] = useState(text) 605 + const [rawRT, setRawRT] = useState(() => new RichTextAPI({text})) 606 + const [resolvedRT, setResolvedRT] = useState<RichTextAPI | null>(null) 607 607 if (text !== prevText) { 608 608 setPrevText(text) 609 609 setRawRT(new RichTextAPI({text})) 610 610 setResolvedRT(null) 611 611 // This will queue an immediate re-render 612 612 } 613 - React.useEffect(() => { 613 + useEffect(() => { 614 614 let ignore = false 615 615 async function resolveRTFacets() { 616 616 // new each time ··· 620 620 setResolvedRT(resolvedRT) 621 621 } 622 622 } 623 - resolveRTFacets() 623 + void resolveRTFacets() 624 624 return () => { 625 625 ignore = true 626 626 }
+4 -11
web/index.html
··· 149 149 </noscript> 150 150 151 151 <!-- The root element for your Expo app. --> 152 + <div id="splash"> 153 + <!-- Witchsky SVG --> 154 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#ED5345" d="M374.473 57.7173C367.666 50.7995 357.119 49.1209 348.441 53.1659C347.173 53.7567 342.223 56.0864 334.796 59.8613C326.32 64.1696 314.568 70.3869 301.394 78.0596C275.444 93.1728 242.399 114.83 218.408 139.477C185.983 172.786 158.719 225.503 140.029 267.661C130.506 289.144 122.878 308.661 117.629 322.81C116.301 326.389 115.124 329.63 114.104 332.478C87.1783 336.42 64.534 341.641 47.5078 348.101C37.6493 351.84 28.3222 356.491 21.0573 362.538C13.8818 368.511 6.00003 378.262 6.00003 391.822C6.00014 403.222 11.8738 411.777 17.4566 417.235C23.0009 422.655 29.9593 426.793 36.871 430.062C50.8097 436.653 69.5275 441.988 90.8362 446.249C133.828 454.846 192.21 460 256.001 460C319.79 460 378.172 454.846 421.164 446.249C442.472 441.988 461.19 436.653 475.129 430.062C482.041 426.793 488.999 422.655 494.543 417.235C500.039 411.862 505.817 403.489 505.996 392.353L506 391.822L505.995 391.188C505.754 377.959 498.012 368.417 490.945 362.534C483.679 356.485 474.35 351.835 464.491 348.095C446.749 341.366 422.906 335.982 394.476 331.987C393.6 330.57 392.633 328.995 391.595 327.273C386.477 318.777 379.633 306.842 372.737 293.115C358.503 264.781 345.757 232.098 344.756 206.636C343.87 184.121 351.638 154.087 360.819 127.789C365.27 115.041 369.795 103.877 373.207 95.9072C374.909 91.9309 376.325 88.7712 377.302 86.6328C377.79 85.5645 378.167 84.7524 378.416 84.2224C378.54 83.9579 378.632 83.7635 378.69 83.643C378.718 83.5829 378.739 83.5411 378.75 83.5181C378.753 83.5108 378.756 83.5049 378.757 83.5015C382.909 74.8634 381.196 64.5488 374.473 57.7173Z"/></svg> 155 + </div> 152 156 <div id="root"> 153 - <div id="splash"> 154 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> 155 - 156 - <defs> 157 - <linearGradient x1="0" y1="0" x2="0" y2="1" id="index_sky"><stop offset="0" stop-color="#ED5345" stop-opacity="1"></stop><stop offset="1" stop-color="#E25C50" stop-opacity="1"></stop></linearGradient> 158 - </defs> 159 - <path fill="url(#index_sky)" 160 - d="M374.473 57.7173C367.666 50.7995 357.119 49.1209 348.441 53.1659C347.173 53.7567 342.223 56.0864 334.796 59.8613C326.32 64.1696 314.568 70.3869 301.394 78.0596C275.444 93.1728 242.399 114.83 218.408 139.477C185.983 172.786 158.719 225.503 140.029 267.661C130.506 289.144 122.878 308.661 117.629 322.81C116.301 326.389 115.124 329.63 114.104 332.478C87.1783 336.42 64.534 341.641 47.5078 348.101C37.6493 351.84 28.3222 356.491 21.0573 362.538C13.8818 368.511 6.00003 378.262 6.00003 391.822C6.00014 403.222 11.8738 411.777 17.4566 417.235C23.0009 422.655 29.9593 426.793 36.871 430.062C50.8097 436.653 69.5275 441.988 90.8362 446.249C133.828 454.846 192.21 460 256.001 460C319.79 460 378.172 454.846 421.164 446.249C442.472 441.988 461.19 436.653 475.129 430.062C482.041 426.793 488.999 422.655 494.543 417.235C500.039 411.862 505.817 403.489 505.996 392.353L506 391.822L505.995 391.188C505.754 377.959 498.012 368.417 490.945 362.534C483.679 356.485 474.35 351.835 464.491 348.095C446.749 341.366 422.906 335.982 394.476 331.987C393.6 330.57 392.633 328.995 391.595 327.273C386.477 318.777 379.633 306.842 372.737 293.115C358.503 264.781 345.757 232.098 344.756 206.636C343.87 184.121 351.638 154.087 360.819 127.789C365.27 115.041 369.795 103.877 373.207 95.9072C374.909 91.9309 376.325 88.7712 377.302 86.6328C377.79 85.5645 378.167 84.7524 378.416 84.2224C378.54 83.9579 378.632 83.7635 378.69 83.643C378.718 83.5829 378.739 83.5411 378.75 83.5181C378.753 83.5108 378.756 83.5049 378.757 83.5015C382.909 74.8634 381.196 64.5488 374.473 57.7173Z" 161 - /> 162 - </svg> 163 - </div> 164 157 </div> 165 158 </body> 166 159 </html>
+160 -115
yarn.lock
··· 82 82 "@atproto/xrpc" "^0.7.6" 83 83 "@atproto/xrpc-server" "^0.10.0" 84 84 85 - "@atproto/api@^0.18.18": 86 - version "0.18.18" 87 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.18.tgz#0c02585ea1b0f4a37444b0d8dfbe347c99ea5f64" 88 - integrity sha512-Vg7/sjbwDQZDj8fXtb4E48U4gA+6RC1iSt7onGnH2NyR0E25uds1KnqSKMzqcphdJXrz5GXrgHWc747XPGibzg== 85 + "@atproto/api@^0.18.20": 86 + version "0.18.20" 87 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.20.tgz#3fdbb7b7fae90bd59101970c2b56cc31e8cf417d" 88 + integrity sha512-BZYZkh2VJIFCXEnc/vzKwAwWjAQQTgbNJ8FBxpBK+z+KYh99O0uPCsRYKoCQsRrnkgrhzdU9+g2G+7zanTIGbw== 89 89 dependencies: 90 - "@atproto/common-web" "^0.4.14" 90 + "@atproto/common-web" "^0.4.15" 91 91 "@atproto/lexicon" "^0.6.1" 92 92 "@atproto/syntax" "^0.4.3" 93 93 "@atproto/xrpc" "^0.7.7" ··· 96 96 tlds "^1.234.0" 97 97 zod "^3.23.8" 98 98 99 - "@atproto/api@^0.18.19", "@atproto/api@^0.18.20": 100 - version "0.18.20" 101 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.20.tgz#3fdbb7b7fae90bd59101970c2b56cc31e8cf417d" 102 - integrity sha512-BZYZkh2VJIFCXEnc/vzKwAwWjAQQTgbNJ8FBxpBK+z+KYh99O0uPCsRYKoCQsRrnkgrhzdU9+g2G+7zanTIGbw== 99 + "@atproto/api@^0.18.21": 100 + version "0.18.21" 101 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.21.tgz#19dbea422de22e7f5253f1c5475e3588c3a9d4a0" 102 + integrity sha512-s35MIJerGT/pKe2xJtKKswqlIr/ola2r2iURBKBL0Mk1OKe6jP4YvTMh1N2d2PEANFzNNTbKoDaLfJPo2Uvc/w== 103 103 dependencies: 104 - "@atproto/common-web" "^0.4.15" 104 + "@atproto/common-web" "^0.4.16" 105 105 "@atproto/lexicon" "^0.6.1" 106 106 "@atproto/syntax" "^0.4.3" 107 107 "@atproto/xrpc" "^0.7.7" ··· 128 128 multiformats "^9.9.0" 129 129 uint8arrays "3.0.0" 130 130 131 - "@atproto/bsky@^0.0.214": 132 - version "0.0.214" 133 - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.214.tgz#54183957e323fe0800606acdcc3137acadf6672e" 134 - integrity sha512-Fn+o3WcaX57EmcLoUdcViY4wU2UzfbyNlS/qFQu+1clADtL3pthylwTJgwUU73cZE+7MILZPfTzhQTc15snR9A== 131 + "@atproto/bsky@^0.0.215": 132 + version "0.0.215" 133 + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.215.tgz#df566ccc7bc77d27dd2f9506c96739352b395cef" 134 + integrity sha512-1X9G4L8boh9naCtXzorFYm8VskE1xLOJslthR16Hd4uH+ryOEVxN0BLk16OP86qy00hU6VNvyGkDqBthUyy62w== 135 135 dependencies: 136 136 "@atproto-labs/fetch-node" "^0.2.0" 137 137 "@atproto-labs/xrpc-utils" "^0.0.24" 138 - "@atproto/api" "^0.18.20" 139 - "@atproto/common" "^0.5.10" 138 + "@atproto/api" "^0.18.21" 139 + "@atproto/common" "^0.5.11" 140 140 "@atproto/crypto" "^0.4.5" 141 141 "@atproto/did" "^0.3.0" 142 - "@atproto/identity" "^0.4.10" 142 + "@atproto/identity" "^0.4.11" 143 143 "@atproto/lexicon" "^0.6.1" 144 144 "@atproto/repo" "^0.8.12" 145 145 "@atproto/sync" "^0.1.39" 146 146 "@atproto/syntax" "^0.4.3" 147 - "@atproto/xrpc-server" "^0.10.11" 147 + "@atproto/xrpc-server" "^0.10.12" 148 148 "@bufbuild/protobuf" "^1.5.0" 149 149 "@connectrpc/connect" "^1.1.4" 150 150 "@connectrpc/connect-express" "^1.1.4" ··· 194 194 pino-http "^8.2.1" 195 195 typed-emitter "^2.1.0" 196 196 197 - "@atproto/common-web@^0.4.13", "@atproto/common-web@^0.4.14", "@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.7": 197 + "@atproto/common-web@^0.4.13", "@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.7": 198 198 version "0.4.14" 199 199 resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.14.tgz#77545d454add08d735af00ffc29dd6abf1ab77b3" 200 200 integrity sha512-rMU8Q+kpyPpirUS9OqT7aOD1hxKa+diem3vc7BA0lOkj0tU6wcAxegxmbPZ8JaOsR7SSYhP/jCt/5wbT4qqkuQ== ··· 214 214 "@atproto/syntax" "^0.4.3" 215 215 zod "^3.23.8" 216 216 217 + "@atproto/common-web@^0.4.16": 218 + version "0.4.16" 219 + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.16.tgz#1bee6073ee7a102ea6c47a512a8f6992c683012e" 220 + integrity sha512-Ufvaff5JgxUyUyTAG0/3o7ltpy3lnZ1DvLjyAnvAf+hHfiK7OMQg+8byr+orN+KP9MtIQaRTsCgYPX+PxMKUoA== 221 + dependencies: 222 + "@atproto/lex-data" "^0.0.11" 223 + "@atproto/lex-json" "^0.0.11" 224 + "@atproto/syntax" "^0.4.3" 225 + zod "^3.23.8" 226 + 217 227 "@atproto/common@0.1.0": 218 228 version "0.1.0" 219 229 resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210" ··· 246 256 multiformats "^9.9.0" 247 257 pino "^8.21.0" 248 258 249 - "@atproto/common@^0.5.10": 250 - version "0.5.10" 251 - resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.5.10.tgz#f0d6b7e012b3dd3991d7e2975e26913d4f41e90f" 252 - integrity sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g== 259 + "@atproto/common@^0.5.11": 260 + version "0.5.11" 261 + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.5.11.tgz#88499978d3b29c451e7ad9a71f9d2f89446e89f6" 262 + integrity sha512-WRlT4s+wv80WdQuzkQub9D5vTD82O8dH2p91u4b+x3O17q5IQbmA3Lj+1NICINNSy2voqloqAWdqXEkRfdlAPw== 253 263 dependencies: 254 - "@atproto/common-web" "^0.4.15" 255 - "@atproto/lex-cbor" "^0.0.10" 256 - "@atproto/lex-data" "^0.0.10" 264 + "@atproto/common-web" "^0.4.16" 265 + "@atproto/lex-cbor" "^0.0.11" 266 + "@atproto/lex-data" "^0.0.11" 257 267 iso-datestring-validator "^2.2.2" 258 268 multiformats "^9.9.0" 259 269 pino "^8.21.0" ··· 278 288 "@noble/hashes" "^1.6.1" 279 289 uint8arrays "3.0.0" 280 290 281 - "@atproto/dev-env@^0.3.208": 282 - version "0.3.208" 283 - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.208.tgz#01d07d4b1ef2404adb07bdddf324393989231a52" 284 - integrity sha512-ttx+MIT21iFICW7sZNaFJoCPlxZAqgFy1ztOEn1ebW2MkJzsNHSq2oQC8MHxmpbYEOMPekTXC2iMHW6PYVQJxQ== 291 + "@atproto/dev-env@^0.3.209": 292 + version "0.3.209" 293 + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.209.tgz#0df234a7feab7628ad376773de0e8bd612fb7f2a" 294 + integrity sha512-i4IVqoyCAqULNeye9xpo5YVfrNjFQjGwAJZ4QAA26L9LuFlXmudnAX17KOCMvmJiPgtRe2ofIy6h+ZJ6qm52tQ== 285 295 dependencies: 286 - "@atproto/api" "^0.18.20" 287 - "@atproto/bsky" "^0.0.214" 296 + "@atproto/api" "^0.18.21" 297 + "@atproto/bsky" "^0.0.215" 288 298 "@atproto/bsync" "^0.0.23" 289 - "@atproto/common-web" "^0.4.15" 299 + "@atproto/common-web" "^0.4.16" 290 300 "@atproto/crypto" "^0.4.5" 291 - "@atproto/identity" "^0.4.10" 301 + "@atproto/identity" "^0.4.11" 292 302 "@atproto/lexicon" "^0.6.1" 293 - "@atproto/ozone" "^0.1.162" 294 - "@atproto/pds" "^0.4.207" 303 + "@atproto/ozone" "^0.1.163" 304 + "@atproto/pds" "^0.4.209" 295 305 "@atproto/sync" "^0.1.39" 296 306 "@atproto/syntax" "^0.4.3" 297 - "@atproto/xrpc-server" "^0.10.11" 307 + "@atproto/xrpc-server" "^0.10.12" 298 308 "@did-plc/lib" "^0.0.1" 299 309 "@did-plc/server" "^0.0.1" 300 310 dotenv "^16.0.3" ··· 319 329 "@atproto/common-web" "^0.4.4" 320 330 "@atproto/crypto" "^0.4.4" 321 331 332 + "@atproto/identity@^0.4.11": 333 + version "0.4.11" 334 + resolved "https://registry.yarnpkg.com/@atproto/identity/-/identity-0.4.11.tgz#9622496499919cb2c05d21ab1ba45132dd1e3b52" 335 + integrity sha512-/xXPPojR0PRD0pqoEPmgKMeclUCrkaMKfFGFkssAmXSuT39aCtiibxNqvhp+S2jOdeM6rKrs2QgH91OCGvwPcg== 336 + dependencies: 337 + "@atproto/common-web" "^0.4.16" 338 + "@atproto/crypto" "^0.4.5" 339 + 322 340 "@atproto/jwk-jose@^0.1.11": 323 341 version "0.1.11" 324 342 resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.11.tgz#ef64bce940a66e267fc3cf0db8df4dbd062bb28a" ··· 343 361 "@atproto/lex-data" "0.0.9" 344 362 tslib "^2.8.1" 345 363 346 - "@atproto/lex-cbor@^0.0.10": 347 - version "0.0.10" 348 - resolved "https://registry.yarnpkg.com/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz#712abc4fedef4854c341e32d0fd1608e45616984" 349 - integrity sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA== 364 + "@atproto/lex-cbor@^0.0.11": 365 + version "0.0.11" 366 + resolved "https://registry.yarnpkg.com/@atproto/lex-cbor/-/lex-cbor-0.0.11.tgz#7a464a2032eb61a1dfc254bae1e2759c8279d6a6" 367 + integrity sha512-A7ETtPsEsJ/VuPJOFw4bPNTKxHvFN1JbTQ2NjLuisd3ry7fVxgMpo/qGXsUQsAh/I/uziGbhpNqdS6GnI2p/Wg== 350 368 dependencies: 351 - "@atproto/lex-data" "^0.0.10" 369 + "@atproto/lex-data" "^0.0.11" 352 370 tslib "^2.8.1" 353 371 354 - "@atproto/lex-client@^0.0.11": 355 - version "0.0.11" 356 - resolved "https://registry.yarnpkg.com/@atproto/lex-client/-/lex-client-0.0.11.tgz#b8e8d0da81ed27f1093d0a1e1c7fdb994394e323" 357 - integrity sha512-2DCidAlhATtZc1Z11PUd+C98BiW/Od4pWtDlQSAxkjHSC/56ZwuSkZQVx27ISk1HldfKVc9qUvQWA9nhmrxYIw== 372 + "@atproto/lex-client@^0.0.12": 373 + version "0.0.12" 374 + resolved "https://registry.yarnpkg.com/@atproto/lex-client/-/lex-client-0.0.12.tgz#87be8075746e32dafc3d23f282e53daadb50ea66" 375 + integrity sha512-ef4jQQ7SOtBsXr+Gf1UHuBfCiAGYZxO5PCCXl3eT4ObO83SROtIf7pyO06jBQI/IZChSVsXqXsgakR0aru6lYQ== 358 376 dependencies: 359 - "@atproto/lex-data" "^0.0.10" 360 - "@atproto/lex-json" "^0.0.10" 361 - "@atproto/lex-schema" "^0.0.11" 377 + "@atproto/lex-data" "^0.0.11" 378 + "@atproto/lex-json" "^0.0.11" 379 + "@atproto/lex-schema" "^0.0.12" 362 380 tslib "^2.8.1" 363 381 364 382 "@atproto/lex-data@0.0.9": ··· 381 399 uint8arrays "3.0.0" 382 400 unicode-segmenter "^0.14.0" 383 401 384 - "@atproto/lex-document@^0.0.12": 385 - version "0.0.12" 386 - resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.12.tgz#18e0ca344101bb742a2930f663473e0213721f52" 387 - integrity sha512-+no+ZXyCNdOdjkOj6a4n4WHAQzZz3M6VJTwx7IQQC2+to41/4fFr6k8U1y3Jtq2lYcbHqapkJj3RGIxzFcrtwA== 402 + "@atproto/lex-data@^0.0.11": 403 + version "0.0.11" 404 + resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.11.tgz#de98894a80ff907f9cd6c3faa561d3a16e21dd8d" 405 + integrity sha512-4+KTtHdqwlhiTKA7D4SACea4jprsNpCQsNALW09wsZ6IHhCDGO5tr1cmV+QnLYe3G3mu1E1yXHXbPUHrUUDT/A== 388 406 dependencies: 389 - "@atproto/lex-schema" "^0.0.11" 407 + multiformats "^9.9.0" 408 + tslib "^2.8.1" 409 + uint8arrays "3.0.0" 410 + unicode-segmenter "^0.14.0" 411 + 412 + "@atproto/lex-document@^0.0.13": 413 + version "0.0.13" 414 + resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.13.tgz#fcf794addf6cb328c78d4393e91ec0050a43c623" 415 + integrity sha512-LWsBsKIbyuG7jFObTtnCFQNYHxWWVpVVspqv6UtnS/QsaCyCMg1GIz5vlgi8QBnmGvaPiQxIzGt6mERpTvEXpg== 416 + dependencies: 417 + "@atproto/lex-schema" "^0.0.12" 390 418 core-js "^3" 391 419 tslib "^2.8.1" 392 420 ··· 406 434 "@atproto/lex-data" "^0.0.10" 407 435 tslib "^2.8.1" 408 436 409 - "@atproto/lex-resolver@^0.0.13": 410 - version "0.0.13" 411 - resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.13.tgz#6407b813001d3a3f9e4048197755deccb3cc6f62" 412 - integrity sha512-CcqCE6W3ZVMVzAihatpVbXLxO15mtvddzRzovIJ9QxBTpUmtkdmIk9/gle0oAsRToTJYQy2a6dwAmnAosxP/XQ== 437 + "@atproto/lex-json@^0.0.11": 438 + version "0.0.11" 439 + resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.11.tgz#56be96a4d2366113da3911020cedfb0fb97fbb4f" 440 + integrity sha512-2IExAoQ4KsR5fyPa1JjIvtR316PvdgRH/l3BVGLBd3cSxM3m5MftIv1B6qZ9HjNiK60SgkWp0mi9574bTNDhBQ== 441 + dependencies: 442 + "@atproto/lex-data" "^0.0.11" 443 + tslib "^2.8.1" 444 + 445 + "@atproto/lex-resolver@^0.0.14": 446 + version "0.0.14" 447 + resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.14.tgz#8bc32e49c231bc29ca0d1ea927b33d7a21447de3" 448 + integrity sha512-jBkYRmMKap2OM1zm0VDvs7Heuf3pGjw9xJEHQx1ohkMmM5f+cHPS40RQ8x8SNE+Vl9gMuOrgmgKyPDIuYSIBTw== 413 449 dependencies: 414 450 "@atproto-labs/did-resolver" "^0.2.6" 415 451 "@atproto/crypto" "^0.4.5" 416 - "@atproto/lex-client" "^0.0.11" 417 - "@atproto/lex-data" "^0.0.10" 418 - "@atproto/lex-document" "^0.0.12" 419 - "@atproto/lex-schema" "^0.0.11" 452 + "@atproto/lex-client" "^0.0.12" 453 + "@atproto/lex-data" "^0.0.11" 454 + "@atproto/lex-document" "^0.0.13" 455 + "@atproto/lex-schema" "^0.0.12" 420 456 "@atproto/repo" "^0.8.12" 421 457 "@atproto/syntax" "^0.4.3" 422 458 tslib "^2.8.1" 423 459 424 - "@atproto/lex-schema@^0.0.11": 425 - version "0.0.11" 426 - resolved "https://registry.yarnpkg.com/@atproto/lex-schema/-/lex-schema-0.0.11.tgz#7ff8a1e94971cb750cacaa40e30f546a64db0acd" 427 - integrity sha512-1vLUPQIMeawKP6ehSx2RiqaJDkiseFTXyUk3C4PaoFCktaH8FgVgPVKgUSSy02m1pxVMKnsOBV5psKeN53HG+Q== 460 + "@atproto/lex-schema@^0.0.12": 461 + version "0.0.12" 462 + resolved "https://registry.yarnpkg.com/@atproto/lex-schema/-/lex-schema-0.0.12.tgz#3519b1e4040503e1e6e5a47df1012660f4c38d8c" 463 + integrity sha512-l1RNYmqNwIEjgMEjC9i2o6FLsUFdpAc610xQYK/CRxN31cRzY0lAJ2GFbp2GZ4rRAD3FGYCXid6gZ42KsieUcw== 428 464 dependencies: 429 - "@atproto/lex-data" "^0.0.10" 465 + "@atproto/lex-data" "^0.0.11" 430 466 "@atproto/syntax" "^0.4.3" 431 467 tslib "^2.8.1" 432 468 ··· 441 477 multiformats "^9.9.0" 442 478 zod "^3.23.8" 443 479 444 - "@atproto/oauth-provider-api@0.3.7", "@atproto/oauth-provider-api@^0.3.7": 480 + "@atproto/oauth-provider-api@0.3.7": 445 481 version "0.3.7" 446 482 resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.3.7.tgz#7b911256536a72dbba6f38081a200836a3ab50b8" 447 483 integrity sha512-7yU9vuQFt/hy4NzlDtn+LuhIGvVKkhgWAkCmopnseMPBw6oGPT90uOsTxMkVGtHuKVvBSz7hOXoELXpnZq3gDQ== ··· 449 485 "@atproto/jwk" "0.6.0" 450 486 "@atproto/oauth-types" "0.6.2" 451 487 452 - "@atproto/oauth-provider-frontend@^0.2.8": 453 - version "0.2.8" 454 - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.2.8.tgz#97f6dc33257b0f846229839dd74f606528f49846" 455 - integrity sha512-wHypQrsbwE6LUlyDADfiJfOH5pDAHWm4l/v1dwkQhRndOai7L+knsbdLAOZVXuz6bRs7oV2Frb3mSS03OS4Gdw== 488 + "@atproto/oauth-provider-frontend@0.2.9": 489 + version "0.2.9" 490 + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.2.9.tgz#68486c0d9710a1325a02a303a6b22c733239b219" 491 + integrity sha512-m1NhqC7ydEpF5ELV13CxkgCawyMX8wbAXls/4af3Tsz3IOkdM6zPgQeYK7Zj+7Nywjml4mZTkJ8rZiT6O9+5sw== 456 492 optionalDependencies: 457 493 "@atproto/oauth-provider-api" "0.3.7" 458 494 459 - "@atproto/oauth-provider-ui@^0.4.2": 460 - version "0.4.2" 461 - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.4.2.tgz#4edd41c71fd1bf5aaa1df219fb8a20c0b8d90738" 462 - integrity sha512-j3Afu23JYy68GY8L4t/cTEZz1PnrvxLrjiG/nPhXPURZbCnqZ1puIU+HRVCBJfqKyCwaWthXgUjwaL+SFlPGKA== 495 + "@atproto/oauth-provider-ui@0.4.3": 496 + version "0.4.3" 497 + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.4.3.tgz#c80b076c2d2a7cc45e45866796f09b959bcf1c03" 498 + integrity sha512-BLNZmtOwoHu2qk/Oi6dUR8TcXQaJre6wgW8YjkW5bKf+Vftn3PGzh8bKgr1fQZDYweZ6AF148imA2OzVSAaLHQ== 463 499 optionalDependencies: 464 500 "@atproto/oauth-provider-api" "0.3.7" 465 501 466 - "@atproto/oauth-provider@^0.15.7": 467 - version "0.15.7" 468 - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.15.7.tgz#fc5bd420984dac60f726949d2473bbdb3ddff991" 469 - integrity sha512-rsPez44TJGVC5vFdwSTfSOLIIG5O1JF8NXLe1BjjsUbzRuflN6TyM6HyfBJVsnOVF2bOq6W6xPcnBFUJHCSyUQ== 502 + "@atproto/oauth-provider@^0.15.9": 503 + version "0.15.9" 504 + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.15.9.tgz#3ec21380711e34f4303a0782ecfffffb8080f144" 505 + integrity sha512-nuyOVw5piS/iEk7xKEtdulRbcFZ7HBB0gPXL62z5IpxmluEvLem/VLqsntj2OK/mtP6xhnIfFHP5pb5+PnLuYQ== 470 506 dependencies: 471 507 "@atproto-labs/fetch" "^0.2.3" 472 508 "@atproto-labs/fetch-node" "^0.2.0" 473 509 "@atproto-labs/pipe" "^0.1.1" 474 510 "@atproto-labs/simple-store" "^0.3.0" 475 511 "@atproto-labs/simple-store-memory" "^0.1.4" 476 - "@atproto/common" "^0.5.10" 512 + "@atproto/common" "^0.5.11" 477 513 "@atproto/did" "^0.3.0" 478 514 "@atproto/jwk" "^0.6.0" 479 515 "@atproto/jwk-jose" "^0.1.11" 480 - "@atproto/lex-document" "^0.0.12" 481 - "@atproto/lex-resolver" "^0.0.13" 482 - "@atproto/oauth-provider-api" "^0.3.7" 483 - "@atproto/oauth-provider-frontend" "^0.2.8" 484 - "@atproto/oauth-provider-ui" "^0.4.2" 516 + "@atproto/lex-document" "^0.0.13" 517 + "@atproto/lex-resolver" "^0.0.14" 518 + "@atproto/oauth-provider-api" "0.3.7" 519 + "@atproto/oauth-provider-frontend" "0.2.9" 520 + "@atproto/oauth-provider-ui" "0.4.3" 485 521 "@atproto/oauth-scopes" "^0.3.1" 486 - "@atproto/oauth-types" "^0.6.2" 522 + "@atproto/oauth-types" "^0.6.3" 487 523 "@atproto/syntax" "^0.4.3" 488 524 "@hapi/accept" "^6.0.3" 489 525 "@hapi/address" "^5.1.1" ··· 505 541 "@atproto/did" "^0.3.0" 506 542 "@atproto/syntax" "^0.4.3" 507 543 508 - "@atproto/oauth-types@0.6.2", "@atproto/oauth-types@^0.6.2": 544 + "@atproto/oauth-types@0.6.2": 509 545 version "0.6.2" 510 546 resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.6.2.tgz#d829fae63421dcea7ac703e84460175d2f8d9299" 511 547 integrity sha512-2cuboM4RQBCYR8NQC5uGRkW6KgCgKyq/B5/+tnMmWZYtZGVUQvsUWQHK/ZiMCnVXbcDNtc/RIEJQJDZ8FXMoxg== ··· 514 550 "@atproto/jwk" "0.6.0" 515 551 zod "^3.23.8" 516 552 517 - "@atproto/ozone@^0.1.162": 518 - version "0.1.162" 519 - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.162.tgz#2c1775c1c45fb03b96087937c7675f7f1825c429" 520 - integrity sha512-UeSbHJ4qHIrWcRelifPm4BFh8PFN7DG9tpKuXSqFjZFG3hO2ZDyBzxV6e9zh7Qg+m2tibdst4Hrk4vyT/7wO6Q== 553 + "@atproto/oauth-types@^0.6.3": 554 + version "0.6.3" 555 + resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.6.3.tgz#4fc996d0af61874830079d1b1bddc7c0774ad6b2" 556 + integrity sha512-jdKuoPknJuh/WjI+mYk7agSbx9mNVMbS6Dr3k1z2YMY2oRiCQjxYBuo4MLKATbxj05nMQaZRWlHRUazoAu5Cng== 521 557 dependencies: 522 - "@atproto/api" "^0.18.18" 523 - "@atproto/common" "^0.5.9" 558 + "@atproto/did" "^0.3.0" 559 + "@atproto/jwk" "^0.6.0" 560 + zod "^3.23.8" 561 + 562 + "@atproto/ozone@^0.1.163": 563 + version "0.1.163" 564 + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.163.tgz#7935f9180b6c7f3bd05043d42e14b6055c268596" 565 + integrity sha512-Rz5p0Bf9K3KVo6VuPNe5KTiuJVM5eV7fXSIeWidC1WbDgfEEM8jJu2YRQHvDQq5r95SKKY0gTFYqU6vW0DKSJA== 566 + dependencies: 567 + "@atproto/api" "^0.18.20" 568 + "@atproto/common" "^0.5.11" 524 569 "@atproto/crypto" "^0.4.5" 525 - "@atproto/identity" "^0.4.10" 570 + "@atproto/identity" "^0.4.11" 526 571 "@atproto/lexicon" "^0.6.1" 527 572 "@atproto/syntax" "^0.4.3" 528 573 "@atproto/ws-client" "^0.0.4" 529 574 "@atproto/xrpc" "^0.7.7" 530 - "@atproto/xrpc-server" "^0.10.10" 575 + "@atproto/xrpc-server" "^0.10.12" 531 576 "@did-plc/lib" "^0.0.1" 532 577 compression "^1.7.4" 533 578 cors "^2.8.5" ··· 545 590 undici "^6.14.1" 546 591 ws "^8.12.0" 547 592 548 - "@atproto/pds@^0.4.207": 549 - version "0.4.207" 550 - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.207.tgz#20ff3afcf1541b96f016a6843d69d3731b562f19" 551 - integrity sha512-LtzgqeD4aB3O1efzDbhdEopdujB4GG/klLT4N/3v35+0Jpylrkht5Ux2yJGKAqJkzFCfpBdRhNMWpJqS2ssu8A== 593 + "@atproto/pds@^0.4.209": 594 + version "0.4.209" 595 + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.209.tgz#eb12f325c55692a6487c0620a7754b13f0914fe1" 596 + integrity sha512-jW9dnrrqj65u6FZoj/idIez+idGgYBG4KGVB75i7pqrRkE1a91Bj5MXX60JfIQ0VY9YpfBthSnJG9Pzz5OzA/Q== 552 597 dependencies: 553 598 "@atproto-labs/fetch-node" "^0.2.0" 554 599 "@atproto-labs/simple-store" "^0.3.0" 555 600 "@atproto-labs/simple-store-memory" "^0.1.4" 556 601 "@atproto-labs/simple-store-redis" "^0.0.1" 557 602 "@atproto-labs/xrpc-utils" "^0.0.24" 558 - "@atproto/api" "^0.18.19" 603 + "@atproto/api" "^0.18.21" 559 604 "@atproto/aws" "^0.2.31" 560 - "@atproto/common" "^0.5.10" 605 + "@atproto/common" "^0.5.11" 561 606 "@atproto/crypto" "^0.4.5" 562 - "@atproto/identity" "^0.4.10" 563 - "@atproto/lex-cbor" "^0.0.10" 564 - "@atproto/lex-data" "^0.0.10" 607 + "@atproto/identity" "^0.4.11" 608 + "@atproto/lex-cbor" "^0.0.11" 609 + "@atproto/lex-data" "^0.0.11" 565 610 "@atproto/lexicon" "^0.6.1" 566 - "@atproto/oauth-provider" "^0.15.7" 611 + "@atproto/oauth-provider" "^0.15.9" 567 612 "@atproto/oauth-scopes" "^0.3.1" 568 613 "@atproto/repo" "^0.8.12" 569 614 "@atproto/syntax" "^0.4.3" 570 615 "@atproto/xrpc" "^0.7.7" 571 - "@atproto/xrpc-server" "^0.10.11" 616 + "@atproto/xrpc-server" "^0.10.12" 572 617 "@did-plc/lib" "^0.0.4" 573 618 "@hapi/address" "^5.1.1" 574 619 better-sqlite3 "^10.0.0" ··· 642 687 "@atproto/common" "^0.5.3" 643 688 ws "^8.12.0" 644 689 645 - "@atproto/xrpc-server@^0.10.0", "@atproto/xrpc-server@^0.10.10", "@atproto/xrpc-server@^0.10.3": 690 + "@atproto/xrpc-server@^0.10.0", "@atproto/xrpc-server@^0.10.3": 646 691 version "0.10.10" 647 692 resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.10.10.tgz#69d1a3ce8278a6b49286a5df33ebc204d42129ed" 648 693 integrity sha512-USDjOZGiletqzuWHC3Q2fk30hJDk4uYt6KPgvnZidShCouTf3hzwJZ8d2eOKOofSjGXW+GC0QYp7fYJFn6lZ2Q== ··· 661 706 ws "^8.12.0" 662 707 zod "^3.23.8" 663 708 664 - "@atproto/xrpc-server@^0.10.11": 665 - version "0.10.11" 666 - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.10.11.tgz#cd6f880ebb6a913db2925a23aa0ef0b05f5b02fd" 667 - integrity sha512-7XR+n1G4j1PO33slr2Agl+lmTXbEQzk5iaCJmrcsfTC/0BbCHqSSbm+WHduz3EH4dfFioZsnDo1UesCF0EQEtg== 709 + "@atproto/xrpc-server@^0.10.12": 710 + version "0.10.12" 711 + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.10.12.tgz#6b88c9af5d301302e5e177b5f982a3494e734d78" 712 + integrity sha512-HuciFHxvQfWtvq+dmH3vK9uXii49vqkJgZFLcHums1xMvLvNBY5bgtUBdpQdsdEuq8B4DTuanZRs+z3zGariyg== 668 713 dependencies: 669 - "@atproto/common" "^0.5.10" 714 + "@atproto/common" "^0.5.11" 670 715 "@atproto/crypto" "^0.4.5" 671 - "@atproto/lex-cbor" "^0.0.10" 672 - "@atproto/lex-data" "^0.0.10" 716 + "@atproto/lex-cbor" "^0.0.11" 717 + "@atproto/lex-data" "^0.0.11" 673 718 "@atproto/lexicon" "^0.6.1" 674 719 "@atproto/ws-client" "^0.0.4" 675 720 "@atproto/xrpc" "^0.7.7"