Merge branch 'main' into starter-packs

This commit is contained in:
Hailey
2024-06-04 21:18:13 -07:00
42 changed files with 17372 additions and 15415 deletions
+9 -2
View File
@@ -34,6 +34,9 @@ jobs:
node-version-file: .nvmrc node-version-file: .nvmrc
cache: yarn cache: yarn
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS - name: 🔨 Setup EAS
uses: expo/expo-github-action@v8 uses: expo/expo-github-action@v8
with: with:
@@ -96,13 +99,17 @@ jobs:
name: build-${{ steps.timestamp.outputs.time }}.apk name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk path: build.apk
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
- name: 🔔 Notify Slack of Production Build - name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }} if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0 uses: slackapi/slack-github-action@v1.25.0
with: with:
payload: | payload: |
{ {
"text": "Android build is ready for submission. This is a production build! Download the artifact here: ${{ steps.upload-artifact-production.outputs.artifact-url }}" "text": "Android production build for Google Play Store submission is ready!\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
} }
env: env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
@@ -140,7 +147,7 @@ jobs:
with: with:
payload: | payload: |
{ {
"text": "Android production APK build is ready for download. This is a production build, and you should add it to the GitHub release! Download the artifact here: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}" "text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
} }
env: env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
+19
View File
@@ -34,6 +34,9 @@ jobs:
node-version-file: .nvmrc node-version-file: .nvmrc
cache: yarn cache: yarn
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS - name: 🔨 Setup EAS
uses: expo/expo-github-action@v8 uses: expo/expo-github-action@v8
with: with:
@@ -81,6 +84,22 @@ jobs:
- name: 🚀 Deploy - name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa run: eas submit -p ios --non-interactive --path build.ipa
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: ⬇️ Restore Cache - name: ⬇️ Restore Cache
id: get-base-commit id: get-base-commit
uses: actions/cache@v4 uses: actions/cache@v4
+1 -1
View File
@@ -50,7 +50,7 @@ appId: xyz.blueskyweb.app
text: "1" text: "1"
- tapOn: - tapOn:
id: "repostBtn" id: "repostBtn"
- tapOn: "Undo repost" - tapOn: "Remove repost"
- assertNotVisible: - assertNotVisible:
id: "repostCount" id: "repostCount"
-31
View File
@@ -1,31 +0,0 @@
# Remove this test when the old onboarding is deprecated
appId: xyz.blueskyweb.app
---
- runScript:
file: ../setupServer.js
env:
SERVER_PATH: "?users"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- tapOn:
id: "e2eStartLongboarding"
- tapOn: "Continue to next step"
- tapOn: "Continue to the next step without following any accounts"
- tapOn: Show replies in Following feed
- tapOn: Show quote-posts in Following feed
- tapOn: Show re-posts in Following feed
- tapOn: Show replies in Following feed
- waitForAnimationToEnd
- tapOn: Continue to next step
- waitForAnimationToEnd
- tapOn: "Continue to the next step"
- waitForAnimationToEnd
- tapOn: Continue to next step
- waitForAnimationToEnd
- tapOn: Continue to next step
- waitForAnimationToEnd
- tapOn: "Complete onboarding and start using your account"
- waitForAnimationToEnd
- assertVisible: Following
+22 -8
View File
@@ -14,15 +14,20 @@ diff results in incompatible native changes, a new client build will automatical
### Prerequisites ### Prerequisites
- Remove any internal client from your device and download the client from the App Store/Google Play. This will help for - Find the latest production build number for both iOS and Android in Slack. These are listed in #client-builds
testing as well as retrieving the build number. - Production builds always send the Version Number and Build Number in the Slack message. Search for the latest
- You should have signed in to EAS locally through npx eas login. You will need to modify the build number in a production version number, and you should find the correct information.
subsequent step.
- Identify the build number of the production app you want to deploy an update for. iOS and Android build numbers are ![slack-build-info](./img/slack-build-info.png)
divergent, so you will need to find both
- It may also be useful to check the current production clients for these values. This will also help for testing. Note
that you will need to _fully_ remove the existing internal client build from your device, otherwise the given values in
the app may differ from the actual production values.
![app-build-number](./img/app-build-number.png) ![app-build-number](./img/app-build-number.png)
- You should have signed in to EAS locally through npx eas login. You will need to modify the build number in a
subsequent step.
- Ensure that the commit the initial client was cut from is properly tagged in git. The tag should be in the format of 1.X.0 - Ensure that the commit the initial client was cut from is properly tagged in git. The tag should be in the format of 1.X.0
- Note: If the commit is not properly tagged, then the OTA deployment will simply fail since the GitHub Action will - Note: If the commit is not properly tagged, then the OTA deployment will simply fail since the GitHub Action will
not be able to find a commit to fingerprint and diff against. not be able to find a commit to fingerprint and diff against.
@@ -38,14 +43,17 @@ to create your branch from, this should be properly set.
### Deployment ### Deployment
- Update the build number through EAS - Update the build number through EAS to match the build numbers of the
production iOS/Android apps
- Note: This isnt strictly necessary, but having a step that takes you off of GitHub and into the terminal provides - Note: This isnt strictly necessary, but having a step that takes you off of GitHub and into the terminal provides
a little “friction” to avoid fat fingering a release. Since there are legitimate reasons to just “click and deploy” a little “friction” to avoid fat fingering a release. Since there are legitimate reasons to just “click and deploy”
for internal builds, I felt it useful to make sure it doesnt accidentally become a prod deployment. for internal builds, I felt it useful to make sure it doesnt accidentally become a prod deployment.
- Set the build number to the appropriate build number found in the prerequisite steps. Again, this should be the - Set the build numbers to the values found in the prerequisite steps. Again, this should be the
build number for the current production release you want to deploy for. build number for the current production release you want to deploy for.
- `npx eas build:version:set -p ios` - `npx eas build:version:set -p ios`
- `npx eas build:version:set -p android` - `npx eas build:version:set -p android`
- These steps should spit out what the current build number is, save those values
for later too
- Run the deployment - Run the deployment
- Navigate to https://github.com/bluesky-social/social-app/actions/workflows/bundle-deploy-eas-update.yml - Navigate to https://github.com/bluesky-social/social-app/actions/workflows/bundle-deploy-eas-update.yml
- Select the “Run Workflow” dropdown - Select the “Run Workflow” dropdown
@@ -79,3 +87,9 @@ In about five minutes, the new deployment should be available for download. To t
- Launch the app once and wait approximately 15 seconds - Launch the app once and wait approximately 15 seconds
- Relaunch the app - Relaunch the app
- Check the Settings page and scroll to the bottom. The commit hash should now be the latest commit on your deployed branch. - Check the Settings page and scroll to the bottom. The commit hash should now be the latest commit on your deployed branch.
### Post Deployment
- Reset both platforms build numbers to what they were before the OTA
deployment. These values should have been logged by the EAS CLI when you
reset them to the production values prior to OTA.
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+1 -1
View File
@@ -49,7 +49,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.12.14", "@atproto/api": "^0.12.16",
"@bam.tech/react-native-image-resizer": "^3.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
outputIos=$(eas build:version:get -p ios)
outputAndroid=$(eas build:version:get -p android)
BSKY_IOS_BUILD_NUMBER=${outputIos#*buildNumber - }
BSKY_ANDROID_VERSION_CODE=${outputAndroid#*versionCode - }
echo PACKAGE_VERSION="$(jq -r '.version' package.json)" > "$GITHUB_OUTPUT"
echo BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER >> "$GITHUB_OUTPUT"
echo BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE >> "$GITHUB_OUTPUT"
@@ -16,6 +16,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
@@ -55,9 +56,11 @@ type Item =
export function SearchablePeopleList({ export function SearchablePeopleList({
title, title,
onSelectChat, onSelectChat,
showRecentConvos,
}: { }: {
title: string title: string
onSelectChat: (did: string) => void onSelectChat: (did: string) => void
showRecentConvos?: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
@@ -75,6 +78,7 @@ export function SearchablePeopleList({
isFetching, isFetching,
} = useActorAutocompleteQuery(searchText, true, 12) } = useActorAutocompleteQuery(searchText, true, 12)
const {data: follows} = useProfileFollowsQuery(currentAccount?.did) const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
const {data: convos} = useListConvosQuery({enabled: showRecentConvos})
const items = useMemo(() => { const items = useMemo(() => {
let _items: Item[] = [] let _items: Item[] = []
@@ -103,7 +107,65 @@ export function SearchablePeopleList({
}) })
} }
} else { } else {
if (follows) { const placeholders: Item[] = Array(10)
.fill(0)
.map((_, i) => ({
type: 'placeholder',
key: i + '',
}))
if (showRecentConvos) {
if (convos && follows) {
const usedDids = new Set()
for (const page of convos.pages) {
for (const convo of page.convos) {
const profiles = convo.members.filter(
m => m.did !== currentAccount?.did,
)
for (const profile of profiles) {
if (usedDids.has(profile.did)) continue
usedDids.add(profile.did)
_items.push({
type: 'profile',
key: profile.did,
enabled: true,
profile,
})
}
}
}
let followsItems: typeof _items = []
for (const page of follows.pages) {
for (const profile of page.follows) {
if (usedDids.has(profile.did)) continue
followsItems.push({
type: 'profile',
key: profile.did,
enabled: canBeMessaged(profile),
profile,
})
}
}
// only sort follows
followsItems = followsItems.sort(a => {
// @ts-ignore
return a.enabled ? -1 : 1
})
// then append
_items.push(...followsItems)
} else {
_items.push(...placeholders)
}
} else if (follows) {
for (const page of follows.pages) { for (const page of follows.pages) {
for (const profile of page.follows) { for (const profile of page.follows) {
_items.push({ _items.push({
@@ -120,19 +182,21 @@ export function SearchablePeopleList({
return a.enabled ? -1 : 1 return a.enabled ? -1 : 1
}) })
} else { } else {
Array(10) _items.push(...placeholders)
.fill(0)
.forEach((_, i) => {
_items.push({
type: 'placeholder',
key: i + '',
})
})
} }
} }
return _items return _items
}, [_, searchText, results, isError, currentAccount?.did, follows]) }, [
_,
searchText,
results,
isError,
currentAccount?.did,
follows,
convos,
showRecentConvos,
])
if (searchText && !isFetching && !items.length && !isError) { if (searchText && !isFetching && !items.length && !isError) {
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)}) items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
@@ -331,11 +395,13 @@ function ProfileCard({
/> />
<View style={[a.flex_1, a.gap_2xs]}> <View style={[a.flex_1, a.gap_2xs]}>
<Text <Text
style={[t.atoms.text, a.font_bold, a.leading_snug]} style={[t.atoms.text, a.font_bold, a.leading_tight]}
numberOfLines={1}> numberOfLines={1}>
{displayName} {displayName}
</Text> </Text>
<Text style={t.atoms.text_contrast_high} numberOfLines={2}> <Text
style={[a.leading_tight, t.atoms.text_contrast_high]}
numberOfLines={2}>
{!enabled ? <Trans>{handle} can't be messaged</Trans> : handle} {!enabled ? <Trans>{handle} can't be messaged</Trans> : handle}
</Text> </Text>
</View> </View>
@@ -46,6 +46,7 @@ export function SendViaChatDialog({
<SearchablePeopleList <SearchablePeopleList
title={_(msg`Send post to...`)} title={_(msg`Send post to...`)}
onSelectChat={onCreateChat} onSelectChat={onCreateChat}
showRecentConvos
/> />
</Dialog.Outer> </Dialog.Outer>
) )
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+60 -3
View File
@@ -4,18 +4,27 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {type SessionAccount, useSession, useSessionApi} from '#/state/session' import {
type SessionAccount,
useAgent,
useSession,
useSessionApi,
} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {ScrollView} from '#/view/com/util/Views' import {ScrollView} from '#/view/com/util/Views'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {AccountList} from '#/components/AccountList' import {AccountList} from '#/components/AccountList'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
const COL_WIDTH = 400 const COL_WIDTH = 400
@@ -30,6 +39,10 @@ export function Deactivated() {
const hasOtherAccounts = accounts.length > 1 const hasOtherAccounts = accounts.length > 1
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {logout} = useSessionApi() const {logout} = useSessionApi()
const agent = useAgent()
const [pending, setPending] = React.useState(false)
const [error, setError] = React.useState<string | undefined>()
const queryClient = useQueryClient()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -62,6 +75,34 @@ export function Deactivated() {
logout('Deactivated') logout('Deactivated')
}, [logout]) }, [logout])
const handleActivate = React.useCallback(async () => {
try {
setPending(true)
await agent.com.atproto.server.activateAccount()
await queryClient.resetQueries()
await agent.resumeSession(agent.session!)
} catch (e: any) {
switch (e.message) {
case 'Bad token scope':
setError(
_(
msg`You're logged in with an App Password. Please log in with your main password to continue deactivating your account.`,
),
)
break
default:
setError(_(msg`Something went wrong, please try again`))
break
}
logger.error(e, {
context: 'Failed to activate account',
})
} finally {
setPending(false)
}
}, [_, agent, setPending, setError, queryClient])
return ( return (
<View style={[a.h_full_vh, a.flex_1, t.atoms.bg]}> <View style={[a.h_full_vh, a.flex_1, t.atoms.bg]}>
<ScrollView <ScrollView
@@ -104,10 +145,11 @@ export function Deactivated() {
size="medium" size="medium"
variant="solid" variant="solid"
color="primary" color="primary"
onPress={() => setShowLoggedOut(true)}> onPress={handleActivate}>
<ButtonText> <ButtonText>
<Trans>Yes, reactivate my account</Trans> <Trans>Yes, reactivate my account</Trans>
</ButtonText> </ButtonText>
{pending && <ButtonIcon icon={Loader} position="right" />}
</Button> </Button>
<Button <Button
label={_(msg`Cancel reactivation and log out`)} label={_(msg`Cancel reactivation and log out`)}
@@ -120,6 +162,21 @@ export function Deactivated() {
</ButtonText> </ButtonText>
</Button> </Button>
</View> </View>
{error && (
<View
style={[
a.flex_row,
a.gap_sm,
a.mt_md,
a.p_md,
a.rounded_sm,
t.atoms.bg_contrast_25,
]}>
<CircleInfo size="md" fill={t.palette.negative_400} />
<Text style={[a.flex_1, a.leading_snug]}>{error}</Text>
</View>
)}
</View> </View>
<View style={[a.pb_3xl]}> <View style={[a.pb_3xl]}>
+2
View File
@@ -81,6 +81,7 @@ let ProfileHeaderShell = ({
{children} {children}
{!isPlaceholderProfile && (
<View <View
style={[a.px_lg, a.pb_sm]} style={[a.px_lg, a.pb_sm]}
pointerEvents={isIOS ? 'auto' : 'box-none'}> pointerEvents={isIOS ? 'auto' : 'box-none'}>
@@ -90,6 +91,7 @@ let ProfileHeaderShell = ({
<ProfileHeaderAlerts moderation={moderation} /> <ProfileHeaderAlerts moderation={moderation} />
)} )}
</View> </View>
)}
{!isDesktop && !hideBackButton && ( {!isDesktop && !hideBackButton && (
<TouchableWithoutFeedback <TouchableWithoutFeedback
@@ -3,9 +3,14 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf' import {logger} from '#/logger'
import {useAgent, useSessionApi} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {DialogOuterProps} from '#/components/Dialog' import {DialogOuterProps} from '#/components/Dialog'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -13,12 +18,58 @@ export function DeactivateAccountDialog({
control, control,
}: { }: {
control: DialogOuterProps['control'] control: DialogOuterProps['control']
}) {
return (
<Prompt.Outer control={control}>
<DeactivateAccountDialogInner control={control} />
</Prompt.Outer>
)
}
function DeactivateAccountDialogInner({
control,
}: {
control: DialogOuterProps['control']
}) { }) {
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints()
const {_} = useLingui() const {_} = useLingui()
const agent = useAgent()
const {logout} = useSessionApi()
const [pending, setPending] = React.useState(false)
const [error, setError] = React.useState<string | undefined>()
const handleDeactivate = React.useCallback(async () => {
try {
setPending(true)
await agent.com.atproto.server.deactivateAccount({})
control.close(() => {
logout('Deactivated')
})
} catch (e: any) {
switch (e.message) {
case 'Bad token scope':
setError(
_(
msg`You're logged in with an App Password. Please log in with your main password to continue deactivating your account.`,
),
)
break
default:
setError(_(msg`Something went wrong, please try again`))
break
}
logger.error(e, {
context: 'Failed to deactivate account',
})
} finally {
setPending(false)
}
}, [agent, control, logout, _, setPending])
return ( return (
<Prompt.Outer control={control} testID="confirmModal"> <>
<Prompt.TitleText>{_(msg`Deactivate account`)}</Prompt.TitleText> <Prompt.TitleText>{_(msg`Deactivate account`)}</Prompt.TitleText>
<Prompt.DescriptionText> <Prompt.DescriptionText>
<Trans> <Trans>
@@ -48,13 +99,32 @@ export function DeactivateAccountDialog({
<Divider /> <Divider />
</View> </View>
<Prompt.Actions> <Prompt.Actions>
<Prompt.Action <Button
cta={_(msg`Yes, deactivate`)} variant="solid"
onPress={() => {}}
color="negative" color="negative"
/> size={gtMobile ? 'small' : 'medium'}
label={_(msg`Yes, deactivate`)}
onPress={handleDeactivate}>
<ButtonText>{_(msg`Yes, deactivate`)}</ButtonText>
{pending && <ButtonIcon icon={Loader} position="right" />}
</Button>
<Prompt.Cancel /> <Prompt.Cancel />
</Prompt.Actions> </Prompt.Actions>
</Prompt.Outer>
{error && (
<View
style={[
a.flex_row,
a.gap_sm,
a.mt_md,
a.p_md,
a.rounded_sm,
t.atoms.bg_contrast_25,
]}>
<CircleInfo size="md" fill={t.palette.negative_400} />
<Text style={[a.flex_1, a.leading_snug]}>{error}</Text>
</View>
)}
</>
) )
} }
+6 -3
View File
@@ -18,9 +18,12 @@ const accountSchema = z.object({
refreshJwt: z.string().optional(), // optional because it can expire refreshJwt: z.string().optional(), // optional because it can expire
accessJwt: z.string().optional(), // optional because it can expire accessJwt: z.string().optional(), // optional because it can expire
signupQueued: z.boolean().optional(), signupQueued: z.boolean().optional(),
status: z active: z.boolean().optional(), // optional for backwards compat
.enum(['active', 'takendown', 'suspended', 'deactivated']) /**
.optional(), * Known values: takendown, suspended, deactivated
* @see https://github.com/bluesky-social/atproto/blob/5441fbde9ed3b22463e91481ec80cb095643e141/lexicons/com/atproto/server/getSession.json
*/
status: z.string().optional(),
pdsUrl: z.string().optional(), pdsUrl: z.string().optional(),
}) })
export type PersistedAccount = z.infer<typeof accountSchema> export type PersistedAccount = z.infer<typeof accountSchema>
+13 -2
View File
@@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import {Schema, schema} from '#/state/persisted/schema'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Schema, schema} from '#/state/persisted/schema'
const BSKY_STORAGE = 'BSKY_STORAGE' const BSKY_STORAGE = 'BSKY_STORAGE'
@@ -13,8 +13,19 @@ export async function write(value: Schema) {
export async function read(): Promise<Schema | undefined> { export async function read(): Promise<Schema | undefined> {
const rawData = await AsyncStorage.getItem(BSKY_STORAGE) const rawData = await AsyncStorage.getItem(BSKY_STORAGE)
const objData = rawData ? JSON.parse(rawData) : undefined const objData = rawData ? JSON.parse(rawData) : undefined
if (schema.safeParse(objData).success) { const parsed = schema.safeParse(objData)
if (parsed.success) {
return objData return objData
} else {
const errors =
parsed.error?.errors?.map(e => ({
code: e.code,
// @ts-ignore exists on some types
expected: e?.expected,
path: e.path,
})) || []
logger.error(`persisted store: data failed validation on read`, {errors})
return undefined
} }
} }
@@ -26,10 +26,15 @@ import {useAgent, useSession} from '#/state/session'
export const RQKEY = ['convo-list'] export const RQKEY = ['convo-list']
type RQPageParam = string | undefined type RQPageParam = string | undefined
export function useListConvosQuery() { export function useListConvosQuery({
enabled,
}: {
enabled?: boolean
} = {}) {
const agent = useAgent() const agent = useAgent()
return useInfiniteQuery({ return useInfiniteQuery({
enabled,
queryKey: RQKEY, queryKey: RQKEY,
queryFn: async ({pageParam}) => { queryFn: async ({pageParam}) => {
const {data} = await agent.api.chat.bsky.convo.listConvos( const {data} = await agent.api.chat.bsky.convo.listConvos(
+90 -29
View File
@@ -28,6 +28,7 @@ describe('session', () => {
const agent = new BskyAgent({service: 'https://alice.com'}) const agent = new BskyAgent({service: 'https://alice.com'})
agent.session = { agent.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -50,6 +51,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "alice-access-jwt-1", "accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -59,7 +61,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-1", "refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -88,6 +90,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": undefined, "accessJwt": undefined,
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -97,7 +100,7 @@ describe('session', () => {
"refreshJwt": undefined, "refreshJwt": undefined,
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -116,6 +119,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -138,6 +142,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "alice-access-jwt-1", "accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -147,7 +152,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-1", "refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -162,6 +167,7 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://bob.com'}) const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = { agent2.session = {
active: true,
did: 'bob-did', did: 'bob-did',
handle: 'bob.test', handle: 'bob.test',
accessJwt: 'bob-access-jwt-1', accessJwt: 'bob-access-jwt-1',
@@ -186,6 +192,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "bob-access-jwt-1", "accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did", "did": "bob-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -195,10 +202,11 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1", "refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/", "service": "https://bob.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
{ {
"accessJwt": "alice-access-jwt-1", "accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -208,7 +216,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-1", "refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -223,6 +231,7 @@ describe('session', () => {
const agent3 = new BskyAgent({service: 'https://alice.com'}) const agent3 = new BskyAgent({service: 'https://alice.com'})
agent3.session = { agent3.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice-updated.test', handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2', accessJwt: 'alice-access-jwt-2',
@@ -247,6 +256,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "alice-access-jwt-2", "accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -256,10 +266,11 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2", "refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
{ {
"accessJwt": "bob-access-jwt-1", "accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did", "did": "bob-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -269,7 +280,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1", "refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/", "service": "https://bob.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -284,6 +295,7 @@ describe('session', () => {
const agent4 = new BskyAgent({service: 'https://jay.com'}) const agent4 = new BskyAgent({service: 'https://jay.com'})
agent4.session = { agent4.session = {
active: true,
did: 'jay-did', did: 'jay-did',
handle: 'jay.test', handle: 'jay.test',
accessJwt: 'jay-access-jwt-1', accessJwt: 'jay-access-jwt-1',
@@ -306,6 +318,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "jay-access-jwt-1", "accessJwt": "jay-access-jwt-1",
"active": true,
"did": "jay-did", "did": "jay-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -315,10 +328,11 @@ describe('session', () => {
"refreshJwt": "jay-refresh-jwt-1", "refreshJwt": "jay-refresh-jwt-1",
"service": "https://jay.com/", "service": "https://jay.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
{ {
"accessJwt": "alice-access-jwt-2", "accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -328,10 +342,11 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2", "refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
{ {
"accessJwt": "bob-access-jwt-1", "accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did", "did": "bob-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -341,7 +356,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1", "refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/", "service": "https://bob.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -374,6 +389,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": undefined, "accessJwt": undefined,
"active": true,
"did": "jay-did", "did": "jay-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -383,10 +399,11 @@ describe('session', () => {
"refreshJwt": undefined, "refreshJwt": undefined,
"service": "https://jay.com/", "service": "https://jay.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
{ {
"accessJwt": undefined, "accessJwt": undefined,
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -396,10 +413,11 @@ describe('session', () => {
"refreshJwt": undefined, "refreshJwt": undefined,
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
{ {
"accessJwt": undefined, "accessJwt": undefined,
"active": true,
"did": "bob-did", "did": "bob-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -409,7 +427,7 @@ describe('session', () => {
"refreshJwt": undefined, "refreshJwt": undefined,
"service": "https://bob.com/", "service": "https://bob.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -428,6 +446,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -459,6 +478,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": undefined, "accessJwt": undefined,
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -468,7 +488,7 @@ describe('session', () => {
"refreshJwt": undefined, "refreshJwt": undefined,
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -483,6 +503,7 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://alice.com'}) const agent2 = new BskyAgent({service: 'https://alice.com'})
agent2.session = { agent2.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-2', accessJwt: 'alice-access-jwt-2',
@@ -504,6 +525,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "alice-access-jwt-2", "accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -513,7 +535,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2", "refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -532,6 +554,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -576,6 +599,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -583,6 +607,7 @@ describe('session', () => {
} }
const agent2 = new BskyAgent({service: 'https://bob.com'}) const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = { agent2.session = {
active: true,
did: 'bob-did', did: 'bob-did',
handle: 'bob.test', handle: 'bob.test',
accessJwt: 'bob-access-jwt-1', accessJwt: 'bob-access-jwt-1',
@@ -616,6 +641,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "bob-access-jwt-1", "accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did", "did": "bob-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -625,7 +651,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1", "refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/", "service": "https://bob.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -653,6 +679,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -669,6 +696,7 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('alice-did') expect(state.currentAgentState.did).toBe('alice-did')
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice-updated.test', handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2', accessJwt: 'alice-access-jwt-2',
@@ -697,6 +725,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "alice-access-jwt-2", "accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": "alice@foo.bar", "email": "alice@foo.bar",
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -706,7 +735,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2", "refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -720,6 +749,7 @@ describe('session', () => {
`) `)
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice-updated.test', handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-3', accessJwt: 'alice-access-jwt-3',
@@ -748,6 +778,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "alice-access-jwt-3", "accessJwt": "alice-access-jwt-3",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": "alice@foo.baz", "email": "alice@foo.baz",
"emailAuthFactor": true, "emailAuthFactor": true,
@@ -757,7 +788,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-3", "refreshJwt": "alice-refresh-jwt-3",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -771,6 +802,7 @@ describe('session', () => {
`) `)
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice-updated.test', handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-4', accessJwt: 'alice-access-jwt-4',
@@ -799,6 +831,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "alice-access-jwt-4", "accessJwt": "alice-access-jwt-4",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": "alice@foo.baz", "email": "alice@foo.baz",
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -808,7 +841,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-4", "refreshJwt": "alice-refresh-jwt-4",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -827,6 +860,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -843,6 +877,7 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('alice-did') expect(state.currentAgentState.did).toBe('alice-did')
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice-updated.test', handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2', accessJwt: 'alice-access-jwt-2',
@@ -873,6 +908,7 @@ describe('session', () => {
expect(lastState === state).toBe(true) expect(lastState === state).toBe(true)
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice-updated.test', handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-3', accessJwt: 'alice-access-jwt-3',
@@ -896,6 +932,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -904,6 +941,7 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://bob.com'}) const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = { agent2.session = {
active: true,
did: 'bob-did', did: 'bob-did',
handle: 'bob.test', handle: 'bob.test',
accessJwt: 'bob-access-jwt-1', accessJwt: 'bob-access-jwt-1',
@@ -928,6 +966,7 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('bob-did') expect(state.currentAgentState.did).toBe('bob-did')
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice-updated.test', handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2', accessJwt: 'alice-access-jwt-2',
@@ -956,6 +995,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "bob-access-jwt-1", "accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did", "did": "bob-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -965,10 +1005,11 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1", "refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/", "service": "https://bob.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
{ {
"accessJwt": "alice-access-jwt-2", "accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": "alice@foo.bar", "email": "alice@foo.bar",
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -978,7 +1019,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2", "refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -992,6 +1033,7 @@ describe('session', () => {
`) `)
agent2.session = { agent2.session = {
active: true,
did: 'bob-did', did: 'bob-did',
handle: 'bob-updated.test', handle: 'bob-updated.test',
accessJwt: 'bob-access-jwt-2', accessJwt: 'bob-access-jwt-2',
@@ -1018,6 +1060,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "bob-access-jwt-2", "accessJwt": "bob-access-jwt-2",
"active": true,
"did": "bob-did", "did": "bob-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -1027,10 +1070,11 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-2", "refreshJwt": "bob-refresh-jwt-2",
"service": "https://bob.com/", "service": "https://bob.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
{ {
"accessJwt": "alice-access-jwt-2", "accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": "alice@foo.bar", "email": "alice@foo.bar",
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -1040,7 +1084,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2", "refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -1083,6 +1127,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -1091,6 +1136,7 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://bob.com'}) const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = { agent2.session = {
active: true,
did: 'bob-did', did: 'bob-did',
handle: 'bob.test', handle: 'bob.test',
accessJwt: 'bob-access-jwt-1', accessJwt: 'bob-access-jwt-1',
@@ -1117,6 +1163,7 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('bob-did') expect(state.currentAgentState.did).toBe('bob-did')
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-2', accessJwt: 'alice-access-jwt-2',
@@ -1142,6 +1189,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -1179,6 +1227,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "alice-access-jwt-1", "accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -1188,7 +1237,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-1", "refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -1207,6 +1256,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -1242,6 +1292,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": undefined, "accessJwt": undefined,
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -1251,7 +1302,7 @@ describe('session', () => {
"refreshJwt": undefined, "refreshJwt": undefined,
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -1270,6 +1321,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -1305,6 +1357,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": undefined, "accessJwt": undefined,
"active": true,
"did": "alice-did", "did": "alice-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -1314,7 +1367,7 @@ describe('session', () => {
"refreshJwt": undefined, "refreshJwt": undefined,
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -1333,6 +1386,7 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'}) const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = { agent1.session = {
active: true,
did: 'alice-did', did: 'alice-did',
handle: 'alice.test', handle: 'alice.test',
accessJwt: 'alice-access-jwt-1', accessJwt: 'alice-access-jwt-1',
@@ -1340,6 +1394,7 @@ describe('session', () => {
} }
const agent2 = new BskyAgent({service: 'https://bob.com'}) const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = { agent2.session = {
active: true,
did: 'bob-did', did: 'bob-did',
handle: 'bob.test', handle: 'bob.test',
accessJwt: 'bob-access-jwt-1', accessJwt: 'bob-access-jwt-1',
@@ -1362,6 +1417,7 @@ describe('session', () => {
const anotherTabAgent1 = new BskyAgent({service: 'https://jay.com'}) const anotherTabAgent1 = new BskyAgent({service: 'https://jay.com'})
anotherTabAgent1.session = { anotherTabAgent1.session = {
active: true,
did: 'jay-did', did: 'jay-did',
handle: 'jay.test', handle: 'jay.test',
accessJwt: 'jay-access-jwt-1', accessJwt: 'jay-access-jwt-1',
@@ -1369,6 +1425,7 @@ describe('session', () => {
} }
const anotherTabAgent2 = new BskyAgent({service: 'https://alice.com'}) const anotherTabAgent2 = new BskyAgent({service: 'https://alice.com'})
anotherTabAgent2.session = { anotherTabAgent2.session = {
active: true,
did: 'bob-did', did: 'bob-did',
handle: 'bob.test', handle: 'bob.test',
accessJwt: 'bob-access-jwt-2', accessJwt: 'bob-access-jwt-2',
@@ -1397,6 +1454,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "jay-access-jwt-1", "accessJwt": "jay-access-jwt-1",
"active": true,
"did": "jay-did", "did": "jay-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -1406,10 +1464,11 @@ describe('session', () => {
"refreshJwt": "jay-refresh-jwt-1", "refreshJwt": "jay-refresh-jwt-1",
"service": "https://jay.com/", "service": "https://jay.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
{ {
"accessJwt": "bob-access-jwt-2", "accessJwt": "bob-access-jwt-2",
"active": true,
"did": "bob-did", "did": "bob-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -1419,7 +1478,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-2", "refreshJwt": "bob-refresh-jwt-2",
"service": "https://alice.com/", "service": "https://alice.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
@@ -1434,6 +1493,7 @@ describe('session', () => {
const anotherTabAgent3 = new BskyAgent({service: 'https://clarence.com'}) const anotherTabAgent3 = new BskyAgent({service: 'https://clarence.com'})
anotherTabAgent3.session = { anotherTabAgent3.session = {
active: true,
did: 'clarence-did', did: 'clarence-did',
handle: 'clarence.test', handle: 'clarence.test',
accessJwt: 'clarence-access-jwt-2', accessJwt: 'clarence-access-jwt-2',
@@ -1457,6 +1517,7 @@ describe('session', () => {
"accounts": [ "accounts": [
{ {
"accessJwt": "clarence-access-jwt-2", "accessJwt": "clarence-access-jwt-2",
"active": true,
"did": "clarence-did", "did": "clarence-did",
"email": undefined, "email": undefined,
"emailAuthFactor": false, "emailAuthFactor": false,
@@ -1466,7 +1527,7 @@ describe('session', () => {
"refreshJwt": "clarence-refresh-jwt-2", "refreshJwt": "clarence-refresh-jwt-2",
"service": "https://clarence.com/", "service": "https://clarence.com/",
"signupQueued": false, "signupQueued": false,
"status": "active", "status": undefined,
}, },
], ],
"currentAgentState": { "currentAgentState": {
+7 -2
View File
@@ -46,6 +46,11 @@ export async function createAgentAndResume(
emailConfirmed: storedAccount.emailConfirmed, emailConfirmed: storedAccount.emailConfirmed,
handle: storedAccount.handle, handle: storedAccount.handle,
refreshJwt: storedAccount.refreshJwt ?? '', refreshJwt: storedAccount.refreshJwt ?? '',
/**
* @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188
*/
active: storedAccount.active ?? true,
status: storedAccount.status,
} }
if (isSessionExpired(storedAccount)) { if (isSessionExpired(storedAccount)) {
await networkRetry(1, () => agent.resumeSession(prevSession)) await networkRetry(1, () => agent.resumeSession(prevSession))
@@ -235,8 +240,8 @@ export function agentToSessionAccount(
refreshJwt: agent.session.refreshJwt, refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt, accessJwt: agent.session.accessJwt,
signupQueued: isSignupQueued(agent.session.accessJwt), signupQueued: isSignupQueued(agent.session.accessJwt),
// @ts-expect-error TODO remove when backend is ready active: agent.session.active,
status: agent.session.status || 'active', status: agent.session.status as SessionAccount['status'],
pdsUrl: agent.pdsUrl?.toString(), pdsUrl: agent.pdsUrl?.toString(),
} }
} }
+8 -1
View File
@@ -154,6 +154,13 @@ export const ComposePost = observer(function ComposePost({
const [extGif, setExtGif] = useState<Gif>() const [extGif, setExtGif] = useState<Gif>()
const [labels, setLabels] = useState<string[]>([]) const [labels, setLabels] = useState<string[]>([])
const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([]) const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
React.useEffect(() => {
if (!isAndroid) return
const id = setTimeout(() => textInput.current?.focus(), 100)
return () => clearTimeout(id)
}, [])
const gallery = useMemo( const gallery = useMemo(
() => new GalleryModel(initImageUris), () => new GalleryModel(initImageUris),
[initImageUris], [initImageUris],
@@ -517,7 +524,7 @@ export const ComposePost = observer(function ComposePost({
ref={textInput} ref={textInput}
richtext={richtext} richtext={richtext}
placeholder={selectTextInputPlaceholder} placeholder={selectTextInputPlaceholder}
autoFocus={true} autoFocus={!isAndroid}
setRichText={setRichText} setRichText={setRichText}
onPhotoPasted={onPhotoPasted} onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish} onPressPublish={onPressPublish}
+6 -1
View File
@@ -161,7 +161,11 @@ export function Feed({
) )
} else if (item === LOADING_ITEM) { } else if (item === LOADING_ITEM) {
return ( return (
<View style={[pal.border, {borderTopWidth: hairlineWidth}]}> <View
style={[
pal.border,
!isTabletOrMobile && {borderTopWidth: hairlineWidth},
]}>
<NotificationFeedLoadingPlaceholder /> <NotificationFeedLoadingPlaceholder />
</View> </View>
) )
@@ -217,6 +221,7 @@ export function Feed({
desktopFixedHeight desktopFixedHeight
initialNumToRender={initialNumToRender} initialNumToRender={initialNumToRender}
windowSize={11} windowSize={11}
sideBorders={false}
/> />
</View> </View>
) )
@@ -108,6 +108,7 @@ let RepostButton = ({
</Text> </Text>
</Button> </Button>
<Button <Button
testID="quoteBtn"
style={[a.justify_start, a.px_md]} style={[a.justify_start, a.px_md]}
label={_(msg`Quote post`)} label={_(msg`Quote post`)}
onPress={() => { onPress={() => {
+4 -2
View File
@@ -181,9 +181,11 @@ export function QuoteEmbed({
}, [queryClient, quote.author, onOpen]) }, [queryClient, quote.author, onOpen])
return ( return (
<ContentHider modui={moderation?.ui('contentList')}> <ContentHider
<Link modui={moderation?.ui('contentList')}
style={[styles.container, pal.borderDark, style]} style={[styles.container, pal.borderDark, style]}
childContainerStyle={[a.pt_sm]}>
<Link
hoverStyle={{borderColor: pal.colors.borderLinkHover}} hoverStyle={{borderColor: pal.colors.borderLinkHover}}
href={itemHref} href={itemHref}
title={itemTitle} title={itemTitle}
+6 -2
View File
@@ -29,6 +29,7 @@ import {colors, s} from 'lib/styles'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {ListMethods} from 'view/com/util/List' import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {CenteredView} from 'view/com/util/Views'
import {Feed} from '../com/notifications/Feed' import {Feed} from '../com/notifications/Feed'
import {FAB} from '../com/util/fab/FAB' import {FAB} from '../com/util/fab/FAB'
import {MainScrollProvider} from '../com/util/MainScrollProvider' import {MainScrollProvider} from '../com/util/MainScrollProvider'
@@ -145,7 +146,10 @@ export function NotificationsScreen({}: Props) {
}, [isDesktop, pal, hasNew]) }, [isDesktop, pal, hasNew])
return ( return (
<View testID="notificationsScreen" style={s.hContentRegion}> <CenteredView
testID="notificationsScreen"
style={s.hContentRegion}
sideBorders={true}>
<ViewHeader <ViewHeader
title={_(msg`Notifications`)} title={_(msg`Notifications`)}
canGoBack={false} canGoBack={false}
@@ -173,6 +177,6 @@ export function NotificationsScreen({}: Props) {
accessibilityLabel={_(msg`New post`)} accessibilityLabel={_(msg`New post`)}
accessibilityHint="" accessibilityHint=""
/> />
</View> </CenteredView>
) )
} }
+7 -8
View File
@@ -32,6 +32,7 @@ import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
import hairlineWidth = StyleSheet.hairlineWidth
const HITSLOP_TOP = { const HITSLOP_TOP = {
top: 20, top: 20,
@@ -92,7 +93,7 @@ export function SavedFeeds({}: Props) {
<ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder /> <ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder />
<ScrollView style={s.flex1} contentContainerStyle={[styles.noBorder]}> <ScrollView style={s.flex1} contentContainerStyle={[styles.noBorder]}>
{noSavedFeedsOfAnyType && ( {noSavedFeedsOfAnyType && (
<View style={[pal.border, {borderBottomWidth: 1}]}> <View style={[pal.border, {borderBottomWidth: hairlineWidth}]}>
<NoSavedFeedsOfAnyType /> <NoSavedFeedsOfAnyType />
</View> </View>
)} )}
@@ -134,7 +135,7 @@ export function SavedFeeds({}: Props) {
)} )}
{noFollowingFeed && ( {noFollowingFeed && (
<View style={[pal.border, {borderBottomWidth: 1}]}> <View style={[pal.border, {borderBottomWidth: hairlineWidth}]}>
<NoFollowingFeed /> <NoFollowingFeed />
</View> </View>
)} )}
@@ -298,9 +299,10 @@ function ListItem({
<FeedSourceCard <FeedSourceCard
key={feedUri} key={feedUri}
feedUri={feedUri} feedUri={feedUri}
style={[styles.noTopBorder, isPinned && {paddingRight: 8}]} style={[isPinned && {paddingRight: 8}]}
showMinimalPlaceholder showMinimalPlaceholder
showSaveBtn={!isPinned} showSaveBtn={!isPinned}
hideTopBorder={true}
/> />
)} )}
{isPinned ? ( {isPinned ? (
@@ -435,15 +437,12 @@ const styles = StyleSheet.create({
paddingHorizontal: 14, paddingHorizontal: 14,
paddingTop: 20, paddingTop: 20,
paddingBottom: 10, paddingBottom: 10,
borderBottomWidth: 1, borderBottomWidth: hairlineWidth,
}, },
itemContainer: { itemContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderBottomWidth: 1, borderBottomWidth: hairlineWidth,
},
noTopBorder: {
borderTopWidth: 0,
}, },
footerText: { footerText: {
paddingHorizontal: 26, paddingHorizontal: 26,
+4 -4
View File
@@ -34,10 +34,10 @@
jsonpointer "^5.0.0" jsonpointer "^5.0.0"
leven "^3.1.0" leven "^3.1.0"
"@atproto/api@^0.12.14": "@atproto/api@^0.12.16":
version "0.12.14" version "0.12.16"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.14.tgz#81252fd166ec8fe950056531e690d563437720fa" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.16.tgz#f5b5e06d75d379dafe79521d727ed8ad5516d3fc"
integrity sha512-ZPh/afoRjFEQDQgMZW2FQiG5CDUifY7SxBqI0zVJUwed8Zi6fqYzGYM8fcDvD8yJfflRCqRxUE72g5fKiA1zAQ== integrity sha512-v3lA/m17nkawDXiqgwXyaUSzJPeXJBMH8QKOoYxcDqN+8yG9LFlGe2ecGarXcbGQjYT0GJTAAW3Y/AaCOEwuLg==
dependencies: dependencies:
"@atproto/common-web" "^0.3.0" "@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0" "@atproto/lexicon" "^0.4.0"