Merge branch 'scroll-refactor' into web-layout

This commit is contained in:
Dan Abramov
2023-12-14 02:39:43 +00:00
44 changed files with 614 additions and 8131 deletions
+2 -2
View File
@@ -9,12 +9,12 @@ module.exports = function () {
/**
* iOS build number. Must be incremented for each TestFlight version.
*/
const IOS_BUILD_NUMBER = '1'
const IOS_BUILD_NUMBER = '2'
/**
* Android build number. Must be incremented for each release.
*/
const ANDROID_VERSION_CODE = 48
const ANDROID_VERSION_CODE = 49
/**
* Uses built-in Expo env vars
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.58.0",
"version": "1.59.0",
"private": true,
"scripts": {
"prepare": "is-ci || husky install",
@@ -35,7 +35,7 @@
"intl:compile": "lingui compile"
},
"dependencies": {
"@atproto/api": "^0.7.2",
"@atproto/api": "^0.7.3",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@emoji-mart/react": "^1.1.1",
+35
View File
@@ -0,0 +1,35 @@
import React, {createContext, useContext, useMemo} from 'react'
import {ScrollHandlers} from 'react-native-reanimated'
const ScrollContext = createContext<ScrollHandlers<any>>({
onBeginDrag: undefined,
onEndDrag: undefined,
onScroll: undefined,
})
export function useScrollHandlers(): ScrollHandlers<any> {
return useContext(ScrollContext)
}
type ProviderProps = {children: React.ReactNode} & ScrollHandlers<any>
// Note: this completely *overrides* the parent handlers.
// It's up to you to compose them with the parent ones via useScrollHandlers() if needed.
export function ScrollProvider({
children,
onBeginDrag,
onEndDrag,
onScroll,
}: ProviderProps) {
const handlers = useMemo(
() => ({
onBeginDrag,
onEndDrag,
onScroll,
}),
[onBeginDrag, onEndDrag, onScroll],
)
return (
<ScrollContext.Provider value={handlers}>{children}</ScrollContext.Provider>
)
}
+38 -34
View File
@@ -1,3 +1,4 @@
import {useMemo} from 'react'
import {TextStyle, ViewStyle} from 'react-native'
import {useTheme, PaletteColorName, PaletteColor} from '../ThemeContext'
@@ -15,38 +16,41 @@ export interface UsePaletteValue {
icon: TextStyle
}
export function usePalette(color: PaletteColorName): UsePaletteValue {
const palette = useTheme().palette[color]
return {
colors: palette,
view: {
backgroundColor: palette.background,
},
viewLight: {
backgroundColor: palette.backgroundLight,
},
btn: {
backgroundColor: palette.backgroundLight,
},
border: {
borderColor: palette.border,
},
borderDark: {
borderColor: palette.borderDark,
},
text: {
color: palette.text,
},
textLight: {
color: palette.textLight,
},
textInverted: {
color: palette.textInverted,
},
link: {
color: palette.link,
},
icon: {
color: palette.icon,
},
}
const theme = useTheme()
return useMemo(() => {
const palette = theme.palette[color]
return {
colors: palette,
view: {
backgroundColor: palette.background,
},
viewLight: {
backgroundColor: palette.backgroundLight,
},
btn: {
backgroundColor: palette.backgroundLight,
},
border: {
borderColor: palette.border,
},
borderDark: {
borderColor: palette.borderDark,
},
text: {
color: palette.text,
},
textLight: {
color: palette.textLight,
},
textInverted: {
color: palette.textInverted,
},
link: {
color: palette.link,
},
icon: {
color: palette.icon,
},
}
}, [theme, color])
}
File diff suppressed because it is too large Load Diff
+43 -43
View File
@@ -21,7 +21,7 @@ msgstr ""
#~ msgid ". This warning is only available for posts with media attached."
#~ msgstr ""
#: src/view/shell/desktop/RightNav.tsx:158
#: src/view/shell/desktop/RightNav.tsx:160
msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
msgstr ""
@@ -33,7 +33,7 @@ msgstr ""
msgid "{0} {purposeLabel} List"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:141
#: src/view/shell/desktop/RightNav.tsx:143
msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}"
msgstr ""
@@ -175,7 +175,7 @@ msgstr ""
msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone."
msgstr ""
#: src/view/com/modals/VerifyEmail.tsx:110
#: src/view/com/modals/VerifyEmail.tsx:118
msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below."
msgstr ""
@@ -232,7 +232,7 @@ msgstr ""
msgid "Are you sure?"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:185
#: src/view/com/util/forms/PostDropdownBtn.tsx:188
msgid "Are you sure? This cannot be undone."
msgstr ""
@@ -287,7 +287,7 @@ msgstr ""
msgid "Block these accounts?"
msgstr ""
#: src/view/screens/Moderation.tsx:121
#: src/view/screens/Moderation.tsx:123
msgid "Blocked accounts"
msgstr ""
@@ -331,7 +331,7 @@ msgstr ""
msgid "Bluesky uses invites to build a healthier community. If you don't know anybody with an invite, you can sign up for the waitlist and we'll send one soon."
msgstr ""
#: src/view/screens/Moderation.tsx:222
#: src/view/screens/Moderation.tsx:225
msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private."
msgstr ""
@@ -344,7 +344,7 @@ msgid "Build version {0} {1}"
msgstr ""
#: src/view/com/composer/photos/OpenCameraBtn.tsx:60
#: src/view/com/util/UserAvatar.tsx:217
#: src/view/com/util/UserAvatar.tsx:221
#: src/view/com/util/UserBanner.tsx:38
msgid "Camera"
msgstr ""
@@ -420,7 +420,7 @@ msgstr ""
msgid "Change Handle"
msgstr ""
#: src/view/com/modals/VerifyEmail.tsx:133
#: src/view/com/modals/VerifyEmail.tsx:141
msgid "Change my email"
msgstr ""
@@ -508,7 +508,7 @@ msgstr ""
#: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:217
#: src/view/com/modals/VerifyEmail.tsx:225
#: src/view/screens/PreferencesHomeFeed.tsx:299
#: src/view/screens/PreferencesThreads.tsx:153
msgid "Confirm"
@@ -529,7 +529,7 @@ msgstr ""
#: src/view/com/modals/ChangeEmail.tsx:157
#: src/view/com/modals/DeleteAccount.tsx:176
#: src/view/com/modals/VerifyEmail.tsx:151
#: src/view/com/modals/VerifyEmail.tsx:159
msgid "Confirmation code"
msgstr ""
@@ -538,7 +538,7 @@ msgstr ""
msgid "Connecting..."
msgstr ""
#: src/view/screens/Moderation.tsx:79
#: src/view/screens/Moderation.tsx:81
msgid "Content filtering"
msgstr ""
@@ -577,7 +577,7 @@ msgstr ""
msgid "Copy link to list"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:126
#: src/view/com/util/forms/PostDropdownBtn.tsx:129
msgid "Copy link to post"
msgstr ""
@@ -585,7 +585,7 @@ msgstr ""
msgid "Copy link to profile"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:112
#: src/view/com/util/forms/PostDropdownBtn.tsx:115
msgid "Copy post text"
msgstr ""
@@ -656,11 +656,11 @@ msgstr ""
msgid "Delete my account…"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:180
#: src/view/com/util/forms/PostDropdownBtn.tsx:183
msgid "Delete post"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:184
#: src/view/com/util/forms/PostDropdownBtn.tsx:187
msgid "Delete this post?"
msgstr ""
@@ -691,7 +691,7 @@ msgstr ""
msgid "Discard draft"
msgstr ""
#: src/view/screens/Moderation.tsx:204
#: src/view/screens/Moderation.tsx:207
msgid "Discourage apps from showing my account to logged-out users"
msgstr ""
@@ -840,7 +840,7 @@ msgstr ""
msgid "Feed Preferences"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:64
#: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:292
msgid "Feedback"
msgstr ""
@@ -936,7 +936,7 @@ msgstr ""
msgid "Gallery"
msgstr ""
#: src/view/com/modals/VerifyEmail.tsx:175
#: src/view/com/modals/VerifyEmail.tsx:183
msgid "Get Started"
msgstr ""
@@ -964,7 +964,7 @@ msgstr ""
msgid "Handle"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:93
#: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:302
msgid "Help"
msgstr ""
@@ -1031,7 +1031,7 @@ msgstr ""
msgid "Hosting provider address"
msgstr ""
#: src/view/com/modals/VerifyEmail.tsx:200
#: src/view/com/modals/VerifyEmail.tsx:208
msgid "I have a code"
msgstr ""
@@ -1047,7 +1047,7 @@ msgstr ""
msgid "Image alt text"
msgstr ""
#: src/view/com/util/UserAvatar.tsx:304
#: src/view/com/util/UserAvatar.tsx:308
#: src/view/com/util/UserBanner.tsx:116
msgid "Image options"
msgstr ""
@@ -1121,7 +1121,7 @@ msgstr ""
msgid "Learn more about this warning"
msgstr ""
#: src/view/screens/Moderation.tsx:239
#: src/view/screens/Moderation.tsx:242
msgid "Learn more about what is public on Bluesky."
msgstr ""
@@ -1138,7 +1138,7 @@ msgstr ""
msgid "Let's get your password reset!"
msgstr ""
#: src/view/com/util/UserAvatar.tsx:241
#: src/view/com/util/UserAvatar.tsx:245
#: src/view/com/util/UserBanner.tsx:60
msgid "Library"
msgstr ""
@@ -1208,7 +1208,7 @@ msgstr ""
#~ msgid "Logged-out users"
#~ msgstr ""
#: src/view/screens/Moderation.tsx:134
#: src/view/screens/Moderation.tsx:136
msgid "Logged-out visibility"
msgstr ""
@@ -1245,7 +1245,7 @@ msgstr ""
msgid "Message from server"
msgstr ""
#: src/view/screens/Moderation.tsx:63
#: src/view/screens/Moderation.tsx:64
#: src/view/screens/Settings.tsx:563
#: src/view/shell/desktop/LeftNav.tsx:391
#: src/view/shell/Drawer.tsx:490
@@ -1253,7 +1253,7 @@ msgstr ""
msgid "Moderation"
msgstr ""
#: src/view/screens/Moderation.tsx:93
#: src/view/screens/Moderation.tsx:95
msgid "Moderation lists"
msgstr ""
@@ -1291,11 +1291,11 @@ msgstr ""
msgid "Mute these accounts?"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:144
#: src/view/com/util/forms/PostDropdownBtn.tsx:147
msgid "Mute thread"
msgstr ""
#: src/view/screens/Moderation.tsx:107
#: src/view/screens/Moderation.tsx:109
msgid "Muted accounts"
msgstr ""
@@ -1421,7 +1421,7 @@ msgstr ""
#~ msgid "Note: Bluesky is an open and public network, and enabling this will not make your profile private or limit the ability of logged in users to see your posts. This setting only limits the visibility of posts on the Bluesky app and website; third-party apps that display Bluesky content may not respect this setting, and could show your content to logged-out users."
#~ msgstr ""
#: src/view/screens/Moderation.tsx:229
#: src/view/screens/Moderation.tsx:232
msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites."
msgstr ""
@@ -1462,7 +1462,7 @@ msgstr ""
msgid "Opens configurable language settings"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:146
#: src/view/shell/desktop/RightNav.tsx:148
#: src/view/shell/Drawer.tsx:622
msgid "Opens list of invite codes"
msgstr ""
@@ -1611,7 +1611,7 @@ msgstr ""
msgid "Prioritize Your Follows"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:75
#: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy"
msgstr ""
@@ -1671,7 +1671,7 @@ msgstr ""
#: src/view/com/modals/ListAddRemoveUsers.tsx:264
#: src/view/com/modals/SelfLabel.tsx:83
#: src/view/com/modals/UserAddRemoveLists.tsx:193
#: src/view/com/util/UserAvatar.tsx:278
#: src/view/com/util/UserAvatar.tsx:282
#: src/view/com/util/UserBanner.tsx:89
msgid "Remove"
msgstr ""
@@ -1744,7 +1744,7 @@ msgid "Report List"
msgstr ""
#: src/view/com/modals/report/SendReportButton.tsx:37
#: src/view/com/util/forms/PostDropdownBtn.tsx:162
#: src/view/com/util/forms/PostDropdownBtn.tsx:165
msgid "Report post"
msgstr ""
@@ -1898,7 +1898,7 @@ msgstr ""
msgid "Select your preferred language for translations in your feed."
msgstr ""
#: src/view/com/modals/VerifyEmail.tsx:188
#: src/view/com/modals/VerifyEmail.tsx:196
msgid "Send Confirmation Email"
msgstr ""
@@ -1955,7 +1955,7 @@ msgid "Sexual activity or erotic nudity."
msgstr ""
#: src/view/com/profile/ProfileHeader.tsx:338
#: src/view/com/util/forms/PostDropdownBtn.tsx:126
#: src/view/com/util/forms/PostDropdownBtn.tsx:129
#: src/view/screens/ProfileList.tsx:407
msgid "Share"
msgstr ""
@@ -2075,7 +2075,7 @@ msgstr ""
#: src/view/com/modals/AppealLabel.tsx:101
msgid "Submit"
msgstr ">>>>>>> cb8a33b6 (Fix translations)"
msgstr "Submit"
#: src/view/screens/ProfileList.tsx:597
msgid "Subscribe"
@@ -2110,7 +2110,7 @@ msgstr ""
msgid "Tall"
msgstr ""
#: src/view/shell/desktop/RightNav.tsx:84
#: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms"
msgstr ""
@@ -2175,7 +2175,7 @@ msgstr ""
msgid "This information is not shared with other users."
msgstr ""
#: src/view/com/modals/VerifyEmail.tsx:105
#: src/view/com/modals/VerifyEmail.tsx:113
msgid "This is important in case you ever need to change your email or reset your password."
msgstr ""
@@ -2212,9 +2212,9 @@ msgstr ""
msgid "Transformations"
msgstr ""
#: src/view/com/post-thread/PostThreadItem.tsx:704
#: src/view/com/post-thread/PostThreadItem.tsx:706
#: src/view/com/util/forms/PostDropdownBtn.tsx:98
#: src/view/com/post-thread/PostThreadItem.tsx:708
#: src/view/com/util/forms/PostDropdownBtn.tsx:101
msgid "Translate"
msgstr ""
@@ -2258,7 +2258,7 @@ msgstr ""
msgid "Unmute Account"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:144
#: src/view/com/util/forms/PostDropdownBtn.tsx:147
msgid "Unmute thread"
msgstr ""
@@ -2493,7 +2493,7 @@ msgstr ""
msgid "Your email has been updated but not verified. As a next step, please verify your new email."
msgstr ""
#: src/view/com/modals/VerifyEmail.tsx:100
#: src/view/com/modals/VerifyEmail.tsx:108
msgid "Your email has not yet been verified. This is an important security step which we recommend."
msgstr ""
@@ -2507,7 +2507,7 @@ msgid "Your hosting provider"
msgstr ""
#: src/view/screens/Settings.tsx:402
#: src/view/shell/desktop/RightNav.tsx:127
#: src/view/shell/desktop/RightNav.tsx:129
#: src/view/shell/Drawer.tsx:636
msgid "Your invite codes are hidden when logged in using an App Password"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+42 -42
View File
@@ -21,7 +21,7 @@ msgstr ""
#~ msgid ". This warning is only available for posts with media attached."
#~ msgstr "यह चेतावनी केवल मीडिया वाले पोस्ट के लिए उपलब्ध है।"
#: src/view/shell/desktop/RightNav.tsx:158
#: src/view/shell/desktop/RightNav.tsx:160
msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
msgstr ""
@@ -33,7 +33,7 @@ msgstr "{0}"
msgid "{0} {purposeLabel} List"
msgstr "{0} {purposeLabel} सूची"
#: src/view/shell/desktop/RightNav.tsx:141
#: src/view/shell/desktop/RightNav.tsx:143
msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}"
msgstr ""
@@ -175,7 +175,7 @@ msgstr "वैकल्पिक पाठ"
msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone."
msgstr "ऑल्ट टेक्स्ट अंधा और कम दृश्य लोगों के लिए छवियों का वर्णन करता है, और हर किसी को संदर्भ देने में मदद करता है।।"
#: src/view/com/modals/VerifyEmail.tsx:110
#: src/view/com/modals/VerifyEmail.tsx:118
msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below."
msgstr "{0} को ईमेल भेजा गया है। इसमें एक OTP कोड शामिल है जिसे आप नीचे दर्ज कर सकते हैं।।"
@@ -232,7 +232,7 @@ msgstr "क्या आप वाकई इस ड्राफ्ट को ह
msgid "Are you sure?"
msgstr "क्या आप वास्तव में इसे करना चाहते हैं?"
#: src/view/com/util/forms/PostDropdownBtn.tsx:185
#: src/view/com/util/forms/PostDropdownBtn.tsx:188
msgid "Are you sure? This cannot be undone."
msgstr "क्या आप वास्तव में इसे करना चाहते हैं? इसे असंपादित नहीं किया जा सकता है।"
@@ -287,7 +287,7 @@ msgstr ""
msgid "Block these accounts?"
msgstr "खाता ब्लॉक करें?"
#: src/view/screens/Moderation.tsx:121
#: src/view/screens/Moderation.tsx:123
msgid "Blocked accounts"
msgstr "ब्लॉक किए गए खाते"
@@ -331,7 +331,7 @@ msgstr "Bluesky सार्वजनिक है।।"
msgid "Bluesky uses invites to build a healthier community. If you don't know anybody with an invite, you can sign up for the waitlist and we'll send one soon."
msgstr "ब्लूस्की एक स्वस्थ समुदाय बनाने के लिए आमंत्रित करता है। यदि आप किसी को आमंत्रित नहीं करते हैं, तो आप प्रतीक्षा सूची के लिए साइन अप कर सकते हैं और हम जल्द ही एक भेज देंगे।।"
#: src/view/screens/Moderation.tsx:222
#: src/view/screens/Moderation.tsx:225
msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private."
msgstr ""
@@ -344,7 +344,7 @@ msgid "Build version {0} {1}"
msgstr "Build version {0} {1}"
#: src/view/com/composer/photos/OpenCameraBtn.tsx:60
#: src/view/com/util/UserAvatar.tsx:217
#: src/view/com/util/UserAvatar.tsx:221
#: src/view/com/util/UserBanner.tsx:38
msgid "Camera"
msgstr "कैमरा"
@@ -420,7 +420,7 @@ msgstr "हैंडल बदलें"
msgid "Change Handle"
msgstr "हैंडल बदलें"
#: src/view/com/modals/VerifyEmail.tsx:133
#: src/view/com/modals/VerifyEmail.tsx:141
msgid "Change my email"
msgstr "मेरा ईमेल बदलें"
@@ -504,7 +504,7 @@ msgstr "जवाब लिखो"
#: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:217
#: src/view/com/modals/VerifyEmail.tsx:225
#: src/view/screens/PreferencesHomeFeed.tsx:299
#: src/view/screens/PreferencesThreads.tsx:153
msgid "Confirm"
@@ -525,7 +525,7 @@ msgstr "खाते को हटा दें"
#: src/view/com/modals/ChangeEmail.tsx:157
#: src/view/com/modals/DeleteAccount.tsx:176
#: src/view/com/modals/VerifyEmail.tsx:151
#: src/view/com/modals/VerifyEmail.tsx:159
msgid "Confirmation code"
msgstr "OTP कोड"
@@ -534,7 +534,7 @@ msgstr "OTP कोड"
msgid "Connecting..."
msgstr "कनेक्टिंग ..।"
#: src/view/screens/Moderation.tsx:79
#: src/view/screens/Moderation.tsx:81
msgid "Content filtering"
msgstr "सामग्री फ़िल्टरिंग"
@@ -573,7 +573,7 @@ msgstr "कॉपी"
msgid "Copy link to list"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:126
#: src/view/com/util/forms/PostDropdownBtn.tsx:129
msgid "Copy link to post"
msgstr ""
@@ -581,7 +581,7 @@ msgstr ""
msgid "Copy link to profile"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:112
#: src/view/com/util/forms/PostDropdownBtn.tsx:115
msgid "Copy post text"
msgstr "पोस्ट टेक्स्ट कॉपी करें"
@@ -652,11 +652,11 @@ msgstr "मेरा खाता हटाएं"
msgid "Delete my account…"
msgstr "मेरा खाता हटाएं…"
#: src/view/com/util/forms/PostDropdownBtn.tsx:180
#: src/view/com/util/forms/PostDropdownBtn.tsx:183
msgid "Delete post"
msgstr "पोस्ट को हटाएं"
#: src/view/com/util/forms/PostDropdownBtn.tsx:184
#: src/view/com/util/forms/PostDropdownBtn.tsx:187
msgid "Delete this post?"
msgstr "इस पोस्ट को डीलीट करें?"
@@ -687,7 +687,7 @@ msgstr ""
msgid "Discard draft"
msgstr "ड्राफ्ट हटाएं"
#: src/view/screens/Moderation.tsx:204
#: src/view/screens/Moderation.tsx:207
msgid "Discourage apps from showing my account to logged-out users"
msgstr ""
@@ -836,7 +836,7 @@ msgstr "फ़ीड ऑफ़लाइन है"
msgid "Feed Preferences"
msgstr "फ़ीड प्राथमिकता"
#: src/view/shell/desktop/RightNav.tsx:64
#: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:292
msgid "Feedback"
msgstr "प्रतिक्रिया"
@@ -928,7 +928,7 @@ msgstr "पासवर्ड भूल गए"
msgid "Gallery"
msgstr "गैलरी"
#: src/view/com/modals/VerifyEmail.tsx:175
#: src/view/com/modals/VerifyEmail.tsx:183
msgid "Get Started"
msgstr "प्रारंभ करें"
@@ -956,7 +956,7 @@ msgstr "अगला"
msgid "Handle"
msgstr "हैंडल"
#: src/view/shell/desktop/RightNav.tsx:93
#: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:302
msgid "Help"
msgstr "सहायता"
@@ -1023,7 +1023,7 @@ msgstr "होस्टिंग प्रदाता"
msgid "Hosting provider address"
msgstr "होस्टिंग प्रदाता पता"
#: src/view/com/modals/VerifyEmail.tsx:200
#: src/view/com/modals/VerifyEmail.tsx:208
msgid "I have a code"
msgstr "मेरे पास एक OTP कोड है"
@@ -1039,7 +1039,7 @@ msgstr "यदि किसी को चुना जाता है, तो
msgid "Image alt text"
msgstr "छवि alt पाठ"
#: src/view/com/util/UserAvatar.tsx:304
#: src/view/com/util/UserAvatar.tsx:308
#: src/view/com/util/UserBanner.tsx:116
msgid "Image options"
msgstr "छवि विकल्प"
@@ -1113,7 +1113,7 @@ msgstr "अधिक जानें"
msgid "Learn more about this warning"
msgstr "इस चेतावनी के बारे में अधिक जानें"
#: src/view/screens/Moderation.tsx:239
#: src/view/screens/Moderation.tsx:242
msgid "Learn more about what is public on Bluesky."
msgstr ""
@@ -1130,7 +1130,7 @@ msgstr "लीविंग Bluesky"
msgid "Let's get your password reset!"
msgstr "चलो अपना पासवर्ड रीसेट करें!"
#: src/view/com/util/UserAvatar.tsx:241
#: src/view/com/util/UserAvatar.tsx:245
#: src/view/com/util/UserBanner.tsx:60
msgid "Library"
msgstr "चित्र पुस्तकालय"
@@ -1200,7 +1200,7 @@ msgstr "स्थानीय देव सर्वर"
#~ msgid "Logged-out users"
#~ msgstr ""
#: src/view/screens/Moderation.tsx:134
#: src/view/screens/Moderation.tsx:136
msgid "Logged-out visibility"
msgstr ""
@@ -1237,7 +1237,7 @@ msgstr "मेनू"
msgid "Message from server"
msgstr ""
#: src/view/screens/Moderation.tsx:63
#: src/view/screens/Moderation.tsx:64
#: src/view/screens/Settings.tsx:563
#: src/view/shell/desktop/LeftNav.tsx:391
#: src/view/shell/Drawer.tsx:490
@@ -1245,7 +1245,7 @@ msgstr ""
msgid "Moderation"
msgstr "मॉडरेशन"
#: src/view/screens/Moderation.tsx:93
#: src/view/screens/Moderation.tsx:95
msgid "Moderation lists"
msgstr "मॉडरेशन सूचियाँ"
@@ -1283,11 +1283,11 @@ msgstr ""
msgid "Mute these accounts?"
msgstr "इन खातों को म्यूट करें?"
#: src/view/com/util/forms/PostDropdownBtn.tsx:144
#: src/view/com/util/forms/PostDropdownBtn.tsx:147
msgid "Mute thread"
msgstr "थ्रेड म्यूट करें"
#: src/view/screens/Moderation.tsx:107
#: src/view/screens/Moderation.tsx:109
msgid "Muted accounts"
msgstr "म्यूट किए गए खाते"
@@ -1413,7 +1413,7 @@ msgstr "लागू नहीं।"
#~ msgid "Note: Bluesky is an open and public network, and enabling this will not make your profile private or limit the ability of logged in users to see your posts. This setting only limits the visibility of posts on the Bluesky app and website; third-party apps that display Bluesky content may not respect this setting, and could show your content to logged-out users."
#~ msgstr ""
#: src/view/screens/Moderation.tsx:229
#: src/view/screens/Moderation.tsx:232
msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites."
msgstr ""
@@ -1454,7 +1454,7 @@ msgstr "ओपन नेविगेशन"
msgid "Opens configurable language settings"
msgstr "भाषा सेटिंग्स खोलें"
#: src/view/shell/desktop/RightNav.tsx:146
#: src/view/shell/desktop/RightNav.tsx:148
#: src/view/shell/Drawer.tsx:622
msgid "Opens list of invite codes"
msgstr ""
@@ -1603,7 +1603,7 @@ msgstr "प्राथमिक भाषा"
msgid "Prioritize Your Follows"
msgstr "अपने फ़ॉलोअर्स को प्राथमिकता दें"
#: src/view/shell/desktop/RightNav.tsx:75
#: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy"
msgstr "गोपनीयता"
@@ -1663,7 +1663,7 @@ msgstr "अनुशंसित लोग"
#: src/view/com/modals/ListAddRemoveUsers.tsx:264
#: src/view/com/modals/SelfLabel.tsx:83
#: src/view/com/modals/UserAddRemoveLists.tsx:193
#: src/view/com/util/UserAvatar.tsx:278
#: src/view/com/util/UserAvatar.tsx:282
#: src/view/com/util/UserBanner.tsx:89
msgid "Remove"
msgstr "निकालें"
@@ -1736,7 +1736,7 @@ msgid "Report List"
msgstr "रिपोर्ट सूची"
#: src/view/com/modals/report/SendReportButton.tsx:37
#: src/view/com/util/forms/PostDropdownBtn.tsx:162
#: src/view/com/util/forms/PostDropdownBtn.tsx:165
msgid "Report post"
msgstr "रिपोर्ट पोस्ट"
@@ -1890,7 +1890,7 @@ msgstr "ऐप में प्रदर्शित होने वाले
msgid "Select your preferred language for translations in your feed."
msgstr "अपने फ़ीड में अनुवाद के लिए अपनी पसंदीदा भाषा चुनें।"
#: src/view/com/modals/VerifyEmail.tsx:188
#: src/view/com/modals/VerifyEmail.tsx:196
msgid "Send Confirmation Email"
msgstr "पुष्टिकरण ईमेल भेजें"
@@ -1947,7 +1947,7 @@ msgid "Sexual activity or erotic nudity."
msgstr "यौन गतिविधि या कामुक नग्नता।।"
#: src/view/com/profile/ProfileHeader.tsx:338
#: src/view/com/util/forms/PostDropdownBtn.tsx:126
#: src/view/com/util/forms/PostDropdownBtn.tsx:129
#: src/view/screens/ProfileList.tsx:407
msgid "Share"
msgstr "शेयर"
@@ -2102,7 +2102,7 @@ msgstr "सिस्टम लॉग"
msgid "Tall"
msgstr "लंबा"
#: src/view/shell/desktop/RightNav.tsx:84
#: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms"
msgstr "शर्तें"
@@ -2167,7 +2167,7 @@ msgstr ""
msgid "This information is not shared with other users."
msgstr "यह जानकारी अन्य उपयोगकर्ताओं के साथ साझा नहीं की जाती है।।"
#: src/view/com/modals/VerifyEmail.tsx:105
#: src/view/com/modals/VerifyEmail.tsx:113
msgid "This is important in case you ever need to change your email or reset your password."
msgstr "अगर आपको कभी अपना ईमेल बदलने या पासवर्ड रीसेट करने की आवश्यकता है तो यह महत्वपूर्ण है।।"
@@ -2204,9 +2204,9 @@ msgstr "ड्रॉपडाउन टॉगल करें"
msgid "Transformations"
msgstr "परिवर्तन"
#: src/view/com/post-thread/PostThreadItem.tsx:704
#: src/view/com/post-thread/PostThreadItem.tsx:706
#: src/view/com/util/forms/PostDropdownBtn.tsx:98
#: src/view/com/post-thread/PostThreadItem.tsx:708
#: src/view/com/util/forms/PostDropdownBtn.tsx:101
msgid "Translate"
msgstr "अनुवाद"
@@ -2250,7 +2250,7 @@ msgstr ""
msgid "Unmute Account"
msgstr "अनम्यूट खाता"
#: src/view/com/util/forms/PostDropdownBtn.tsx:144
#: src/view/com/util/forms/PostDropdownBtn.tsx:147
msgid "Unmute thread"
msgstr "थ्रेड को अनम्यूट करें"
@@ -2485,7 +2485,7 @@ msgstr "आपका ईमेल बचाया गया है! हम ज
msgid "Your email has been updated but not verified. As a next step, please verify your new email."
msgstr "आपका ईमेल अद्यतन किया गया है लेकिन सत्यापित नहीं किया गया है। अगले चरण के रूप में, कृपया अपना नया ईमेल सत्यापित करें।।"
#: src/view/com/modals/VerifyEmail.tsx:100
#: src/view/com/modals/VerifyEmail.tsx:108
msgid "Your email has not yet been verified. This is an important security step which we recommend."
msgstr "आपका ईमेल अभी तक सत्यापित नहीं हुआ है। यह एक महत्वपूर्ण सुरक्षा कदम है जिसे हम अनुशंसा करते हैं।।"
@@ -2499,7 +2499,7 @@ msgid "Your hosting provider"
msgstr "आपका होस्टिंग प्रदाता"
#: src/view/screens/Settings.tsx:402
#: src/view/shell/desktop/RightNav.tsx:127
#: src/view/shell/desktop/RightNav.tsx:129
#: src/view/shell/Drawer.tsx:636
msgid "Your invite codes are hidden when logged in using an App Password"
msgstr ""
+42 -42
View File
@@ -13,7 +13,7 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/view/shell/desktop/RightNav.tsx:158
#: src/view/shell/desktop/RightNav.tsx:160
msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
msgstr ""
@@ -25,7 +25,7 @@ msgstr "{0}"
msgid "{0} {purposeLabel} List"
msgstr "{0} {purposeLabel} リスト"
#: src/view/shell/desktop/RightNav.tsx:141
#: src/view/shell/desktop/RightNav.tsx:143
msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}"
msgstr ""
@@ -155,7 +155,7 @@ msgstr "ALTテキスト"
msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone."
msgstr "ALTテキストは、視覚障害者や低視力者のために画像を説明し、すべての人に文脈を与えるのに役立ちます。"
#: src/view/com/modals/VerifyEmail.tsx:110
#: src/view/com/modals/VerifyEmail.tsx:118
msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below."
msgstr "Eメールが{0}に送信されました。以下に入力できる確認コードが含まれています。"
@@ -208,7 +208,7 @@ msgstr "本当にこの下書きを破棄しますか?"
msgid "Are you sure?"
msgstr "本当ですか?"
#: src/view/com/util/forms/PostDropdownBtn.tsx:185
#: src/view/com/util/forms/PostDropdownBtn.tsx:188
msgid "Are you sure? This cannot be undone."
msgstr "本当ですか?これは元に戻せません。"
@@ -259,7 +259,7 @@ msgstr ""
msgid "Block these accounts?"
msgstr "これらのアカウントをブロックしますか?"
#: src/view/screens/Moderation.tsx:121
#: src/view/screens/Moderation.tsx:123
msgid "Blocked accounts"
msgstr "ブロックされたブロック"
@@ -303,7 +303,7 @@ msgstr "Blueskyはオープンです。"
msgid "Bluesky uses invites to build a healthier community. If you don't know anybody with an invite, you can sign up for the waitlist and we'll send one soon."
msgstr "Blueskyはより健全なコミュニティを構築するために招待状を使用します。招待状をお持ちでない方の場合、waitlistに申し込めば招待状をお送りします。"
#: src/view/screens/Moderation.tsx:222
#: src/view/screens/Moderation.tsx:225
msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private."
msgstr ""
@@ -316,7 +316,7 @@ msgid "Build version {0} {1}"
msgstr "ビルドバージョン {0} {1}"
#: src/view/com/composer/photos/OpenCameraBtn.tsx:60
#: src/view/com/util/UserAvatar.tsx:217
#: src/view/com/util/UserAvatar.tsx:221
#: src/view/com/util/UserBanner.tsx:38
msgid "Camera"
msgstr "カメラ"
@@ -392,7 +392,7 @@ msgstr "ハンドルを変更"
msgid "Change Handle"
msgstr "ハンドルを変更"
#: src/view/com/modals/VerifyEmail.tsx:133
#: src/view/com/modals/VerifyEmail.tsx:141
msgid "Change my email"
msgstr "Eメールの変更"
@@ -476,7 +476,7 @@ msgstr "返信を作成"
#: src/view/com/modals/AppealLabel.tsx:98
#: src/view/com/modals/Confirm.tsx:75
#: src/view/com/modals/SelfLabel.tsx:154
#: src/view/com/modals/VerifyEmail.tsx:217
#: src/view/com/modals/VerifyEmail.tsx:225
#: src/view/screens/PreferencesHomeFeed.tsx:299
#: src/view/screens/PreferencesThreads.tsx:153
msgid "Confirm"
@@ -497,7 +497,7 @@ msgstr "アカウントの削除を確認"
#: src/view/com/modals/ChangeEmail.tsx:157
#: src/view/com/modals/DeleteAccount.tsx:176
#: src/view/com/modals/VerifyEmail.tsx:151
#: src/view/com/modals/VerifyEmail.tsx:159
msgid "Confirmation code"
msgstr "確認コード"
@@ -506,7 +506,7 @@ msgstr "確認コード"
msgid "Connecting..."
msgstr "接続中..."
#: src/view/screens/Moderation.tsx:79
#: src/view/screens/Moderation.tsx:81
msgid "Content filtering"
msgstr "コンテンツフィルタリング"
@@ -545,7 +545,7 @@ msgstr "コピー"
msgid "Copy link to list"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:126
#: src/view/com/util/forms/PostDropdownBtn.tsx:129
msgid "Copy link to post"
msgstr ""
@@ -553,7 +553,7 @@ msgstr ""
msgid "Copy link to profile"
msgstr ""
#: src/view/com/util/forms/PostDropdownBtn.tsx:112
#: src/view/com/util/forms/PostDropdownBtn.tsx:115
msgid "Copy post text"
msgstr "投稿テキストをコピー"
@@ -620,11 +620,11 @@ msgstr "マイアカウントを削除"
msgid "Delete my account…"
msgstr "マイアカウントを削除…"
#: src/view/com/util/forms/PostDropdownBtn.tsx:180
#: src/view/com/util/forms/PostDropdownBtn.tsx:183
msgid "Delete post"
msgstr "投稿を削除"
#: src/view/com/util/forms/PostDropdownBtn.tsx:184
#: src/view/com/util/forms/PostDropdownBtn.tsx:187
msgid "Delete this post?"
msgstr "この投稿を削除しますか?"
@@ -655,7 +655,7 @@ msgstr "破棄"
msgid "Discard draft"
msgstr "ドラフトを破棄"
#: src/view/screens/Moderation.tsx:204
#: src/view/screens/Moderation.tsx:207
msgid "Discourage apps from showing my account to logged-out users"
msgstr ""
@@ -804,7 +804,7 @@ msgstr "フィードはオフライン"
msgid "Feed Preferences"
msgstr "フィード設定"
#: src/view/shell/desktop/RightNav.tsx:64
#: src/view/shell/desktop/RightNav.tsx:65
#: src/view/shell/Drawer.tsx:292
msgid "Feedback"
msgstr "フィードバック"
@@ -896,7 +896,7 @@ msgstr "パスワードを失念"
msgid "Gallery"
msgstr "ギャラリー"
#: src/view/com/modals/VerifyEmail.tsx:175
#: src/view/com/modals/VerifyEmail.tsx:183
msgid "Get Started"
msgstr "はじめに"
@@ -924,7 +924,7 @@ msgstr "次へ"
msgid "Handle"
msgstr "ハンドル"
#: src/view/shell/desktop/RightNav.tsx:93
#: src/view/shell/desktop/RightNav.tsx:94
#: src/view/shell/Drawer.tsx:302
msgid "Help"
msgstr "ヘルプ"
@@ -983,7 +983,7 @@ msgstr "ホスティングプロバイダー"
msgid "Hosting provider address"
msgstr "ホスティングプロバイダーアドレス"
#: src/view/com/modals/VerifyEmail.tsx:200
#: src/view/com/modals/VerifyEmail.tsx:208
msgid "I have a code"
msgstr "コードを持っている"
@@ -999,7 +999,7 @@ msgstr "選択されていない場合は、すべての年齢に適していま
msgid "Image alt text"
msgstr "画像のALTテキスト"
#: src/view/com/util/UserAvatar.tsx:304
#: src/view/com/util/UserAvatar.tsx:308
#: src/view/com/util/UserBanner.tsx:116
msgid "Image options"
msgstr "イメージオプション"
@@ -1068,7 +1068,7 @@ msgstr "詳細"
msgid "Learn more about this warning"
msgstr "この警告の詳細"
#: src/view/screens/Moderation.tsx:239
#: src/view/screens/Moderation.tsx:242
msgid "Learn more about what is public on Bluesky."
msgstr ""
@@ -1085,7 +1085,7 @@ msgstr "Blueskyから離れる"
msgid "Let's get your password reset!"
msgstr "パスワードをリセットしましょう!"
#: src/view/com/util/UserAvatar.tsx:241
#: src/view/com/util/UserAvatar.tsx:245
#: src/view/com/util/UserBanner.tsx:60
msgid "Library"
msgstr "ライブラリー"
@@ -1147,7 +1147,7 @@ msgstr "ローカル開発者サーバー"
#~ msgid "Logged-out users"
#~ msgstr ""
#: src/view/screens/Moderation.tsx:134
#: src/view/screens/Moderation.tsx:136
msgid "Logged-out visibility"
msgstr ""
@@ -1184,7 +1184,7 @@ msgstr "メニュー"
msgid "Message from server"
msgstr ""
#: src/view/screens/Moderation.tsx:63
#: src/view/screens/Moderation.tsx:64
#: src/view/screens/Settings.tsx:563
#: src/view/shell/desktop/LeftNav.tsx:391
#: src/view/shell/Drawer.tsx:490
@@ -1192,7 +1192,7 @@ msgstr ""
msgid "Moderation"
msgstr "モデレート"
#: src/view/screens/Moderation.tsx:93
#: src/view/screens/Moderation.tsx:95
msgid "Moderation lists"
msgstr "モデレートリスト"
@@ -1226,11 +1226,11 @@ msgstr ""
msgid "Mute these accounts?"
msgstr "これらのアカウントをミュートしますか?"
#: src/view/com/util/forms/PostDropdownBtn.tsx:144
#: src/view/com/util/forms/PostDropdownBtn.tsx:147
msgid "Mute thread"
msgstr "スレッドをミュート"
#: src/view/screens/Moderation.tsx:107
#: src/view/screens/Moderation.tsx:109
msgid "Muted accounts"
msgstr "ミュート済みアカウント"
@@ -1343,7 +1343,7 @@ msgstr "該当なし。"
#~ msgid "Note: Bluesky is an open and public network, and enabling this will not make your profile private or limit the ability of logged in users to see your posts. This setting only limits the visibility of posts on the Bluesky app and website; third-party apps that display Bluesky content may not respect this setting, and could show your content to logged-out users."
#~ msgstr ""
#: src/view/screens/Moderation.tsx:229
#: src/view/screens/Moderation.tsx:232
msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites."
msgstr ""
@@ -1380,7 +1380,7 @@ msgstr "ナビゲーションを開く"
msgid "Opens configurable language settings"
msgstr "構成可能な言語設定を開く"
#: src/view/shell/desktop/RightNav.tsx:146
#: src/view/shell/desktop/RightNav.tsx:148
#: src/view/shell/Drawer.tsx:622
msgid "Opens list of invite codes"
msgstr ""
@@ -1529,7 +1529,7 @@ msgstr "第一言語"
msgid "Prioritize Your Follows"
msgstr "フォローの優先順位付け"
#: src/view/shell/desktop/RightNav.tsx:75
#: src/view/shell/desktop/RightNav.tsx:76
msgid "Privacy"
msgstr "プライバシー"
@@ -1584,7 +1584,7 @@ msgstr "推奨ユーザー"
#: src/view/com/modals/ListAddRemoveUsers.tsx:264
#: src/view/com/modals/SelfLabel.tsx:83
#: src/view/com/modals/UserAddRemoveLists.tsx:193
#: src/view/com/util/UserAvatar.tsx:278
#: src/view/com/util/UserAvatar.tsx:282
#: src/view/com/util/UserBanner.tsx:89
msgid "Remove"
msgstr "削除"
@@ -1657,7 +1657,7 @@ msgid "Report List"
msgstr "レポートリスト"
#: src/view/com/modals/report/SendReportButton.tsx:37
#: src/view/com/util/forms/PostDropdownBtn.tsx:162
#: src/view/com/util/forms/PostDropdownBtn.tsx:165
msgid "Report post"
msgstr "レポート投稿"
@@ -1795,7 +1795,7 @@ msgstr "アプリに表示するデフォルトのテキストのアプリ言語
msgid "Select your preferred language for translations in your feed."
msgstr "フィード内の翻訳に使用する言語を選択します。"
#: src/view/com/modals/VerifyEmail.tsx:188
#: src/view/com/modals/VerifyEmail.tsx:196
msgid "Send Confirmation Email"
msgstr "確認Eメールを送信"
@@ -1852,7 +1852,7 @@ msgid "Sexual activity or erotic nudity."
msgstr "性行為またはエロティックなヌード。"
#: src/view/com/profile/ProfileHeader.tsx:338
#: src/view/com/util/forms/PostDropdownBtn.tsx:126
#: src/view/com/util/forms/PostDropdownBtn.tsx:129
#: src/view/screens/ProfileList.tsx:407
msgid "Share"
msgstr "共有"
@@ -1999,7 +1999,7 @@ msgstr "システムログ"
msgid "Tall"
msgstr "トール"
#: src/view/shell/desktop/RightNav.tsx:84
#: src/view/shell/desktop/RightNav.tsx:85
msgid "Terms"
msgstr "条件"
@@ -2064,7 +2064,7 @@ msgstr ""
msgid "This information is not shared with other users."
msgstr "この情報は他のユーザーと共有されません。"
#: src/view/com/modals/VerifyEmail.tsx:105
#: src/view/com/modals/VerifyEmail.tsx:113
msgid "This is important in case you ever need to change your email or reset your password."
msgstr "これは、Eメールの変更やパスワードのリセットが必要な場合に重要です。"
@@ -2101,9 +2101,9 @@ msgstr "トグルドロップダウン"
msgid "Transformations"
msgstr "変換"
#: src/view/com/post-thread/PostThreadItem.tsx:704
#: src/view/com/post-thread/PostThreadItem.tsx:706
#: src/view/com/util/forms/PostDropdownBtn.tsx:98
#: src/view/com/post-thread/PostThreadItem.tsx:708
#: src/view/com/util/forms/PostDropdownBtn.tsx:101
msgid "Translate"
msgstr "翻訳"
@@ -2147,7 +2147,7 @@ msgstr "残念ながら、アカウントを作成するための要件を満た
msgid "Unmute Account"
msgstr "アカウントのミュート解除"
#: src/view/com/util/forms/PostDropdownBtn.tsx:144
#: src/view/com/util/forms/PostDropdownBtn.tsx:147
msgid "Unmute thread"
msgstr "スレッドのミュート解除"
@@ -2366,7 +2366,7 @@ msgstr "Eメールが保存されました!すぐにご連絡いたします
msgid "Your email has been updated but not verified. As a next step, please verify your new email."
msgstr "Eメールは更新されましたが、確認されていません。次のステップとして、新しいEメールを確認してください。"
#: src/view/com/modals/VerifyEmail.tsx:100
#: src/view/com/modals/VerifyEmail.tsx:108
msgid "Your email has not yet been verified. This is an important security step which we recommend."
msgstr "Eメールはまだ確認されていません。これは、当社が推奨する重要なセキュリティステップです。"
@@ -2380,7 +2380,7 @@ msgid "Your hosting provider"
msgstr "ホスティングプロバイダー"
#: src/view/screens/Settings.tsx:402
#: src/view/shell/desktop/RightNav.tsx:127
#: src/view/shell/desktop/RightNav.tsx:129
#: src/view/shell/Drawer.tsx:636
msgid "Your invite codes are hidden when logged in using an App Password"
msgstr ""
+25 -27
View File
@@ -16,7 +16,7 @@
* 3. Don't call this query's `refetch()` if you're trying to sync latest; call `checkUnread()` instead.
*/
import {useEffect, useRef} from 'react'
import {useEffect} from 'react'
import {AppBskyFeedDefs} from '@atproto/api'
import {
useInfiniteQuery,
@@ -49,8 +49,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const threadMutes = useMutedThreads()
const unreads = useUnreadNotificationsApi()
const enabled = opts?.enabled !== false
// state tracked across page fetches
const pageState = useRef({pageNum: 0, hasMarkedRead: false})
const query = useInfiniteQuery<
FeedPage,
@@ -66,8 +64,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
if (!pageParam) {
// for the first page, we check the cached page held by the unread-checker first
page = unreads.getCachedUnreadPage()
// reset the page state
pageState.current = {pageNum: 0, hasMarkedRead: false}
}
if (!page) {
page = await fetchPage({
@@ -80,27 +76,9 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
})
}
// NOTE
// this section checks to see if we need to mark notifs read
// we want to wait until we've seen a read notification because
// of a timing challenge; marking read on the first page would
// cause subsequent pages of unread notifs to incorrectly come
// back as "read". we use page 6 as an abort condition, which means
// after ~180 notifs we give up on tracking unread state correctly
// -prf
if (!pageState.current.hasMarkedRead) {
let hasMarkedRead = false
if (
pageState.current.pageNum > 5 ||
page.items.some(item => item.notification.isRead)
) {
unreads.markAllRead()
hasMarkedRead = true
}
pageState.current = {
pageNum: pageState.current.pageNum + 1,
hasMarkedRead,
}
// if the first page has an unread, mark all read
if (!pageParam && page.items[0] && !page.items[0].notification.isRead) {
unreads.markAllRead()
}
return page
@@ -108,6 +86,20 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
enabled,
select(data: InfiniteData<FeedPage>) {
// override 'isRead' using the first page's returned seenAt
// we do this because the `markAllRead()` call above will
// mark subsequent pages as read prematurely
const seenAt = data.pages[0]?.seenAt || new Date()
for (const page of data.pages) {
for (const item of page.items) {
item.notification.isRead =
seenAt > new Date(item.notification.indexedAt)
}
}
return data
},
})
useEffect(() => {
@@ -122,7 +114,13 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
count += page.items.length
}
if (!isFetching && hasNextPage && count < PAGE_SIZE && numEmpties < 3) {
if (
!isFetching &&
hasNextPage &&
count < PAGE_SIZE &&
numEmpties < 3 &&
(data?.pages.length || 0) < 6
) {
query.fetchNextPage()
}
}, [query])
+1
View File
@@ -24,6 +24,7 @@ export interface FeedNotification {
export interface FeedPage {
cursor: string | undefined
seenAt: Date
items: FeedNotification[]
}
+6
View File
@@ -68,8 +68,14 @@ export async function fetchPage({
notif => !isThreadMuted(notif, threadMutes),
)
let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date()
if (Number.isNaN(seenAt.getTime())) {
seenAt = new Date()
}
return {
cursor: res.data.cursor,
seenAt,
items: notifsGrouped,
}
}
+7 -1
View File
@@ -291,7 +291,13 @@ export function usePostFeedQuery(
}
}
if (!isFetching && hasNextPage && count < PAGE_SIZE && numEmpties < 3) {
if (
!isFetching &&
hasNextPage &&
count < PAGE_SIZE &&
numEmpties < 3 &&
(data?.pages.length || 0) < 6
) {
query.fetchNextPage()
}
}, [query])
+9
View File
@@ -361,6 +361,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
if (canReusePrevSession) {
logger.info(`session: attempting to reuse previous session`)
agent.session = prevSession
__globalAgent = agent
queryClient.clear()
@@ -370,6 +372,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
resumeSessionWithFreshAccount()
.then(freshAccount => {
if (JSON.stringify(account) !== JSON.stringify(freshAccount)) {
logger.info(
`session: reuse of previous session returned a fresh account, upserting`,
)
upsertAccount(freshAccount)
}
})
@@ -385,6 +390,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
__globalAgent = PUBLIC_BSKY_AGENT
})
} else {
logger.info(`session: attempting to resume using previous session`)
try {
const freshAccount = await resumeSessionWithFreshAccount()
__globalAgent = agent
@@ -404,6 +411,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
logger.info(`session: resumeSessionWithFreshAccount`)
await networkRetry(1, () => agent.resumeSession(prevSession))
/*
+25 -21
View File
@@ -7,13 +7,15 @@ import {useNavigation} from '@react-navigation/native'
import {useAnalytics} from 'lib/analytics/analytics'
import {useQueryClient} from '@tanstack/react-query'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {MainScrollProvider} from '../util/MainScrollProvider'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useSetMinimalShellMode} from '#/state/shell'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {ComposeIcon2} from 'lib/icons'
import {colors, s} from 'lib/styles'
import {FlatList, View, useWindowDimensions} from 'react-native'
import {View, useWindowDimensions} from 'react-native'
import {ListMethods} from '../util/List'
import {Feed} from '../posts/Feed'
import {TextLink} from '../util/Link'
import {FAB} from '../util/fab/FAB'
@@ -51,10 +53,11 @@ export function FeedPage({
const {isDesktop} = useWebMediaQueries()
const queryClient = useQueryClient()
const {openComposer} = useComposerControls()
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const setMinimalShellMode = useSetMinimalShellMode()
const {screen, track} = useAnalytics()
const headerOffset = useHeaderOffset()
const scrollElRef = React.useRef<FlatList>(null)
const scrollElRef = React.useRef<ListMethods>(null)
const [hasNew, setHasNew] = React.useState(false)
const scrollToTop = React.useCallback(() => {
@@ -62,8 +65,8 @@ export function FeedPage({
animated: isNative,
offset: -headerOffset,
})
resetMainScroll()
}, [headerOffset, resetMainScroll])
setMinimalShellMode(false)
}, [headerOffset, setMinimalShellMode])
const onSoftReset = React.useCallback(() => {
const isScreenFocused =
@@ -164,21 +167,22 @@ export function FeedPage({
return (
<View testID={testID} style={s.h100pct}>
<Feed
testID={testID ? `${testID}-feed` : undefined}
enabled={isPageFocused}
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
scrollElRef={scrollElRef}
onScroll={onMainScroll}
onHasNew={setHasNew}
scrollEventThrottle={1}
renderEmptyState={renderEmptyState}
renderEndOfFeed={renderEndOfFeed}
ListHeaderComponent={ListHeaderComponent}
headerOffset={headerOffset}
/>
<MainScrollProvider>
<Feed
testID={testID ? `${testID}-feed` : undefined}
enabled={isPageFocused}
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
scrollElRef={scrollElRef}
onScrolledDownChange={setIsScrolledDown}
onHasNew={setHasNew}
renderEmptyState={renderEmptyState}
renderEndOfFeed={renderEndOfFeed}
ListHeaderComponent={ListHeaderComponent}
headerOffset={headerOffset}
/>
</MainScrollProvider>
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
onPress={onPressLoadLatest}
+5 -21
View File
@@ -1,4 +1,4 @@
import React, {MutableRefObject} from 'react'
import React from 'react'
import {
Dimensions,
RefreshControl,
@@ -8,18 +8,16 @@ import {
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views'
import {List, ListRef} from '../util/List'
import {FeedSourceCardLoaded} from './FeedSourceCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from '#/lib/ThemeContext'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {hydrateFeedGenerator} from '#/state/queries/feed'
@@ -37,9 +35,7 @@ interface SectionRef {
interface ProfileFeedgensProps {
did: string
scrollElRef: MutableRefObject<FlatList<any> | null>
onScroll?: OnScrollHandler
scrollEventThrottle?: number
scrollElRef: ListRef
headerOffset: number
enabled?: boolean
style?: StyleProp<ViewStyle>
@@ -50,16 +46,7 @@ export const ProfileFeedgens = React.forwardRef<
SectionRef,
ProfileFeedgensProps
>(function ProfileFeedgensImpl(
{
did,
scrollElRef,
onScroll,
scrollEventThrottle,
headerOffset,
enabled,
style,
testID,
},
{did, scrollElRef, headerOffset, enabled, style, testID},
ref,
) {
const pal = usePalette('default')
@@ -185,10 +172,9 @@ export const ProfileFeedgens = React.forwardRef<
[error, refetch, onPressRetryLoadMore, pal, preferences],
)
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return (
<View testID={testID} style={style}>
<FlatList
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
@@ -207,8 +193,6 @@ export const ProfileFeedgens = React.forwardRef<
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined}
scrollEventThrottle={scrollEventThrottle}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
+7 -13
View File
@@ -1,4 +1,4 @@
import React, {MutableRefObject} from 'react'
import React from 'react'
import {
ActivityIndicator,
Dimensions,
@@ -8,7 +8,7 @@ import {
ViewStyle,
} from 'react-native'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {FlatList} from '../util/Views'
import {List, ListRef} from '../util/List'
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@@ -18,10 +18,8 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useListMembersQuery} from '#/state/queries/list-members'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useSession} from '#/state/session'
import {cleanError} from '#/lib/strings/errors'
@@ -34,24 +32,22 @@ export function ListMembers({
list,
style,
scrollElRef,
onScroll,
onScrolledDownChange,
onPressTryAgain,
renderHeader,
renderEmptyState,
testID,
scrollEventThrottle,
headerOffset = 0,
desktopFixedHeightOffset,
}: {
list: string
style?: StyleProp<ViewStyle>
scrollElRef?: MutableRefObject<FlatList<any> | null>
onScroll: OnScrollHandler
scrollElRef?: ListRef
onScrolledDownChange: (isScrolledDown: boolean) => void
onPressTryAgain?: () => void
renderHeader: () => JSX.Element
renderEmptyState: () => JSX.Element
testID?: string
scrollEventThrottle?: number
headerOffset?: number
desktopFixedHeightOffset?: number
}) {
@@ -209,10 +205,9 @@ export function ListMembers({
[isFetching],
)
const scrollHandler = useAnimatedScrollHandler(onScroll)
return (
<View testID={testID} style={style}>
<FlatList
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
@@ -233,10 +228,9 @@ export function ListMembers({
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScroll={scrollHandler}
onScrolledDownChange={onScrolledDownChange}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
scrollEventThrottle={scrollEventThrottle}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf
+2 -2
View File
@@ -15,7 +15,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {FlatList} from '../util/Views'
import {List} from '../util/List'
import {s} from 'lib/styles'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
@@ -119,7 +119,7 @@ export function MyLists({
[error, onRefresh, renderItem, pal],
)
const FlatListCom = inline ? RNFlatList : FlatList
const FlatListCom = inline ? RNFlatList : List
return (
<View testID={testID} style={style}>
{items.length > 0 && (
+5 -21
View File
@@ -1,4 +1,4 @@
import React, {MutableRefObject} from 'react'
import React from 'react'
import {
Dimensions,
RefreshControl,
@@ -8,7 +8,7 @@ import {
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views'
import {List, ListRef} from '../util/List'
import {ListCard} from './ListCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@@ -16,11 +16,9 @@ import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from '#/lib/ThemeContext'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {isNative} from '#/platform/detection'
@@ -36,9 +34,7 @@ interface SectionRef {
interface ProfileListsProps {
did: string
scrollElRef: MutableRefObject<FlatList<any> | null>
onScroll?: OnScrollHandler
scrollEventThrottle?: number
scrollElRef: ListRef
headerOffset: number
enabled?: boolean
style?: StyleProp<ViewStyle>
@@ -47,16 +43,7 @@ interface ProfileListsProps {
export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
function ProfileListsImpl(
{
did,
scrollElRef,
onScroll,
scrollEventThrottle,
headerOffset,
enabled,
style,
testID,
},
{did, scrollElRef, headerOffset, enabled, style, testID},
ref,
) {
const pal = usePalette('default')
@@ -187,10 +174,9 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
[error, refetch, onPressRetryLoadMore, pal],
)
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return (
<View testID={testID} style={style}>
<FlatList
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={items}
@@ -209,8 +195,6 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined}
scrollEventThrottle={scrollEventThrottle}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
+8
View File
@@ -22,6 +22,7 @@ import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useSession, useSessionApi, getAgent} from '#/state/session'
import {logger} from '#/logger'
export const snapPoints = ['90%']
@@ -45,6 +46,13 @@ export function Component({showReminder}: {showReminder?: boolean}) {
const {isMobile} = useWebMediaQueries()
const {openModal, closeModal} = useModalControls()
React.useEffect(() => {
if (!currentAccount) {
logger.error(`VerifyEmail modal opened without currentAccount`)
closeModal()
}
}, [currentAccount, closeModal])
const onSendEmail = async () => {
setError('')
setIsProcessing(true)
+8 -11
View File
@@ -1,13 +1,11 @@
import React, {MutableRefObject} from 'react'
import {CenteredView, FlatList} from '../util/Views'
import React from 'react'
import {CenteredView} from '../util/Views'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {FeedItem} from './FeedItem'
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {EmptyState} from '../util/EmptyState'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
@@ -15,6 +13,7 @@ import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
import {useModerationOpts} from '#/state/queries/preferences'
import {List, ListRef} from '../util/List'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
@@ -23,12 +22,12 @@ const LOADING_ITEM = {_reactKey: '__loading__'}
export function Feed({
scrollElRef,
onPressTryAgain,
onScroll,
onScrolledDownChange,
ListHeaderComponent,
}: {
scrollElRef?: MutableRefObject<FlatList<any> | null>
scrollElRef?: ListRef
onPressTryAgain?: () => void
onScroll?: OnScrollHandler
onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element
}) {
const pal = usePalette('default')
@@ -135,7 +134,6 @@ export function Feed({
[isFetchingNextPage],
)
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return (
<View style={s.hContentRegion}>
{error && (
@@ -146,7 +144,7 @@ export function Feed({
/>
</CenteredView>
)}
<FlatList
<List
testID="notifsFeed"
ref={scrollElRef}
data={items}
@@ -164,8 +162,7 @@ export function Feed({
}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
onScroll={scrollHandler}
scrollEventThrottle={1}
onScrolledDownChange={onScrolledDownChange}
contentContainerStyle={s.contentContainer}
// @ts-ignore our .web version only -prf
desktopFixedHeight
+36 -44
View File
@@ -1,7 +1,6 @@
import * as React from 'react'
import {
LayoutChangeEvent,
FlatList,
ScrollView,
StyleSheet,
View,
@@ -19,17 +18,14 @@ import Animated, {
} from 'react-native-reanimated'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {TabBar} from './TabBar'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
const SCROLLED_DOWN_LIMIT = 200
import {ListMethods} from '../util/List'
import {ScrollProvider} from '#/lib/ScrollContext'
export interface PagerWithHeaderChildParams {
headerHeight: number
isFocused: boolean
onScroll: OnScrollHandler
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null>
scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null>
}
export interface PagerWithHeaderProps {
@@ -61,7 +57,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
const [currentPage, setCurrentPage] = React.useState(0)
const [tabBarHeight, setTabBarHeight] = React.useState(0)
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const scrollY = useSharedValue(0)
const headerHeight = headerOnlyHeight + tabBarHeight
@@ -120,13 +115,16 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
)
const scrollRefs = useSharedValue<AnimatedRef<any>[]>([])
const registerRef = (scrollRef: AnimatedRef<any>, index: number) => {
scrollRefs.modify(refs => {
'worklet'
refs[index] = scrollRef
return refs
})
}
const registerRef = React.useCallback(
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
scrollRefs.modify(refs => {
'worklet'
refs[atIndex] = scrollRef
return refs
})
},
[scrollRefs],
)
const lastForcedScrollY = useSharedValue(0)
const adjustScrollForOtherPages = () => {
@@ -137,8 +135,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
lastForcedScrollY.value = forcedScrollY
const refs = scrollRefs.value
for (let i = 0; i < refs.length; i++) {
if (i !== currentPage) {
// This needs to run on the UI thread.
if (i !== currentPage && refs[i] != null) {
scrollTo(refs[i], 0, forcedScrollY, false)
}
}
@@ -152,15 +149,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
if (!throttleTimeout.current) {
throttleTimeout.current = setTimeout(() => {
throttleTimeout.current = null
runOnUI(adjustScrollForOtherPages)()
const nextIsScrolledDown = scrollY.value > SCROLLED_DOWN_LIMIT
if (isScrolledDown !== nextIsScrolledDown) {
React.startTransition(() => {
setIsScrolledDown(nextIsScrolledDown)
})
}
}, 80 /* Sync often enough you're unlikely to catch it unsynced */)
}
})
@@ -205,11 +194,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
<View key={i} collapsable={false}>
<PagerItem
headerHeight={headerHeight}
index={i}
isReady={isReady}
isFocused={i === currentPage}
isScrolledDown={isScrolledDown}
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
registerRef={(r: AnimatedRef<any>) => registerRef(r, i)}
registerRef={registerRef}
renderTab={child}
/>
</View>
@@ -282,42 +271,45 @@ PagerTabBar = React.memo(PagerTabBar)
function PagerItem({
headerHeight,
index,
isReady,
isFocused,
isScrolledDown,
onScrollWorklet,
renderTab,
registerRef,
}: {
headerHeight: number
index: number
isFocused: boolean
isReady: boolean
isScrolledDown: boolean
registerRef: (scrollRef: AnimatedRef<any>) => void
registerRef: (scrollRef: AnimatedRef<any> | null, atIndex: number) => void
onScrollWorklet: (e: NativeScrollEvent) => void
renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null
}) {
const scrollElRef = useAnimatedRef()
registerRef(scrollElRef)
const scrollHandler = React.useMemo(
() => ({onScroll: onScrollWorklet}),
[onScrollWorklet],
)
React.useEffect(() => {
registerRef(scrollElRef, index)
return () => {
registerRef(null, index)
}
}, [scrollElRef, registerRef, index])
if (!isReady || renderTab == null) {
return null
}
return renderTab({
headerHeight,
isFocused,
isScrolledDown,
onScroll: scrollHandler,
scrollElRef: scrollElRef as React.MutableRefObject<
FlatList<any> | ScrollView | null
>,
})
return (
<ScrollProvider onScroll={onScrollWorklet}>
{renderTab({
headerHeight,
isFocused,
scrollElRef: scrollElRef as React.MutableRefObject<
ListMethods | ScrollView | null
>,
})}
</ScrollProvider>
)
}
const styles = StyleSheet.create({
+5 -65
View File
@@ -1,26 +1,15 @@
import * as React from 'react'
import {
FlatList,
ScrollView,
StyleSheet,
View,
NativeScrollEvent,
} from 'react-native'
import {useSharedValue, runOnJS, useAnimatedRef} from 'react-native-reanimated'
import {FlatList, ScrollView, StyleSheet, View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {TabBar} from './TabBar'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
const SCROLLED_DOWN_LIMIT = 200
import {ListMethods} from '../util/List'
export interface PagerWithHeaderChildParams {
headerHeight: number
isFocused: boolean
onScroll: OnScrollHandler
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null>
}
@@ -50,8 +39,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
ref,
) {
const [currentPage, setCurrentPage] = React.useState(0)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const scrollY = useSharedValue(0)
const renderTabBar = React.useCallback(
(props: RenderTabBarFnProps) => {
@@ -69,34 +56,6 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
[items, renderHeader, currentPage, onCurrentPageSelected, testID],
)
const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
null,
)
const queueThrottledOnScroll = useNonReactiveCallback(() => {
if (!throttleTimeout.current) {
throttleTimeout.current = setTimeout(() => {
throttleTimeout.current = null
const nextIsScrolledDown = scrollY.value > SCROLLED_DOWN_LIMIT
if (isScrolledDown !== nextIsScrolledDown) {
React.startTransition(() => {
setIsScrolledDown(nextIsScrolledDown)
})
}
}, 80)
}
})
const onScrollWorklet = React.useCallback(
(e: NativeScrollEvent) => {
'worklet'
const nextScrollY = e.contentOffset.y
scrollY.value = nextScrollY
runOnJS(queueThrottledOnScroll)()
},
[scrollY, queueThrottledOnScroll],
)
const onPageSelectedInner = React.useCallback(
(index: number) => {
setCurrentPage(index)
@@ -123,12 +82,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
.map((child, i) => {
return (
<View key={i} collapsable={false}>
<PagerItem
isFocused={i === currentPage}
isScrolledDown={isScrolledDown}
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
renderTab={child}
/>
<PagerItem isFocused={i === currentPage} renderTab={child} />
</View>
)
})}
@@ -182,30 +136,20 @@ PagerTabBar = React.memo(PagerTabBar)
function PagerItem({
isFocused,
isScrolledDown,
onScrollWorklet,
renderTab,
}: {
isFocused: boolean
isScrolledDown: boolean
onScrollWorklet: (e: NativeScrollEvent) => void
renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null
}) {
const scrollElRef = useAnimatedRef()
const scrollHandler = React.useMemo(
() => ({onScroll: onScrollWorklet}),
[onScrollWorklet],
)
if (renderTab == null) {
return null
}
return renderTab({
headerHeight: 0,
isFocused,
isScrolledDown,
onScroll: scrollHandler,
scrollElRef: scrollElRef as React.MutableRefObject<
FlatList<any> | ScrollView | null
ListMethods | ScrollView | null
>,
})
}
@@ -238,10 +182,6 @@ const styles = StyleSheet.create({
},
})
function noop() {
'worklet'
}
function toArray<T>(v: T | T[]): T[] {
if (Array.isArray(v)) {
return v
+3 -2
View File
@@ -1,7 +1,8 @@
import React, {useCallback, useMemo, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
@@ -84,7 +85,7 @@ export function PostLikedBy({uri}: {uri: string}) {
// loaded
// =
return (
<FlatList
<List
data={likes}
keyExtractor={item => item.actor.did}
refreshControl={
+3 -2
View File
@@ -1,7 +1,8 @@
import React, {useMemo, useCallback, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
@@ -85,7 +86,7 @@ export function PostRepostedBy({uri}: {uri: string}) {
// loaded
// =
return (
<FlatList
<List
data={repostedBy}
keyExtractor={item => item.did}
refreshControl={
+4 -3
View File
@@ -8,7 +8,8 @@ import {
View,
} from 'react-native'
import {AppBskyFeedDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List, ListMethods} from '../util/List'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -140,7 +141,7 @@ function PostThreadLoaded({
const {_} = useLingui()
const pal = usePalette('default')
const {isTablet, isDesktop} = useWebMediaQueries()
const ref = useRef<FlatList>(null)
const ref = useRef<ListMethods>(null)
const highlightedPostRef = useRef<View | null>(null)
const needsScrollAdjustment = useRef<boolean>(
!isNative || // web always uses scroll adjustment
@@ -335,7 +336,7 @@ function PostThreadLoaded({
)
return (
<FlatList
<List
ref={ref}
data={posts}
initialNumToRender={!isNative ? posts.length : undefined}
+8 -15
View File
@@ -1,4 +1,4 @@
import React, {memo, MutableRefObject} from 'react'
import React, {memo} from 'react'
import {
ActivityIndicator,
AppState,
@@ -10,15 +10,13 @@ import {
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {FlatList} from '../util/Views'
import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from 'lib/ThemeContext'
import {logger} from '#/logger'
import {
@@ -45,9 +43,8 @@ let Feed = ({
enabled,
pollInterval,
scrollElRef,
onScroll,
onScrolledDownChange,
onHasNew,
scrollEventThrottle,
renderEmptyState,
renderEndOfFeed,
testID,
@@ -62,10 +59,9 @@ let Feed = ({
style?: StyleProp<ViewStyle>
enabled?: boolean
pollInterval?: number
scrollElRef?: MutableRefObject<FlatList<any> | null>
scrollElRef?: ListRef
onHasNew?: (v: boolean) => void
onScroll?: OnScrollHandler
scrollEventThrottle?: number
onScrolledDownChange?: (isScrolledDown: boolean) => void
renderEmptyState: () => JSX.Element
renderEndOfFeed?: () => JSX.Element
testID?: string
@@ -167,8 +163,7 @@ let Feed = ({
if (isFetched) {
if (isError && isEmpty) {
arr = arr.concat([ERROR_ITEM])
}
if (isEmpty) {
} else if (isEmpty) {
arr = arr.concat([EMPTY_FEED_ITEM])
} else if (data) {
for (const page of data?.pages) {
@@ -271,10 +266,9 @@ let Feed = ({
)
}, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset])
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return (
<View testID={testID} style={style}>
<FlatList
<List
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={feedItems}
@@ -295,8 +289,7 @@ let Feed = ({
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScroll={onScroll != null ? scrollHandler : undefined}
scrollEventThrottle={scrollEventThrottle}
onScrolledDownChange={onScrolledDownChange}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
onEndReached={onEndReached}
onEndReachedThreshold={2} // number of posts left to trigger load more
+3 -2
View File
@@ -1,7 +1,8 @@
import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
@@ -86,7 +87,7 @@ export function ProfileFollowers({name}: {name: string}) {
// loaded
// =
return (
<FlatList
<List
data={followers}
keyExtractor={item => item.did}
refreshControl={
+3 -2
View File
@@ -1,7 +1,8 @@
import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
@@ -86,7 +87,7 @@ export function ProfileFollows({name}: {name: string}) {
// loaded
// =
return (
<FlatList
<List
data={follows}
keyExtractor={item => item.did}
refreshControl={
+64
View File
@@ -0,0 +1,64 @@
import React, {memo, startTransition} from 'react'
import {FlatListProps} from 'react-native'
import {FlatList_INTERNAL} from './Views'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {runOnJS, useSharedValue} from 'react-native-reanimated'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
export type ListMethods = FlatList_INTERNAL
export type ListProps<ItemT> = Omit<
FlatListProps<ItemT>,
'onScroll' // Use ScrollContext instead.
> & {
onScrolledDownChange?: (isScrolledDown: boolean) => void
}
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
const SCROLLED_DOWN_LIMIT = 200
function ListImpl<ItemT>(
{onScrolledDownChange, ...props}: ListProps<ItemT>,
ref: React.Ref<ListMethods>,
) {
const isScrolledDown = useSharedValue(false)
const contextScrollHandlers = useScrollHandlers()
function handleScrolledDownChange(didScrollDown: boolean) {
startTransition(() => {
onScrolledDownChange?.(didScrollDown)
})
}
const scrollHandler = useAnimatedScrollHandler({
onBeginDrag(e, ctx) {
contextScrollHandlers.onBeginDrag?.(e, ctx)
},
onEndDrag(e, ctx) {
contextScrollHandlers.onEndDrag?.(e, ctx)
},
onScroll(e, ctx) {
contextScrollHandlers.onScroll?.(e, ctx)
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
if (isScrolledDown.value !== didScrollDown) {
isScrolledDown.value = didScrollDown
if (onScrolledDownChange != null) {
runOnJS(handleScrolledDownChange)(didScrollDown)
}
}
},
})
return (
<FlatList_INTERNAL
{...props}
onScroll={scrollHandler}
scrollEventThrottle={1}
ref={ref}
/>
)
}
export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
props: ListProps<ItemT> & {ref?: React.Ref<ListMethods>},
) => React.ReactElement
@@ -1,30 +1,18 @@
import {useState, useCallback, useMemo} from 'react'
import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native'
import React, {useCallback} from 'react'
import {ScrollProvider} from '#/lib/ScrollContext'
import {NativeScrollEvent} from 'react-native'
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
import {useShellLayout} from '#/state/shell/shell-layout'
import {s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {
useSharedValue,
interpolate,
runOnJS,
ScrollHandlers,
} from 'react-native-reanimated'
import {useSharedValue, interpolate} from 'react-native-reanimated'
function clamp(num: number, min: number, max: number) {
'worklet'
return Math.min(Math.max(num, min), max)
}
export type OnScrollCb = (
event: NativeSyntheticEvent<NativeScrollEvent>,
) => void
export type OnScrollHandler = ScrollHandlers<any>
export type ResetCb = () => void
export function useOnMainScroll(): [OnScrollHandler, boolean, ResetCb] {
export function MainScrollProvider({children}: {children: React.ReactNode}) {
const {headerHeight} = useShellLayout()
const [isScrolledDown, setIsScrolledDown] = useState(false)
const mode = useMinimalShellMode()
const setMode = useSetMinimalShellMode()
const startDragOffset = useSharedValue<number | null>(null)
@@ -58,13 +46,6 @@ export function useOnMainScroll(): [OnScrollHandler, boolean, ResetCb] {
const onScroll = useCallback(
(e: NativeScrollEvent) => {
'worklet'
// Keep track of whether we want to show "scroll to top".
if (!isScrolledDown && e.contentOffset.y > s.window.height) {
runOnJS(setIsScrolledDown)(true)
} else if (isScrolledDown && e.contentOffset.y < s.window.height) {
runOnJS(setIsScrolledDown)(false)
}
if (startDragOffset.value === null || startMode.value === null) {
if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) {
// If we're close enough to the top, always show the shell.
@@ -102,24 +83,15 @@ export function useOnMainScroll(): [OnScrollHandler, boolean, ResetCb] {
startMode.value = mode.value
}
},
[headerHeight, mode, setMode, isScrolledDown, startDragOffset, startMode],
[headerHeight, mode, setMode, startDragOffset, startMode],
)
const scrollHandler: ScrollHandlers<any> = useMemo(
() => ({
onBeginDrag,
onEndDrag,
onScroll,
}),
[onBeginDrag, onEndDrag, onScroll],
return (
<ScrollProvider
onBeginDrag={onBeginDrag}
onEndDrag={onEndDrag}
onScroll={onScroll}>
{children}
</ScrollProvider>
)
return [
scrollHandler,
isScrolledDown,
useCallback(() => {
setIsScrolledDown(false)
setMode(false)
}, [setMode]),
]
}
+6 -5
View File
@@ -1,13 +1,14 @@
import React, {useEffect, useState} from 'react'
import {
NativeSyntheticEvent,
NativeScrollEvent,
Pressable,
RefreshControl,
StyleSheet,
View,
ScrollView,
} from 'react-native'
import {FlatList} from './Views'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {FlatList_INTERNAL} from './Views'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {Text} from './text/Text'
import {usePalette} from 'lib/hooks/usePalette'
@@ -38,7 +39,7 @@ export const ViewSelector = React.forwardRef<
| null
| undefined
onSelectView?: (viewIndex: number) => void
onScroll?: OnScrollCb
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
onRefresh?: () => void
onEndReached?: (info: {distanceFromEnd: number}) => void
}
@@ -59,7 +60,7 @@ export const ViewSelector = React.forwardRef<
) {
const pal = usePalette('default')
const [selectedIndex, setSelectedIndex] = useState<number>(0)
const flatListRef = React.useRef<FlatList>(null)
const flatListRef = React.useRef<FlatList_INTERNAL>(null)
// events
// =
@@ -110,7 +111,7 @@ export const ViewSelector = React.forwardRef<
[items],
)
return (
<FlatList
<FlatList_INTERNAL
ref={flatListRef}
data={data}
keyExtractor={keyExtractor}
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react'
import {ViewProps} from 'react-native'
export {FlatList, ScrollView} from 'react-native'
export {FlatList as FlatList_INTERNAL, ScrollView} from 'react-native'
export function CenteredView({
style,
sideBorders,
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {View} from 'react-native'
import Animated from 'react-native-reanimated'
export const FlatList = Animated.FlatList
export const FlatList_INTERNAL = Animated.FlatList
export const ScrollView = Animated.ScrollView
export function CenteredView(props) {
return <View {...props} />
+1 -1
View File
@@ -49,7 +49,7 @@ export function CenteredView({
return <View style={style} {...props} />
}
export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
{
data,
extraData,
+2 -2
View File
@@ -19,7 +19,7 @@ import {
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import debounce from 'lodash.debounce'
import {Text} from 'view/com/util/text/Text'
import {FlatList} from 'view/com/util/Views'
import {List} from 'view/com/util/List'
import {useFocusEffect} from '@react-navigation/native'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {Trans, msg} from '@lingui/macro'
@@ -481,7 +481,7 @@ export function FeedsScreen(_props: Props) {
{preferences ? <View /> : <ActivityIndicator />}
<FlatList
<List
style={[!isTabletOrDesktop && s.flex1, styles.list]}
data={items}
keyExtractor={item => item.key}
+14 -11
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {FlatList, View} from 'react-native'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {
@@ -9,8 +9,9 @@ import {
import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed'
import {TextLink} from 'view/com/util/Link'
import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {MainScrollProvider} from '../com/util/MainScrollProvider'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s, colors} from 'lib/styles'
@@ -35,8 +36,8 @@ type Props = NativeStackScreenProps<
export function NotificationsScreen({}: Props) {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
const scrollElRef = React.useRef<FlatList>(null)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const scrollElRef = React.useRef<ListMethods>(null)
const checkLatestRef = React.useRef<() => void | null>()
const {screen} = useAnalytics()
const pal = usePalette('default')
@@ -50,8 +51,8 @@ export function NotificationsScreen({}: Props) {
// =
const scrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({animated: isNative, offset: 0})
resetMainScroll()
}, [scrollElRef, resetMainScroll])
setMinimalShellMode(false)
}, [scrollElRef, setMinimalShellMode])
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
@@ -130,11 +131,13 @@ export function NotificationsScreen({}: Props) {
return (
<View testID="notificationsScreen" style={s.hContentRegion}>
<ViewHeader title={_(msg`Notifications`)} canGoBack={false} />
<Feed
onScroll={onMainScroll}
scrollElRef={scrollElRef}
ListHeaderComponent={ListHeaderComponent}
/>
<MainScrollProvider>
<Feed
onScrolledDownChange={setIsScrolledDown}
scrollElRef={scrollElRef}
ListHeaderComponent={ListHeaderComponent}
/>
</MainScrollProvider>
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
onPress={onPressLoadLatest}
+18 -64
View File
@@ -5,7 +5,8 @@ import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {CenteredView, FlatList} from '../com/util/Views'
import {CenteredView} from '../com/util/Views'
import {ListRef} from '../com/util/List'
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
import {Feed} from 'view/com/posts/Feed'
import {ProfileLists} from '../com/lists/ProfileLists'
@@ -20,7 +21,6 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {ComposeIcon2} from 'lib/icons'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {combinedDisplayName} from 'lib/strings/display-names'
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useProfileQuery} from '#/state/queries/profile'
@@ -277,103 +277,67 @@ function ProfileScreenLoaded({
onPageSelected={onPageSelected}
onCurrentPageSelected={onCurrentPageSelected}
renderHeader={renderHeader}>
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
{({headerHeight, isFocused, scrollElRef}) => (
<FeedSection
ref={postsSectionRef}
feed={`author|${profile.did}|posts_and_author_threads`}
onScroll={onScroll}
headerHeight={headerHeight}
isFocused={isFocused}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
/>
)}
{showRepliesTab
? ({
onScroll,
headerHeight,
isFocused,
isScrolledDown,
scrollElRef,
}) => (
? ({headerHeight, isFocused, scrollElRef}) => (
<FeedSection
ref={repliesSectionRef}
feed={`author|${profile.did}|posts_with_replies`}
onScroll={onScroll}
headerHeight={headerHeight}
isFocused={isFocused}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
/>
)
: null}
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
{({headerHeight, isFocused, scrollElRef}) => (
<FeedSection
ref={mediaSectionRef}
feed={`author|${profile.did}|posts_with_media`}
onScroll={onScroll}
headerHeight={headerHeight}
isFocused={isFocused}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
/>
)}
{showLikesTab
? ({
onScroll,
headerHeight,
isFocused,
isScrolledDown,
scrollElRef,
}) => (
? ({headerHeight, isFocused, scrollElRef}) => (
<FeedSection
ref={likesSectionRef}
feed={`likes|${profile.did}`}
onScroll={onScroll}
headerHeight={headerHeight}
isFocused={isFocused}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
ignoreFilterFor={profile.did}
/>
)
: null}
{showFeedsTab
? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileFeedgens
ref={feedsSectionRef}
did={profile.did}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onScroll={onScroll}
scrollEventThrottle={1}
scrollElRef={scrollElRef as ListRef}
headerOffset={headerHeight}
enabled={isFocused}
/>
)
: null}
{showListsTab
? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
? ({headerHeight, isFocused, scrollElRef}) => (
<ProfileLists
ref={listsSectionRef}
did={profile.did}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onScroll={onScroll}
scrollEventThrottle={1}
scrollElRef={scrollElRef as ListRef}
headerOffset={headerHeight}
enabled={isFocused}
/>
@@ -396,28 +360,19 @@ function ProfileScreenLoaded({
interface FeedSectionProps {
feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number
isFocused: boolean
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | null>
scrollElRef: ListRef
ignoreFilterFor?: string
}
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl(
{
feed,
onScroll,
headerHeight,
isFocused,
isScrolledDown,
scrollElRef,
ignoreFilterFor,
},
{feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
ref,
) {
const queryClient = useQueryClient()
const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const onScrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({
@@ -443,8 +398,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
feed={feed}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScroll={onScroll}
scrollEventThrottle={1}
onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight}
renderEndOfFeed={ProfileEndOfFeed}
+15 -31
View File
@@ -1,11 +1,5 @@
import React, {useMemo, useCallback} from 'react'
import {
Dimensions,
StyleSheet,
View,
ActivityIndicator,
FlatList,
} from 'react-native'
import {Dimensions, StyleSheet, View, ActivityIndicator} from 'react-native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
@@ -20,6 +14,7 @@ import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
import {Feed} from 'view/com/posts/Feed'
import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List'
import {Button} from 'view/com/util/forms/Button'
import {Text} from 'view/com/util/text/Text'
import {RichText} from 'view/com/util/text/RichText'
@@ -29,12 +24,13 @@ import {EmptyState} from 'view/com/util/EmptyState'
import * as Toast from 'view/com/util/Toast'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {shareUrl} from 'lib/sharing'
import {toShareUrl} from 'lib/strings/url-helpers'
import {Haptics} from 'lib/haptics'
import {useAnalytics} from 'lib/analytics/analytics'
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {makeCustomFeedLink} from 'lib/routes/links'
import {pluralize} from 'lib/strings/helpers'
import {CenteredView, ScrollView} from 'view/com/util/Views'
@@ -46,7 +42,6 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {
useFeedSourceInfoQuery,
FeedSourceFeedInfo,
@@ -403,17 +398,13 @@ export function ProfileFeedScreenInner({
isHeaderReady={true}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}) =>
{({headerHeight, scrollElRef, isFocused}) =>
isPublicResponse?.isPublic ? (
<FeedSection
ref={feedSectionRef}
feed={`feedgen|${feedInfo.uri}`}
onScroll={onScroll}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
isFocused={isFocused}
/>
) : (
@@ -422,13 +413,12 @@ export function ProfileFeedScreenInner({
</CenteredView>
)
}
{({onScroll, headerHeight, scrollElRef}) => (
{({headerHeight, scrollElRef}) => (
<AboutSection
feedOwnerDid={feedInfo.creatorDid}
feedRkey={feedInfo.route.params.rkey}
feedInfo={feedInfo}
headerHeight={headerHeight}
onScroll={onScroll}
scrollElRef={
scrollElRef as React.MutableRefObject<ScrollView | null>
}
@@ -497,18 +487,14 @@ function NonPublicFeedMessage({rawError}: {rawError?: Error}) {
interface FeedSectionProps {
feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | null>
scrollElRef: ListRef
isFocused: boolean
}
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl(
{feed, onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused},
ref,
) {
function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) {
const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const queryClient = useQueryClient()
const onScrollToTop = useCallback(() => {
@@ -536,8 +522,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
pollInterval={30e3}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScroll={onScroll}
scrollEventThrottle={5}
onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight}
/>
@@ -558,7 +543,6 @@ function AboutSection({
feedRkey,
feedInfo,
headerHeight,
onScroll,
scrollElRef,
isOwner,
}: {
@@ -566,13 +550,13 @@ function AboutSection({
feedRkey: string
feedInfo: FeedSourceFeedInfo
headerHeight: number
onScroll: OnScrollHandler
scrollElRef: React.MutableRefObject<ScrollView | null>
isOwner: boolean
}) {
const pal = usePalette('default')
const {_} = useLingui()
const scrollHandler = useAnimatedScrollHandler(onScroll)
const scrollHandlers = useScrollHandlers()
const onScroll = useAnimatedScrollHandler(scrollHandlers)
const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
const {hasSession} = useSession()
const {track} = useAnalytics()
@@ -608,12 +592,12 @@ function AboutSection({
return (
<ScrollView
ref={scrollElRef}
onScroll={onScroll}
scrollEventThrottle={1}
contentContainerStyle={{
paddingTop: headerHeight,
minHeight: Dimensions.get('window').height * 1.5,
}}
onScroll={scrollHandler}>
}}>
<View
style={[
{
+16 -47
View File
@@ -1,11 +1,5 @@
import React, {useCallback, useMemo} from 'react'
import {
ActivityIndicator,
FlatList,
Pressable,
StyleSheet,
View,
} from 'react-native'
import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {useNavigation} from '@react-navigation/native'
@@ -22,6 +16,7 @@ import {EmptyState} from 'view/com/util/EmptyState'
import {RichText} from 'view/com/util/text/RichText'
import {Button} from 'view/com/util/forms/Button'
import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List'
import * as Toast from 'view/com/util/Toast'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {FAB} from 'view/com/util/fab/FAB'
@@ -31,7 +26,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {NavigationProp} from 'lib/routes/types'
import {toShareUrl} from 'lib/strings/url-helpers'
import {shareUrl} from 'lib/sharing'
@@ -165,36 +159,22 @@ function ProfileListScreenLoaded({
isHeaderReady={true}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({
onScroll,
headerHeight,
isScrolledDown,
scrollElRef,
isFocused,
}) => (
{({headerHeight, scrollElRef, isFocused}) => (
<FeedSection
ref={feedSectionRef}
feed={`list|${uri}`}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
onScroll={onScroll}
scrollElRef={scrollElRef as ListRef}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
isFocused={isFocused}
/>
)}
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
{({headerHeight, scrollElRef}) => (
<AboutSection
ref={aboutSectionRef}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
list={list}
onPressAddUser={onPressAddUser}
onScroll={onScroll}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
/>
)}
</PagerWithHeader>
@@ -221,16 +201,12 @@ function ProfileListScreenLoaded({
items={SECTION_TITLES_MOD}
isHeaderReady={true}
renderHeader={renderHeader}>
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
{({headerHeight, scrollElRef}) => (
<AboutSection
list={list}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
scrollElRef={scrollElRef as ListRef}
onPressAddUser={onPressAddUser}
onScroll={onScroll}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
/>
)}
</PagerWithHeader>
@@ -615,19 +591,15 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
interface FeedSectionProps {
feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | null>
scrollElRef: ListRef
isFocused: boolean
}
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl(
{feed, scrollElRef, onScroll, headerHeight, isScrolledDown, isFocused},
ref,
) {
function FeedSectionImpl({feed, scrollElRef, headerHeight, isFocused}, ref) {
const queryClient = useQueryClient()
const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({
@@ -654,8 +626,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
pollInterval={30e3}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScroll={onScroll}
scrollEventThrottle={1}
onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight}
/>
@@ -674,20 +645,19 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
interface AboutSectionProps {
list: AppBskyGraphDefs.ListView
onPressAddUser: () => void
onScroll: OnScrollHandler
headerHeight: number
isScrolledDown: boolean
scrollElRef: React.MutableRefObject<FlatList<any> | null>
scrollElRef: ListRef
}
const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
function AboutSectionImpl(
{list, onPressAddUser, onScroll, headerHeight, isScrolledDown, scrollElRef},
{list, onPressAddUser, headerHeight, scrollElRef},
ref,
) {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const {currentAccount} = useSession()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
const isOwner = list.creator.did === currentAccount?.did
@@ -817,8 +787,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
renderHeader={renderHeader}
renderEmptyState={renderEmptyState}
headerOffset={headerHeight}
onScroll={onScroll}
scrollEventThrottle={1}
onScrolledDownChange={setIsScrolledDown}
/>
{isScrolledDown && (
<LoadLatestBtn
+5 -4
View File
@@ -8,7 +8,8 @@ import {
Pressable,
Platform,
} from 'react-native'
import {FlatList, ScrollView, CenteredView} from '#/view/com/util/Views'
import {ScrollView, CenteredView} from '#/view/com/util/Views'
import {List} from '#/view/com/util/List'
import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -155,7 +156,7 @@ function SearchScreenSuggestedFollows() {
}, [currentAccount, setSuggestions, getSuggestedFollowsByActor])
return suggestions.length ? (
<FlatList
<List
data={suggestions}
renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} noBg />}
keyExtractor={item => item.did}
@@ -243,7 +244,7 @@ function SearchScreenPostResults({query}: {query: string}) {
{isFetched ? (
<>
{posts.length ? (
<FlatList
<List
data={items}
renderItem={({item}) => {
if (item.type === 'post') {
@@ -284,7 +285,7 @@ function SearchScreenUserResults({query}: {query: string}) {
return isFetched && results ? (
<>
{results.length ? (
<FlatList
<List
data={results}
renderItem={({item}) => (
<ProfileCardWithFollowBtn profile={item} noBg />
+67 -69
View File
@@ -15,7 +15,6 @@ import {useLingui} from '@lingui/react'
import {Plural, Trans, msg, plural} from '@lingui/macro'
import {useSession} from '#/state/session'
import {useInviteCodesQuery} from '#/state/queries/invites'
import {ScrollView} from '#/view/com/util/Views'
export function DesktopRightNav() {
const pal = usePalette('default')
@@ -30,77 +29,75 @@ export function DesktopRightNav() {
return (
<View style={[styles.rightNav, pal.view]}>
<ScrollView contentContainerStyle={{borderWidth: 0}}>
<View style={{paddingVertical: 20}}>
<DesktopSearch />
<View style={{paddingVertical: 20}}>
<DesktopSearch />
{hasSession && (
<View style={{paddingTop: 18, marginBottom: 18}}>
<DesktopFeeds />
</View>
)}
<View
style={[
styles.message,
{
paddingTop: hasSession ? 0 : 18,
},
]}>
{isSandbox ? (
<View style={[palError.view, styles.messageLine, s.p10]}>
<Text type="md" style={[palError.text, s.bold]}>
SANDBOX. Posts and accounts are not permanent.
</Text>
</View>
) : undefined}
<View style={[s.flexRow]}>
{hasSession && (
<>
<TextLink
type="md"
style={pal.link}
href={FEEDBACK_FORM_URL({
email: currentAccount?.email,
handle: currentAccount?.handle,
})}
text={_(msg`Feedback`)}
/>
<Text type="md" style={pal.textLight}>
&nbsp;&middot;&nbsp;
</Text>
</>
)}
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/privacy-policy"
text={_(msg`Privacy`)}
/>
<Text type="md" style={pal.textLight}>
&nbsp;&middot;&nbsp;
</Text>
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/tos"
text={_(msg`Terms`)}
/>
<Text type="md" style={pal.textLight}>
&nbsp;&middot;&nbsp;
</Text>
<TextLink
type="md"
style={pal.link}
href={HELP_DESK_URL}
text={_(msg`Help`)}
/>
</View>
{hasSession && (
<View style={{paddingTop: 18, marginBottom: 18}}>
<DesktopFeeds />
</View>
)}
{hasSession && <InviteCodes />}
<View
style={[
styles.message,
{
paddingTop: hasSession ? 0 : 18,
},
]}>
{isSandbox ? (
<View style={[palError.view, styles.messageLine, s.p10]}>
<Text type="md" style={[palError.text, s.bold]}>
SANDBOX. Posts and accounts are not permanent.
</Text>
</View>
) : undefined}
<View style={[s.flexRow]}>
{hasSession && (
<>
<TextLink
type="md"
style={pal.link}
href={FEEDBACK_FORM_URL({
email: currentAccount?.email,
handle: currentAccount?.handle,
})}
text={_(msg`Feedback`)}
/>
<Text type="md" style={pal.textLight}>
&nbsp;&middot;&nbsp;
</Text>
</>
)}
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/privacy-policy"
text={_(msg`Privacy`)}
/>
<Text type="md" style={pal.textLight}>
&nbsp;&middot;&nbsp;
</Text>
<TextLink
type="md"
style={pal.link}
href="https://blueskyweb.xyz/support/tos"
text={_(msg`Terms`)}
/>
<Text type="md" style={pal.textLight}>
&nbsp;&middot;&nbsp;
</Text>
<TextLink
type="md"
style={pal.link}
href={HELP_DESK_URL}
text={_(msg`Help`)}
/>
</View>
</View>
</ScrollView>
{hasSession && <InviteCodes />}
</View>
</View>
)
}
@@ -177,7 +174,8 @@ const styles = StyleSheet.create({
// @ts-ignore web only
left: 'calc(50vw + 320px)',
width: 304,
height: '100%',
maxHeight: '100%',
overflowY: 'auto',
},
message: {
+14
View File
@@ -48,6 +48,20 @@
typed-emitter "^2.1.0"
zod "^3.21.4"
"@atproto/api@^0.7.3":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.7.3.tgz#3224000353619970d5e397a157c6e189e195ef47"
integrity sha512-fKU+W+S4kKxClE6IcPBHPZAjcyBYxG28S0FW/bv3T/ZYDkNxGzDV4xuoHOyEDGtB30slltl5U83njuuRZs5xtw==
dependencies:
"@atproto/common-web" "^0.2.3"
"@atproto/lexicon" "^0.3.1"
"@atproto/syntax" "^0.1.5"
"@atproto/xrpc" "^0.4.1"
multiformats "^9.9.0"
tlds "^1.234.0"
typed-emitter "^2.1.0"
zod "^3.21.4"
"@atproto/aws@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.1.6.tgz#c6ecbfd92b325f3c5433688534d47f43358b415b"