Compare commits

...

7 Commits

Author SHA1 Message Date
Alex Benzer be2d4120c3 Update feed drag-and-drop handle icon 2026-01-03 20:18:24 -08:00
Alex Benzer f49065d421 Fix drag and drop UI on web 2026-01-01 19:36:42 -08:00
Alex Benzer 2e9f7f329f Merge /settings/saved-feeds into /feeds 2026-01-01 19:17:43 -08:00
Alex Benzer c88aa2056b Simplified edit feeds UI 2025-12-27 18:48:17 -08:00
hailey 699fdbac01 update actions (#9586)
* update actions

* update actions

* $

* procenv
2025-12-22 19:21:17 -08:00
pfrazee a14a9be127 Nightly source-language update 2025-12-19 02:44:03 +00:00
Eric Bailey 998ee78e3d Move NUXDialogs into shell (#9573) 2025-12-18 12:08:17 -06:00
19 changed files with 1413 additions and 1092 deletions
+6 -2
View File
@@ -79,13 +79,15 @@ jobs:
echo "$json" > google-services.json
- name: 🏗️ EAS Build
env:
PROFILE: ${{ inputs.profile || 'testflight-android' }}
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
--profile ${{ inputs.profile || 'testflight-android' }}
--profile $PROFILE
--local --output build.aab --non-interactive
- name: ✍️ Rename Testflight bundle
@@ -194,4 +196,6 @@ jobs:
- name: ✏️ Write commit hash to cache
if: ${{ inputs.profile == 'testflight-android' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
env:
GITHUB_SHA: ${{ github.sha }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
+6 -2
View File
@@ -91,13 +91,15 @@ jobs:
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
env:
PROFILE: ${{ inputs.profile || 'testflight' }}
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p ios
--profile ${{ inputs.profile || 'testflight' }}
--profile $PROFILE
--local --output build.tar.gz --non-interactive
- name: 📂 Extract build artifact
@@ -147,5 +149,7 @@ jobs:
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
env:
GITHUB_SHA: ${{ github.sha }}
if: ${{ inputs.profile == 'testflight' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
+15 -6
View File
@@ -39,10 +39,12 @@ jobs:
# Validate the version if one is supplied. This should generally happen if the update is for a production client
- name: 🧐 Validate version
env:
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
if: ${{ inputs.runtimeVersion }}
run: |
if [ -z "${{ inputs.runtimeVersion }}" ]; then
[[ "${{ inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
if [ -z "$RUNTIME_VERSION" ]; then
[[ "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
fi
- name: ⬇️ Checkout
@@ -103,12 +105,15 @@ jobs:
# eas.json not used here, set EXPO_PUBLIC_ENV
- name: Env
env:
CHANNEL: ${{ inputs.channel || 'testflight' }}
GITHUB_SHA: ${{ github.sha }}
id: env
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_ENV=${{ inputs.channel || 'testflight' }}" >> .env
echo "EXPO_PUBLIC_ENV=$CHANNEL" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
@@ -145,7 +150,7 @@ jobs:
- name: ✏️ Write commit hash to cache
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
@@ -249,7 +254,9 @@ jobs:
- name: ✏️ Write commit hash to cache
if: ${{ inputs.channel == 'testflight' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
env:
GITHUB_SHA: ${{ github.sha }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
buildIfNecessaryAndroid:
name: Build and Submit Android
@@ -363,5 +370,7 @@ jobs:
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
env:
GITHUB_SHA: ${{ github.sha }}
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
+7 -3
View File
@@ -32,7 +32,7 @@ jobs:
fi
if [[ "${{ github.event.comment.body }}" == *"ota"* ]]; then
if [[ "$COMMENT" == *"ota"* ]]; then
has_ota=true
else
has_ota=false
@@ -75,6 +75,8 @@ jobs:
steps:
- name: Get PR HEAD SHA
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
id: pr-info
uses: actions/github-script@v7
with:
@@ -82,7 +84,7 @@ jobs:
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.issue.number }}
pull_number: process.env.ISSUE_NUMBER,
});
console.log(`PR HEAD SHA: ${pr.data.head.sha}`);
@@ -184,13 +186,15 @@ jobs:
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
with:
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
number: ${{ github.event.issue.number }}
message: |
Your requested OTA deployment was successful! You may now apply it by either scanning the QR code or opening the deep link below in your browser:
<img src="https://bsky-qr.vercel.app?channel=pull-request-${{ github.event.issue.number }}" width=300 height=300>
<img src="https://bsky-qr.vercel.app?channel=pull-request-$ISSUE_NUMBER" width=300 height=300>
`bluesky://intent/apply-ota?channel=pull-request-${{ github.event.issue.number }}`
---
+11 -4
View File
@@ -35,20 +35,27 @@ jobs:
cache: yarn
- name: Ensure tracking relevant branches and checkout base
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
run: |
git checkout ${{ github.head_ref }}
git checkout ${{ github.base_ref }}
git checkout $HEAD_REF
git checkout $BASE_REF
- name: Get the base commit
id: base-commit
run: echo base-commit=$(git log -n 1 ${{ github.base_ref }} --pretty=format:'%H') >> "$GITHUB_OUTPUT"
env:
BASE_REF: ${{ github.base_ref }}
run: echo base-commit=$(git log -n 1 $BASE_REF --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: Merge PR commit
env:
HEAD_REF: ${{ github.head_ref }}
run: |
# Have to set a git config for the merge to work
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git merge --no-edit ${{ github.head_ref }}
git merge --no-edit $HEAD_REF
yarn install
yarn intl:build
+6 -2
View File
@@ -17,7 +17,9 @@ jobs:
fetch-depth: 0
- name: Fetch base branch
run: git fetch origin ${{ github.base_ref }} --depth=1
env:
BASE_REF: ${{ github.base_ref }}
run: git fetch origin $BASE_REF --depth=1
- name: Install node
uses: actions/setup-node@v6
@@ -25,7 +27,9 @@ jobs:
node-version-file: .nvmrc
- name: Reset yarn.lock to base
run: git show "origin/${{ github.base_ref }}:yarn.lock" > yarn.lock
env:
BASE_REF: ${{ github.base_ref }}
run: git show "origin/$BASE_REF:yarn.lock" > yarn.lock
- name: Yarn install
uses: Wandalen/wretry.action@master
-2
View File
@@ -61,7 +61,6 @@ import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
@@ -165,7 +164,6 @@ function InnerApp() {
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
-2
View File
@@ -48,7 +48,6 @@ import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
@@ -138,7 +137,6 @@ function InnerApp() {
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
+47 -21
View File
@@ -6,7 +6,7 @@ import {
AtUri,
RichText as RichTextApi,
} from '@atproto/api'
import {msg, Plural, Trans} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
@@ -17,8 +17,10 @@ import {
useAddSavedFeedsMutation,
usePreferencesQuery,
useRemoveFeedMutation,
useUpdateSavedFeedsMutation,
} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {formatCount} from '#/view/com/util/numeric/format'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
@@ -28,6 +30,7 @@ import {
type ButtonProps,
ButtonText,
} from '#/components/Button'
import {Heart2_Filled_Stroke2_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2'
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
import {Link as InternalLink, type LinkProps} from '#/components/Link'
import {Loader} from '#/components/Loader'
@@ -134,9 +137,9 @@ export function TitleAndByline({
</Text>
{creator && (
<Text
style={[a.leading_snug, t.atoms.text_contrast_medium]}
style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
<Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans>
<Trans>By {sanitizeHandle(creator.handle, '@')}</Trans>
</Text>
)}
</View>
@@ -213,12 +216,14 @@ export function DescriptionPlaceholder() {
export function Likes({count}: {count: number}) {
const t = useTheme()
const {i18n} = useLingui()
return (
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.font_semi_bold]}>
<Trans>
Liked by <Plural value={count || 0} one="# user" other="# users" />
</Trans>
</Text>
<View style={[a.flex_row, a.align_center, {gap: 2}]}>
<HeartIcon size="xs" fill={t.atoms.text_contrast_low.color} />
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{formatCount(i18n, count)}
</Text>
</View>
)
}
@@ -252,6 +257,8 @@ function SaveButtonInner({
useAddSavedFeedsMutation()
const {isPending: isRemovePending, mutateAsync: removeFeed} =
useRemoveFeedMutation()
const {isPending: isUpdatePending, mutateAsync: updateSavedFeeds} =
useUpdateSavedFeedsMutation()
const uri = view.uri
const type = view.uri.includes('app.bsky.feed.generator') ? 'feed' : 'list'
@@ -259,23 +266,26 @@ function SaveButtonInner({
const savedFeedConfig = React.useMemo(() => {
return preferences?.savedFeeds?.find(feed => feed.value === uri)
}, [preferences?.savedFeeds, uri])
const isPinned = savedFeedConfig?.pinned ?? false
const removePromptControl = Prompt.usePromptControl()
const isPending = isAddSavedFeedPending || isRemovePending
const isPending = isAddSavedFeedPending || isRemovePending || isUpdatePending
const toggleSave = React.useCallback(
const onPinFeed = React.useCallback(
async (e: GestureResponderEvent) => {
e.preventDefault()
e.stopPropagation()
try {
if (savedFeedConfig) {
await removeFeed(savedFeedConfig)
// Feed is saved but not pinned, update it to be pinned
await updateSavedFeeds([{...savedFeedConfig, pinned: true}])
} else {
// Feed is not saved, save it with pinned=true
await saveFeeds([
{
type,
value: uri,
pinned: pin || false,
pinned: true,
},
])
}
@@ -285,10 +295,22 @@ function SaveButtonInner({
Toast.show(_(msg`Failed to update feeds`), 'xmark')
}
},
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
[_, pin, saveFeeds, updateSavedFeeds, uri, savedFeedConfig, type],
)
const onPrompRemoveFeed = React.useCallback(
const onRemoveFeed = React.useCallback(async () => {
try {
if (savedFeedConfig) {
await removeFeed(savedFeedConfig)
}
Toast.show(_(msg({message: 'Feeds updated!', context: 'toast'})))
} catch (err: any) {
logger.error(err, {message: `FeedCard: failed to remove feed`})
Toast.show(_(msg`Failed to update feeds`), 'xmark')
}
}, [_, removeFeed, savedFeedConfig])
const onPromptRemoveFeed = React.useCallback(
async (e: GestureResponderEvent) => {
e.preventDefault()
e.stopPropagation()
@@ -302,13 +324,17 @@ function SaveButtonInner({
<>
<Button
disabled={isPending}
label={_(msg`Add this feed to your feeds`)}
label={
isPinned
? _(msg`Remove this feed from your feeds`)
: _(msg`Add this feed to your feeds`)
}
size="small"
variant="solid"
color={savedFeedConfig ? 'secondary' : 'primary'}
onPress={savedFeedConfig ? onPrompRemoveFeed : toggleSave}
color={isPinned ? 'secondary' : 'primary'}
onPress={isPinned ? onPromptRemoveFeed : onPinFeed}
{...buttonProps}>
{savedFeedConfig ? (
{isPinned ? (
<>
{isPending ? (
<ButtonIcon size="md" icon={Loader} />
@@ -317,7 +343,7 @@ function SaveButtonInner({
)}
{text && (
<ButtonText>
<Trans>Unpin Feed</Trans>
<Trans>Unpin</Trans>
</ButtonText>
)}
</>
@@ -326,7 +352,7 @@ function SaveButtonInner({
<ButtonIcon size="md" icon={isPending ? Loader : PinIcon} />
{text && (
<ButtonText>
<Trans>Pin Feed</Trans>
<Trans>Pin</Trans>
</ButtonText>
)}
</>
@@ -339,7 +365,7 @@ function SaveButtonInner({
description={_(
msg`Are you sure you want to remove this from your feeds?`,
)}
onConfirm={toggleSave}
onConfirm={onRemoveFeed}
confirmButtonCta={_(msg`Remove`)}
confirmButtonColor="negative"
/>
+1 -1
View File
@@ -145,7 +145,7 @@ export function TitleAndByline({
{creator && (
<Text
emoji
style={[a.leading_snug, t.atoms.text_contrast_medium]}
style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
{purpose === MODLIST
? _(msg`Moderation list by ${sanitizeHandle(creator.handle, '@')}`)
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Grip_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M 7.5 6 a 1.5 1.5 0 1 0 3 0 a 1.5 1.5 0 1 0 -3 0 M 13.5 6 a 1.5 1.5 0 1 0 3 0 a 1.5 1.5 0 1 0 -3 0 M 7.5 12 a 1.5 1.5 0 1 0 3 0 a 1.5 1.5 0 1 0 -3 0 M 13.5 12 a 1.5 1.5 0 1 0 3 0 a 1.5 1.5 0 1 0 -3 0 M 7.5 18 a 1.5 1.5 0 1 0 3 0 a 1.5 1.5 0 1 0 -3 0 M 13.5 18 a 1.5 1.5 0 1 0 3 0 a 1.5 1.5 0 1 0 -3 0',
})
+46 -46
View File
@@ -844,7 +844,7 @@ msgstr ""
msgid "Add user to list"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:223
#: src/ageAssurance/components/NoAccessScreen.tsx:222
msgid "Add your birthdate"
msgstr ""
@@ -914,7 +914,7 @@ msgctxt "toast"
msgid "Age assurance inquiry was submitted"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:357
#: src/ageAssurance/components/NoAccessScreen.tsx:345
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:191
msgid "Age assurance only takes a few minutes"
msgstr ""
@@ -1448,7 +1448,7 @@ msgstr ""
msgid "Begin the age assurance process by completing the fields below."
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:149
#: src/components/dialogs/BirthDateSettings.tsx:162
msgid "Birthdate"
msgstr ""
@@ -2022,11 +2022,11 @@ msgstr ""
msgid "click here"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:114
#: src/ageAssurance/components/NoAccessScreen.tsx:126
msgid "Click here to contact our support team"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:248
#: src/ageAssurance/components/NoAccessScreen.tsx:236
msgid "Click here to log out"
msgstr ""
@@ -2034,8 +2034,8 @@ msgstr ""
msgid "Click here to restart the verification process."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:97
#: src/ageAssurance/components/NoAccessScreen.tsx:220
#: src/ageAssurance/components/NoAccessScreen.tsx:103
#: src/ageAssurance/components/NoAccessScreen.tsx:219
msgid "Click here to update your birthdate"
msgstr ""
@@ -2121,7 +2121,7 @@ msgstr ""
msgid "Close dialog"
msgstr ""
#: src/view/shell/index.web.tsx:111
#: src/view/shell/index.web.tsx:113
msgid "Close drawer menu"
msgstr ""
@@ -2252,7 +2252,7 @@ msgstr ""
msgid "Confirm delete account"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:371
#: src/ageAssurance/components/NoAccessScreen.tsx:359
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:87
#: src/components/dialogs/DeviceLocationRequestDialog.tsx:40
#: src/components/dialogs/DeviceLocationRequestDialog.tsx:105
@@ -2279,7 +2279,7 @@ msgstr ""
msgid "Connection issue"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:308
#: src/ageAssurance/components/NoAccessScreen.tsx:296
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:130
#: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:29
msgid "Contact our moderation team"
@@ -3074,8 +3074,8 @@ msgstr ""
#: src/components/contacts/components/InviteInfo.tsx:78
#: src/components/contacts/screens/ViewMatches.tsx:392
#: src/components/contacts/screens/ViewMatches.tsx:409
#: src/components/dialogs/BirthDateSettings.tsx:183
#: src/components/dialogs/BirthDateSettings.tsx:190
#: src/components/dialogs/BirthDateSettings.tsx:196
#: src/components/dialogs/BirthDateSettings.tsx:203
#: src/components/dialogs/ServerInput.tsx:240
#: src/components/dialogs/ServerInput.tsx:242
#: src/components/dms/AfterReportDialog.tsx:142
@@ -3449,7 +3449,7 @@ msgstr ""
msgid "Enter the username or email address you used when you created your account"
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:150
#: src/components/dialogs/BirthDateSettings.tsx:163
msgid "Enter your birthdate"
msgstr ""
@@ -4209,7 +4209,7 @@ msgstr ""
msgid "Food"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:125
#: src/ageAssurance/components/NoAccessScreen.tsx:90
msgid "For organizational accounts, use the birthdate of the person who is responsible for the account."
msgstr ""
@@ -4556,11 +4556,11 @@ msgstr ""
msgid "Hey there 👋"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:167
#: src/ageAssurance/components/NoAccessScreen.tsx:170
msgid "Hey there!"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:202
#: src/ageAssurance/components/NoAccessScreen.tsx:201
msgid "Hi there!"
msgstr ""
@@ -4660,7 +4660,7 @@ msgstr ""
msgid "Hides the content"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:229
#: src/components/dialogs/BirthDateSettings.tsx:74
msgid "Hmm, it looks like you're logged in with an <0>App Password</0>. To set your birthdate, you'll need to log in with your main account password, or ask whomever controls this account to do so."
msgstr ""
@@ -4765,11 +4765,11 @@ msgstr ""
msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:110
#: src/ageAssurance/components/NoAccessScreen.tsx:122
msgid "If you believe your birthdate is incorrect, please <0>contact our support team</0>."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:94
#: src/ageAssurance/components/NoAccessScreen.tsx:100
msgid "If you believe your birthdate is incorrect, you can update it by <0>clicking here</0>."
msgstr ""
@@ -4853,7 +4853,7 @@ msgstr ""
msgid "Import contacts to find your friends"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:205
#: src/ageAssurance/components/NoAccessScreen.tsx:204
msgid "In order to provide an age-appropriate experience, we need to know your birthdate. This is a one-time thing, and your data will be kept private."
msgstr ""
@@ -4998,7 +4998,7 @@ msgstr ""
msgid "Invites, but personal"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:368
#: src/ageAssurance/components/NoAccessScreen.tsx:356
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:84
msgid "Is your location not accurate? <0>Tap here to confirm your location.</0>"
msgstr ""
@@ -5095,12 +5095,12 @@ msgstr ""
msgid "Larger"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:351
#: src/ageAssurance/components/NoAccessScreen.tsx:339
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:185
msgid "Last initiated {timeAgo} ago"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:349
#: src/ageAssurance/components/NoAccessScreen.tsx:337
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:183
msgid "Last initiated just now"
msgstr ""
@@ -5825,8 +5825,8 @@ msgstr ""
msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them."
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:43
#: src/components/dialogs/BirthDateSettings.tsx:47
#: src/components/dialogs/BirthDateSettings.tsx:51
msgid "My Birthdate"
msgstr ""
@@ -7935,7 +7935,7 @@ msgstr ""
msgid "Returns to the previous step"
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:190
#: src/components/dialogs/BirthDateSettings.tsx:203
#: src/components/dialogs/lists/CreateOrEditListDialog.tsx:292
#: src/components/dialogs/lists/CreateOrEditListDialog.tsx:307
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:662
@@ -7961,7 +7961,7 @@ msgctxt "action"
msgid "Save"
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:183
#: src/components/dialogs/BirthDateSettings.tsx:196
msgid "Save birthdate"
msgstr ""
@@ -8440,7 +8440,7 @@ msgstr ""
msgid "Set who can reply to your post"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:212
#: src/ageAssurance/components/NoAccessScreen.tsx:211
msgid "Set your birthdate below and we'll get you back to posting and exploring in no time!"
msgstr ""
@@ -8899,8 +8899,8 @@ msgstr ""
msgid "Sorry, we're unable to load account suggestions at this time."
msgstr ""
#: src/App.native.tsx:127
#: src/App.web.tsx:100
#: src/App.native.tsx:126
#: src/App.web.tsx:99
msgid "Sorry! Your session expired. Please sign in again."
msgstr ""
@@ -9212,7 +9212,7 @@ msgstr ""
msgid "Terms"
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:169
#: src/components/dialogs/BirthDateSettings.tsx:182
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:30
#: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:97
#: src/Navigation.tsx:338
@@ -9241,7 +9241,7 @@ msgstr ""
msgid "Thanks, you have successfully verified your email address. You can close this dialog."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:397
#: src/ageAssurance/components/NoAccessScreen.tsx:385
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:113
msgid "Thanks! You're all set."
msgstr ""
@@ -9287,7 +9287,7 @@ msgstr ""
msgid "The author of this thread has hidden this reply."
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:156
#: src/components/dialogs/BirthDateSettings.tsx:169
msgid "The birthdate you've entered means you are under 18 years old. Certain content and features may be unavailable to you."
msgstr ""
@@ -9383,7 +9383,7 @@ msgstr ""
msgid "Theme"
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:91
#: src/components/dialogs/BirthDateSettings.tsx:104
msgid "There is a limit to how often you can change your birthdate. You may need to wait a day or two before updating it again."
msgstr ""
@@ -9607,7 +9607,7 @@ msgstr ""
msgid "This handle is reserved. Please try a different one."
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:51
#: src/components/dialogs/BirthDateSettings.tsx:55
msgid "This information is private and not shared with other users."
msgstr ""
@@ -9785,7 +9785,7 @@ msgstr ""
msgid "To disable your email 2FA method, please verify your access to <0>{0}</0>"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:245
#: src/ageAssurance/components/NoAccessScreen.tsx:233
msgid "To log out, <0>click here</0>."
msgstr ""
@@ -10000,7 +10000,7 @@ msgstr ""
msgid "Unfortunately, none of your subscribed labelers supports this report type."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:187
#: src/ageAssurance/components/NoAccessScreen.tsx:188
msgid "Unfortunately, the birthdate you have saved to your profile makes you too young to access Bluesky."
msgstr ""
@@ -10361,7 +10361,7 @@ msgstr ""
msgid "Verify account"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:334
#: src/ageAssurance/components/NoAccessScreen.tsx:322
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:168
msgid "Verify again"
msgstr ""
@@ -10388,8 +10388,8 @@ msgstr ""
msgid "Verify email dialog"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:322
#: src/ageAssurance/components/NoAccessScreen.tsx:336
#: src/ageAssurance/components/NoAccessScreen.tsx:310
#: src/ageAssurance/components/NoAccessScreen.tsx:324
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:156
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:170
msgid "Verify now"
@@ -10728,7 +10728,7 @@ msgstr ""
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:63
#: src/components/dialogs/BirthDateSettings.tsx:67
msgid "We were unable to load your birthdate preferences. Please try again."
msgstr ""
@@ -10787,7 +10787,7 @@ msgstr ""
msgid "We're so excited to have you join us!"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:390
#: src/ageAssurance/components/NoAccessScreen.tsx:378
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:106
msgid "We're sorry, but based on your device's location, you are currently located in a region that requires age assurance."
msgstr ""
@@ -10995,7 +10995,7 @@ msgstr ""
msgid "You are a trusted verifier"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:170
#: src/ageAssurance/components/NoAccessScreen.tsx:173
msgid "You are accessing Bluesky from a region that legally requires us to verify your age before allowing you to access the app."
msgstr ""
@@ -11003,7 +11003,7 @@ msgstr ""
msgid "You are creating an account on"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:304
#: src/ageAssurance/components/NoAccessScreen.tsx:292
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:126
msgid "You are currently unable to access Bluesky's Age Assurance flow. Please <0>contact our moderation team</0> if you believe this is an error."
msgstr ""
@@ -11262,7 +11262,7 @@ msgstr ""
msgid "You must be 13 years of age or older to create an account."
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:165
#: src/components/dialogs/BirthDateSettings.tsx:178
msgid "You must be at least 13 years old to use Bluesky. Read our <0>Terms of Service</0> for more information."
msgstr ""
@@ -11307,8 +11307,8 @@ msgstr ""
msgid "You reacted {0} to {1}"
msgstr ""
#: src/components/dialogs/BirthDateSettings.tsx:77
#: src/components/dialogs/BirthDateSettings.tsx:87
#: src/components/dialogs/BirthDateSettings.tsx:90
#: src/components/dialogs/BirthDateSettings.tsx:100
msgid "You recently changed your birthdate"
msgstr ""
+12 -400
View File
@@ -1,415 +1,27 @@
import {useCallback, useState} from 'react'
import {useEffect} from 'react'
import {View} from 'react-native'
import Animated, {LinearTransition} from 'react-native-reanimated'
import {type AppBskyActorDefs} from '@atproto/api'
import {TID} from '@atproto/common-web'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {useNavigation} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {RECOMMENDED_SAVED_FEEDS, TIMELINE_SAVED_FEED} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {
type CommonNavigatorParams,
type NavigationProp,
} from '#/lib/routes/types'
import {logger} from '#/logger'
import {
useOverwriteSavedFeedsMutation,
usePreferencesQuery,
} from '#/state/queries/preferences'
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {useSetMinimalShellMode} from '#/state/shell'
import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard'
import * as Toast from '#/view/com/util/Toast'
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {
ArrowBottom_Stroke2_Corner0_Rounded as ArrowDownIcon,
ArrowTop_Stroke2_Corner0_Rounded as ArrowUpIcon,
} from '#/components/icons/Arrow'
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk'
import {Pin_Filled_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
export function SavedFeeds({}: Props) {
const {data: preferences} = usePreferencesQuery()
if (!preferences) {
return <View />
}
return <SavedFeedsInner preferences={preferences} />
}
function SavedFeedsInner({
preferences,
}: {
preferences: UsePreferencesQueryResponse
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const setMinimalShellMode = useSetMinimalShellMode()
const {mutateAsync: overwriteSavedFeeds, isPending: isOverwritePending} =
useOverwriteSavedFeedsMutation()
/**
* This screen has been consolidated into the main Feeds screen.
* Redirect users there automatically.
*/
export function SavedFeeds({}: Props) {
const navigation = useNavigation<NavigationProp>()
/*
* Use optimistic data if exists and no error, otherwise fallback to remote
* data
*/
const [currentFeeds, setCurrentFeeds] = useState(
() => preferences.savedFeeds || [],
)
const hasUnsavedChanges = currentFeeds !== preferences.savedFeeds
const pinnedFeeds = currentFeeds.filter(f => f.pinned)
const unpinnedFeeds = currentFeeds.filter(f => !f.pinned)
const noSavedFeedsOfAnyType = pinnedFeeds.length + unpinnedFeeds.length === 0
const noFollowingFeed =
currentFeeds.every(f => f.type !== 'timeline') && !noSavedFeedsOfAnyType
useEffect(() => {
// Replace current screen with Feeds to avoid back-navigation issues
navigation.replace('Feeds')
}, [navigation])
useFocusEffect(
useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
const onSaveChanges = async () => {
try {
await overwriteSavedFeeds(currentFeeds)
Toast.show(_(msg({message: 'Feeds updated!', context: 'toast'})))
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Feeds')
}
} catch (e) {
Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to toggle pinned feed', {message: e})
}
}
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content align="left">
<Layout.Header.TitleText>
<Trans>Feeds</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Button
testID="saveChangesBtn"
size="small"
color={hasUnsavedChanges ? 'primary' : 'secondary'}
onPress={onSaveChanges}
label={_(msg`Save changes`)}
disabled={isOverwritePending || !hasUnsavedChanges}>
<ButtonIcon icon={isOverwritePending ? Loader : SaveIcon} />
<ButtonText>
{gtMobile ? <Trans>Save changes</Trans> : <Trans>Save</Trans>}
</ButtonText>
</Button>
</Layout.Header.Outer>
<Layout.Content>
{noSavedFeedsOfAnyType && (
<View style={[t.atoms.border_contrast_low, a.border_b]}>
<NoSavedFeedsOfAnyType
onAddRecommendedFeeds={() =>
setCurrentFeeds(
RECOMMENDED_SAVED_FEEDS.map(f => ({
...f,
id: TID.nextStr(),
})),
)
}
/>
</View>
)}
<SectionHeaderText>
<Trans>Pinned Feeds</Trans>
</SectionHeaderText>
{preferences ? (
!pinnedFeeds.length ? (
<View style={[a.flex_1, a.p_lg]}>
<Admonition type="info">
<Trans>You don't have any pinned feeds.</Trans>
</Admonition>
</View>
) : (
pinnedFeeds.map(f => (
<ListItem
key={f.id}
feed={f}
isPinned
currentFeeds={currentFeeds}
setCurrentFeeds={setCurrentFeeds}
preferences={preferences}
/>
))
)
) : (
<View style={[a.w_full, a.py_2xl, a.align_center]}>
<Loader size="xl" />
</View>
)}
{noFollowingFeed && (
<View style={[t.atoms.border_contrast_low, a.border_b]}>
<NoFollowingFeed
onAddFeed={() =>
setCurrentFeeds(feeds => [
...feeds,
{...TIMELINE_SAVED_FEED, id: TID.next().toString()},
])
}
/>
</View>
)}
<SectionHeaderText>
<Trans>Saved Feeds</Trans>
</SectionHeaderText>
{preferences ? (
!unpinnedFeeds.length ? (
<View style={[a.flex_1, a.p_lg]}>
<Admonition type="info">
<Trans>You don't have any saved feeds.</Trans>
</Admonition>
</View>
) : (
unpinnedFeeds.map(f => (
<ListItem
key={f.id}
feed={f}
isPinned={false}
currentFeeds={currentFeeds}
setCurrentFeeds={setCurrentFeeds}
preferences={preferences}
/>
))
)
) : (
<View style={[a.w_full, a.py_2xl, a.align_center]}>
<Loader size="xl" />
</View>
)}
<View style={[a.px_lg, a.py_xl]}>
<Text
style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]}>
<Trans>
Feeds are custom algorithms that users build with a little coding
expertise.{' '}
<InlineLinkText
to="https://github.com/bluesky-social/feed-generator"
label={_(msg`See this guide`)}
disableMismatchWarning
style={[a.leading_snug]}>
See this guide
</InlineLinkText>{' '}
for more information.
</Trans>
</Text>
</View>
</Layout.Content>
</Layout.Screen>
)
}
function ListItem({
feed,
isPinned,
currentFeeds,
setCurrentFeeds,
}: {
feed: AppBskyActorDefs.SavedFeed
isPinned: boolean
currentFeeds: AppBskyActorDefs.SavedFeed[]
setCurrentFeeds: React.Dispatch<AppBskyActorDefs.SavedFeed[]>
preferences: UsePreferencesQueryResponse
}) {
const {_} = useLingui()
const t = useTheme()
const playHaptic = useHaptics()
const feedUri = feed.value
const onTogglePinned = async () => {
playHaptic()
setCurrentFeeds(
currentFeeds.map(f =>
f.id === feed.id ? {...feed, pinned: !feed.pinned} : f,
),
)
}
const onPressUp = async () => {
if (!isPinned) return
const nextFeeds = currentFeeds.slice()
const ids = currentFeeds.map(f => f.id)
const index = ids.indexOf(feed.id)
const nextIndex = index - 1
if (index === -1 || index === 0) return
;[nextFeeds[index], nextFeeds[nextIndex]] = [
nextFeeds[nextIndex],
nextFeeds[index],
]
setCurrentFeeds(nextFeeds)
}
const onPressDown = async () => {
if (!isPinned) return
const nextFeeds = currentFeeds.slice()
const ids = currentFeeds.map(f => f.id)
const index = ids.indexOf(feed.id)
const nextIndex = index + 1
if (index === -1 || index >= nextFeeds.filter(f => f.pinned).length - 1)
return
;[nextFeeds[index], nextFeeds[nextIndex]] = [
nextFeeds[nextIndex],
nextFeeds[index],
]
setCurrentFeeds(nextFeeds)
}
const onPressRemove = async () => {
playHaptic()
setCurrentFeeds(currentFeeds.filter(f => f.id !== feed.id))
}
return (
<Animated.View
style={[a.flex_row, a.border_b, t.atoms.border_contrast_low]}
layout={LinearTransition.duration(100)}>
{feed.type === 'timeline' ? (
<FollowingFeedCard />
) : (
<FeedSourceCard
key={feedUri}
feedUri={feedUri}
style={[isPinned && a.pr_sm]}
showMinimalPlaceholder
hideTopBorder={true}
/>
)}
<View style={[a.pr_lg, a.flex_row, a.align_center, a.gap_sm]}>
{isPinned ? (
<>
<Button
testID={`feed-${feed.type}-moveUp`}
label={_(msg`Move feed up`)}
onPress={onPressUp}
size="small"
color="secondary"
shape="square">
<ButtonIcon icon={ArrowUpIcon} />
</Button>
<Button
testID={`feed-${feed.type}-moveDown`}
label={_(msg`Move feed down`)}
onPress={onPressDown}
size="small"
color="secondary"
shape="square">
<ButtonIcon icon={ArrowDownIcon} />
</Button>
</>
) : (
<Button
testID={`feed-${feedUri}-toggleSave`}
label={_(msg`Remove from my feeds`)}
onPress={onPressRemove}
size="small"
color="secondary"
variant="ghost"
shape="square">
<ButtonIcon icon={TrashIcon} />
</Button>
)}
<Button
testID={`feed-${feed.type}-togglePin`}
label={isPinned ? _(msg`Unpin feed`) : _(msg`Pin feed`)}
onPress={onTogglePinned}
size="small"
color={isPinned ? 'primary_subtle' : 'secondary'}
shape="square">
<ButtonIcon icon={PinIcon} />
</Button>
</View>
</Animated.View>
)
}
function SectionHeaderText({children}: {children: React.ReactNode}) {
const t = useTheme()
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
return (
<View
style={[
a.flex_row,
a.flex_1,
a.px_lg,
a.pt_2xl,
a.pb_md,
a.border_b,
t.atoms.border_contrast_low,
]}>
<Text style={[a.text_xl, a.font_bold, a.leading_snug]}>{children}</Text>
</View>
)
}
function FollowingFeedCard() {
const t = useTheme()
return (
<View style={[a.flex_row, a.align_center, a.flex_1, a.p_lg]}>
<View
style={[
a.align_center,
a.justify_center,
a.rounded_sm,
a.mr_md,
{
width: 36,
height: 36,
backgroundColor: t.palette.primary_500,
},
]}>
<FilterTimeline
style={[
{
width: 22,
height: 22,
},
]}
fill={t.palette.white}
/>
</View>
<View style={[a.flex_1, a.flex_row, a.gap_sm, a.align_center]}>
<Text style={[a.text_sm, a.font_semi_bold, a.leading_snug]}>
<Trans context="feed-name">Following</Trans>
</Text>
</View>
</View>
)
// Show empty view while redirecting
return <View />
}
+82 -10
View File
@@ -209,13 +209,48 @@ export function useOverwriteSavedFeedsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, AppBskyActorDefs.SavedFeed[]>({
return useMutation<
void,
unknown,
AppBskyActorDefs.SavedFeed[],
{previousPrefs: UsePreferencesQueryResponse | undefined}
>({
mutationFn: async savedFeeds => {
await agent.overwriteSavedFeeds(savedFeeds)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
onMutate: async newSavedFeeds => {
// Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({queryKey: preferencesQueryKey})
// Snapshot the previous value
const previousPrefs =
queryClient.getQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
)
// Optimistically update the cache
if (previousPrefs) {
queryClient.setQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
{
...previousPrefs,
savedFeeds: newSavedFeeds,
},
)
}
// Return context with the previous value for rollback
return {previousPrefs}
},
onError: (_err, _newSavedFeeds, context) => {
// Rollback to the previous value on error
if (context?.previousPrefs) {
queryClient.setQueryData(preferencesQueryKey, context.previousPrefs)
}
},
onSettled: () => {
// Always refetch after error or success to ensure server state consistency
queryClient.invalidateQueries({queryKey: preferencesQueryKey})
},
})
}
@@ -227,14 +262,51 @@ export function useAddSavedFeedsMutation() {
return useMutation<
void,
unknown,
Pick<AppBskyActorDefs.SavedFeed, 'type' | 'value' | 'pinned'>[]
Pick<AppBskyActorDefs.SavedFeed, 'type' | 'value' | 'pinned'>[],
{previousPrefs: UsePreferencesQueryResponse | undefined}
>({
mutationFn: async savedFeeds => {
await agent.addSavedFeeds(savedFeeds)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
onMutate: async newFeeds => {
// Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({queryKey: preferencesQueryKey})
// Snapshot the previous value
const previousPrefs =
queryClient.getQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
)
// Optimistically update the cache
if (previousPrefs) {
// Generate temporary IDs for new feeds
const newSavedFeeds = newFeeds.map((feed, index) => ({
...feed,
id: `temp-${Date.now()}-${index}`,
}))
queryClient.setQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
{
...previousPrefs,
savedFeeds: [...previousPrefs.savedFeeds, ...newSavedFeeds],
},
)
}
// Return context with the previous value for rollback
return {previousPrefs}
},
onError: (_err, _newFeeds, context) => {
// Rollback to the previous value on error
if (context?.previousPrefs) {
queryClient.setQueryData(preferencesQueryKey, context.previousPrefs)
}
},
onSettled: () => {
// Always refetch after error or success to ensure server state consistency
queryClient.invalidateQueries({queryKey: preferencesQueryKey})
},
})
}
+2 -2
View File
@@ -193,7 +193,7 @@ export function FeedSourceCardLoaded({
}}
style={[
a.flex_1,
a.p_lg,
a.p_md,
a.gap_md,
!hideTopBorder && !a.border_t,
t.atoms.border_contrast_low,
@@ -207,7 +207,7 @@ export function FeedSourceCardLoaded({
<View
style={[
a.flex_1,
a.p_lg,
a.p_md,
a.gap_md,
!hideTopBorder && !a.border_t,
t.atoms.border_contrast_low,
+1150 -588
View File
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {ActivityIndicator, StyleSheet} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -19,7 +20,10 @@ import {
usePinnedFeedsInfos,
} from '#/state/queries/feed'
import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {
preferencesQueryKey,
usePreferencesQuery,
} from '#/state/queries/preferences'
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
@@ -41,12 +45,20 @@ import {useDemoMode} from '#/storage/hooks/demo-mode'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
export function HomeScreen(props: Props) {
const queryClient = useQueryClient()
const {setShowLoggedOut} = useLoggedOutViewControls()
const {data: preferences} = usePreferencesQuery()
const {currentAccount} = useSession()
const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
usePinnedFeedsInfos()
// Refetch preferences when Home gains focus to sync feed changes
useFocusEffect(
React.useCallback(() => {
queryClient.invalidateQueries({queryKey: preferencesQueryKey})
}, [queryClient]),
)
React.useEffect(() => {
if (isWeb && !currentAccount) {
const getParams = new URLSearchParams(window.location.search)
+2
View File
@@ -32,6 +32,7 @@ import {EmailDialog} from '#/components/dialogs/EmailDialog'
import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent'
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {SigninDialog} from '#/components/dialogs/Signin'
import {
Outlet as PolicyUpdateOverlayPortalOutlet,
@@ -110,6 +111,7 @@ function ShellInner() {
<InAppBrowserConsentDialog />
<LinkWarningDialog />
<Lightbox />
<NuxDialogs />
{/* Until policy update has been completed by the user, don't render anything that is portaled */}
{policyUpdateState.completed && (
+2
View File
@@ -22,6 +22,7 @@ import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssurance
import {EmailDialog} from '#/components/dialogs/EmailDialog'
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {SigninDialog} from '#/components/dialogs/Signin'
import {useWelcomeModal} from '#/components/hooks/useWelcomeModal'
import {
@@ -86,6 +87,7 @@ function ShellInner() {
<AgeAssuranceRedirectDialog />
<LinkWarningDialog />
<Lightbox />
<NuxDialogs />
{welcomeModalControl.isOpen && (
<WelcomeModal control={welcomeModalControl} />