Compare commits

..

2 Commits

Author SHA1 Message Date
Eric Bailey 7d840faacc Make it safer 2024-06-04 14:19:34 -05:00
Eric Bailey ded615d9f9 Report persisted schema validation failures 2024-06-04 14:17:00 -05:00
67 changed files with 15540 additions and 17492 deletions
+2 -9
View File
@@ -34,9 +34,6 @@ jobs:
node-version-file: .nvmrc
cache: yarn
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
with:
@@ -99,17 +96,13 @@ jobs:
name: build-${{ steps.timestamp.outputs.time }}.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
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"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 }}```"
"text": "Android build is ready for submission. This is a production build! Download the artifact here: ${{ steps.upload-artifact-production.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
@@ -147,7 +140,7 @@ jobs:
with:
payload: |
{
"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 }}```"
"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 }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
-19
View File
@@ -34,9 +34,6 @@ jobs:
node-version-file: .nvmrc
cache: yarn
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
with:
@@ -84,22 +81,6 @@ jobs:
- name: 🚀 Deploy
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
id: get-base-commit
uses: actions/cache@v4
+1 -1
View File
@@ -50,7 +50,7 @@ appId: xyz.blueskyweb.app
text: "1"
- tapOn:
id: "repostBtn"
- tapOn: "Remove repost"
- tapOn: "Undo repost"
- assertNotVisible:
id: "repostCount"
+31
View File
@@ -0,0 +1,31 @@
# 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
+1
View File
@@ -204,6 +204,7 @@ module.exports = function (config) {
],
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidManifestLaunchModePlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
+8 -22
View File
@@ -14,20 +14,15 @@ diff results in incompatible native changes, a new client build will automatical
### Prerequisites
- Find the latest production build number for both iOS and Android in Slack. These are listed in #client-builds
- Production builds always send the Version Number and Build Number in the Slack message. Search for the latest
production version number, and you should find the correct information.
![slack-build-info](./img/slack-build-info.png)
- 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.
- Remove any internal client from your device and download the client from the App Store/Google Play. This will help for
testing as well as retrieving the build number.
- You should have signed in to EAS locally through npx eas login. You will need to modify the build number in a
subsequent step.
- Identify the build number of the production app you want to deploy an update for. iOS and Android build numbers are
divergent, so you will need to find both
![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
- 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.
@@ -43,17 +38,14 @@ to create your branch from, this should be properly set.
### Deployment
- Update the build number through EAS to match the build numbers of the
production iOS/Android apps
- Update the build number through EAS
- 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”
for internal builds, I felt it useful to make sure it doesnt accidentally become a prod deployment.
- Set the build numbers to the values found in the prerequisite steps. Again, this should be the
- Set the build number to the appropriate build number found in the prerequisite steps. Again, this should be the
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 android`
- These steps should spit out what the current build number is, save those values
for later too
- Run the deployment
- Navigate to https://github.com/bluesky-social/social-app/actions/workflows/bundle-deploy-eas-update.yml
- Select the “Run Workflow” dropdown
@@ -87,9 +79,3 @@ In about five minutes, the new deployment should be available for download. To t
- Launch the app once and wait approximately 15 seconds
- 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.
### 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.

Before

Width:  |  Height:  |  Size: 99 KiB

+1 -7
View File
@@ -1,5 +1,4 @@
import UserNotifications
import UIKit
let APP_GROUP = "group.app.bsky"
@@ -32,12 +31,7 @@ class NotificationService: UNNotificationServiceExtension {
}
func mutateWithBadge(_ content: UNMutableNotificationContent) {
var count = prefs?.integer(forKey: "badgeCount") ?? 0
count += 1
// Set the new badge number for the notification, then store that value for using later
content.badge = NSNumber(value: count)
prefs?.setValue(count, forKey: "badgeCount")
content.badge = 1
}
func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
@@ -66,9 +66,5 @@ class ExpoBackgroundNotificationHandlerModule : Module() {
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
}
AsyncFunction("setBadgeCountAsync") { _: Int ->
// This does nothing on Android
}
}
}
@@ -10,8 +10,7 @@ let DEFAULTS: [String:Any] = [
"playSoundQuote": false,
"playSoundReply": false,
"playSoundRepost": false,
"mutedThreads": [:] as! [String:[String]],
"badgeCount": 0,
"mutedThreads": [:] as! [String:[String]]
]
/*
@@ -113,9 +112,5 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey)
}
}
AsyncFunction("setBadgeCountAsync") { (count: Int) in
userDefaults?.setValue(count, forKey: "badgeCount")
}
}
}
@@ -31,7 +31,6 @@ export type ExpoBackgroundNotificationHandlerModule = {
forKey: keyof BackgroundNotificationHandlerPreferences,
value: string[],
) => Promise<void>
setBadgeCountAsync: (count: number) => Promise<void>
}
// TODO there are more preferences in the native code, however they have not been added here yet.
@@ -24,5 +24,4 @@ export const BackgroundNotificationHandler = {
removeFromStringArrayAsync: async (_: string, __: string) => {},
addManyToStringArrayAsync: async (_: string, __: string[]) => {},
removeManyFromStringArrayAsync: async (_: string, __: string[]) => {},
setBadgeCountAsync: async (_: number) => {},
} as ExpoBackgroundNotificationHandlerModule
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.86.0",
"version": "1.85.0",
"private": true,
"engines": {
"node": ">=18"
@@ -49,7 +49,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto/api": "^0.12.16",
"@atproto/api": "^0.12.14",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -116,6 +116,7 @@
"expo-build-properties": "^0.12.1",
"expo-camera": "~15.0.9",
"expo-clipboard": "^6.0.3",
"expo-constants": "~16.0.1",
"expo-dev-client": "^4.0.14",
"expo-device": "~6.0.2",
"expo-file-system": "^17.0.1",
@@ -128,7 +129,7 @@
"expo-localization": "~15.0.3",
"expo-media-library": "~16.0.3",
"expo-navigation-bar": "~3.0.4",
"expo-notifications": "~0.28.7",
"expo-notifications": "~0.28.3",
"expo-sharing": "^12.0.1",
"expo-splash-screen": "~0.27.4",
"expo-status-bar": "~1.12.1",
@@ -271,8 +272,6 @@
"resolutions": {
"@types/react": "^18",
"**/zeed-dom": "0.10.9",
"**/expo-constants": "16.0.1",
"**/expo-device": "6.0.2",
"@react-native/babel-preset": "0.74.1"
},
"jest": {
-5
View File
@@ -1,5 +0,0 @@
## expo-modules-core Patch
This patch fixes crashes seen in some Android clients when using intents to open the app. See https://github.com/expo/expo/pull/29513.
Do not remove this patch until that PR lands in Expo and is released.
-14
View File
@@ -1,14 +0,0 @@
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
index bb74e80..0aa0202 100644
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
@@ -90,8 +90,8 @@ public class NativeModulesProxy extends ReactContextBaseJavaModule {
mModuleRegistry.ensureIsInitialized();
KotlinInteropModuleRegistry kotlinModuleRegistry = getKotlinInteropModuleRegistry();
- kotlinModuleRegistry.emitOnCreate();
kotlinModuleRegistry.installJSIInterop();
+ kotlinModuleRegistry.emitOnCreate();
Map<String, Object> constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
-16
View File
@@ -67,19 +67,3 @@ index b09e653..4c32b31 100644
+}
+
@end
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
index 5f5e1ab..aac00b6 100644
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
@@ -99,8 +99,9 @@ public class JavaTimerManager {
}
// If the JS thread is busy for multiple frames we cancel any other pending runnable.
- if (mCurrentIdleCallbackRunnable != null) {
- mCurrentIdleCallbackRunnable.cancel();
+ IdleCallbackRunnable currentRunnable = mCurrentIdleCallbackRunnable;
+ if (currentRunnable != null) {
+ currentRunnable.cancel();
}
mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
@@ -0,0 +1,17 @@
const {withAndroidManifest} = require('expo/config-plugins')
module.exports = function withAndroidManifestLaunchModePlugin(appConfig) {
return withAndroidManifest(appConfig, function (decoratedAppConfig) {
try {
const mainApplication =
decoratedAppConfig.modResults.manifest.application[0]
const mainActivity = mainApplication.activity.find(
elem => elem.$['android:name'] === '.MainActivity',
)
mainActivity.$['android:launchMode'] = 'singleTop'
} catch (e) {
console.error(`withAndroidManifestLaunchModePlugin failed`, e)
}
return decoratedAppConfig
})
}
-9
View File
@@ -1,9 +0,0 @@
#!/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"
@@ -395,13 +395,11 @@ function ProfileCard({
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Text
style={[t.atoms.text, a.font_bold, a.leading_tight]}
style={[t.atoms.text, a.font_bold, a.leading_snug]}
numberOfLines={1}>
{displayName}
</Text>
<Text
style={[a.leading_tight, t.atoms.text_contrast_high]}
numberOfLines={2}>
<Text style={t.atoms.text_contrast_high} numberOfLines={2}>
{!enabled ? <Trans>{handle} can't be messaged</Trans> : handle}
</Text>
</View>
@@ -14,25 +14,9 @@ export function SendViaChatDialog({
}: {
control: Dialog.DialogControlProps
onSelectChat: (chatId: string) => void
}) {
return (
<Dialog.Outer
control={control}
testID="sendViaChatChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
</Dialog.Outer>
)
}
function SendViaChatDialogInner({
control,
onSelectChat,
}: {
control: Dialog.DialogControlProps
onSelectChat: (chatId: string) => void
}) {
const {_} = useLingui()
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
onSelectChat(data.convo.id)
@@ -55,10 +39,15 @@ function SendViaChatDialogInner({
)
return (
<SearchablePeopleList
title={_(msg`Send post to...`)}
onSelectChat={onCreateChat}
showRecentConvos
/>
<Dialog.Outer
control={control}
testID="sendViaChatChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<SearchablePeopleList
title={_(msg`Send post to...`)}
onSelectChat={onCreateChat}
showRecentConvos
/>
</Dialog.Outer>
)
}
+1 -3
View File
@@ -3,9 +3,8 @@ import {
launchImageLibraryAsync,
MediaTypeOptions,
} from 'expo-image-picker'
import * as Toast from 'view/com/util/Toast'
import {getDataUriSize} from './util'
import * as Toast from 'view/com/util/Toast'
export async function openPicker(opts?: ImagePickerOptions) {
const response = await launchImageLibraryAsync({
@@ -13,7 +12,6 @@ export async function openPicker(opts?: ImagePickerOptions) {
mediaTypes: MediaTypeOptions.Images,
quality: 1,
...opts,
legacy: true,
})
if (response.assets && response.assets.length > 4) {
+11 -13
View File
@@ -7,7 +7,6 @@ import {logger} from '#/logger'
import {SessionAccount, useAgent, useSession} from '#/state/session'
import {logEvent, useGate} from 'lib/statsig/statsig'
import {devicePlatform, isNative} from 'platform/detection'
import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
const SERVICE_DID = (serviceUrl?: string) =>
serviceUrl?.includes('staging')
@@ -109,20 +108,19 @@ export function useRequestNotificationsPermission() {
}
}
export async function decrementBadgeCount(by: number) {
export async function decrementBadgeCount(by: number | 'reset' = 1) {
if (!isNative) return
let count = await getBadgeCountAsync()
count -= by
if (count < 0) {
count = 0
const currCount = await getBadgeCountAsync()
if (by === 'reset') {
await setBadgeCountAsync(0)
return
}
await BackgroundNotificationHandler.setBadgeCountAsync(count)
await setBadgeCountAsync(count)
}
export async function resetBadgeCount() {
await BackgroundNotificationHandler.setBadgeCountAsync(0)
await setBadgeCountAsync(0)
let newCount = currCount - by
if (newCount < 0) {
newCount = 0
}
await setBadgeCountAsync(newCount)
}
-1
View File
@@ -3,4 +3,3 @@ export type Gate =
| 'request_notifications_permission_after_onboarding_v2'
| 'show_avi_follow_button'
| 'show_follow_back_label_v2'
| 'show_notification_badge_mobile_web'
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
+3 -60
View File
@@ -4,27 +4,18 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {
type SessionAccount,
useAgent,
useSession,
useSessionApi,
} from '#/state/session'
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {ScrollView} from '#/view/com/util/Views'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {AccountList} from '#/components/AccountList'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Button, ButtonText} from '#/components/Button'
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'
const COL_WIDTH = 400
@@ -39,10 +30,6 @@ export function Deactivated() {
const hasOtherAccounts = accounts.length > 1
const setMinimalShellMode = useSetMinimalShellMode()
const {logout} = useSessionApi()
const agent = useAgent()
const [pending, setPending] = React.useState(false)
const [error, setError] = React.useState<string | undefined>()
const queryClient = useQueryClient()
useFocusEffect(
React.useCallback(() => {
@@ -75,34 +62,6 @@ export function Deactivated() {
logout('Deactivated')
}, [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 (
<View style={[a.h_full_vh, a.flex_1, t.atoms.bg]}>
<ScrollView
@@ -145,11 +104,10 @@ export function Deactivated() {
size="medium"
variant="solid"
color="primary"
onPress={handleActivate}>
onPress={() => setShowLoggedOut(true)}>
<ButtonText>
<Trans>Yes, reactivate my account</Trans>
</ButtonText>
{pending && <ButtonIcon icon={Loader} position="right" />}
</Button>
<Button
label={_(msg`Cancel reactivation and log out`)}
@@ -162,21 +120,6 @@ export function Deactivated() {
</ButtonText>
</Button>
</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 style={[a.pb_3xl]}>
@@ -102,7 +102,6 @@ export function StepProfile() {
mediaTypes: MediaTypeOptions.Images,
quality: 1,
...opts,
legacy: true,
})
return (response.assets ?? [])
+9 -11
View File
@@ -81,17 +81,15 @@ let ProfileHeaderShell = ({
{children}
{!isPlaceholderProfile && (
<View
style={[a.px_lg, a.pb_sm]}
pointerEvents={isIOS ? 'auto' : 'box-none'}>
{isMe ? (
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} />
) : (
<ProfileHeaderAlerts moderation={moderation} />
)}
</View>
)}
<View
style={[a.px_lg, a.pb_sm]}
pointerEvents={isIOS ? 'auto' : 'box-none'}>
{isMe ? (
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} />
) : (
<ProfileHeaderAlerts moderation={moderation} />
)}
</View>
{!isDesktop && !hideBackButton && (
<TouchableWithoutFeedback
@@ -3,14 +3,9 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
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 {atoms as a, useTheme} from '#/alf'
import {DialogOuterProps} from '#/components/Dialog'
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 {Text} from '#/components/Typography'
@@ -18,58 +13,12 @@ export function DeactivateAccountDialog({
control,
}: {
control: DialogOuterProps['control']
}) {
return (
<Prompt.Outer control={control}>
<DeactivateAccountDialogInner control={control} />
</Prompt.Outer>
)
}
function DeactivateAccountDialogInner({
control,
}: {
control: DialogOuterProps['control']
}) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
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 (
<>
<Prompt.Outer control={control} testID="confirmModal">
<Prompt.TitleText>{_(msg`Deactivate account`)}</Prompt.TitleText>
<Prompt.DescriptionText>
<Trans>
@@ -99,32 +48,13 @@ function DeactivateAccountDialogInner({
<Divider />
</View>
<Prompt.Actions>
<Button
variant="solid"
<Prompt.Action
cta={_(msg`Yes, deactivate`)}
onPress={() => {}}
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.Actions>
{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>
)}
</>
</Prompt.Outer>
)
}
+3 -6
View File
@@ -18,12 +18,9 @@ const accountSchema = z.object({
refreshJwt: z.string().optional(), // optional because it can expire
accessJwt: z.string().optional(), // optional because it can expire
signupQueued: z.boolean().optional(),
active: z.boolean().optional(), // optional for backwards compat
/**
* Known values: takendown, suspended, deactivated
* @see https://github.com/bluesky-social/atproto/blob/5441fbde9ed3b22463e91481ec80cb095643e141/lexicons/com/atproto/server/getSession.json
*/
status: z.string().optional(),
status: z
.enum(['active', 'takendown', 'suspended', 'deactivated'])
.optional(),
pdsUrl: z.string().optional(),
})
export type PersistedAccount = z.infer<typeof accountSchema>
+2 -2
View File
@@ -11,7 +11,7 @@ import BroadcastChannel from '#/lib/broadcast'
import {logger} from '#/logger'
import {useMutedThreads} from '#/state/muted-threads'
import {useAgent, useSession} from '#/state/session'
import {resetBadgeCount} from 'lib/notifications/notifications'
import {decrementBadgeCount} from 'lib/notifications/notifications'
import {useModerationOpts} from '../../preferences/moderation-opts'
import {truncateAndInvalidate} from '../util'
import {RQKEY as RQKEY_NOTIFS} from './feed'
@@ -119,7 +119,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// update & broadcast
setNumUnread('')
broadcast.postMessage({event: ''})
resetBadgeCount()
decrementBadgeCount('reset')
},
async checkUnread({
@@ -28,7 +28,6 @@ describe('session', () => {
const agent = new BskyAgent({service: 'https://alice.com'})
agent.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -51,7 +50,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -90,7 +88,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -119,7 +116,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -142,7 +138,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -167,7 +162,6 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@@ -192,7 +186,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@@ -206,7 +199,6 @@ describe('session', () => {
},
{
"accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -231,7 +223,6 @@ describe('session', () => {
const agent3 = new BskyAgent({service: 'https://alice.com'})
agent3.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2',
@@ -256,7 +247,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -270,7 +260,6 @@ describe('session', () => {
},
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@@ -295,7 +284,6 @@ describe('session', () => {
const agent4 = new BskyAgent({service: 'https://jay.com'})
agent4.session = {
active: true,
did: 'jay-did',
handle: 'jay.test',
accessJwt: 'jay-access-jwt-1',
@@ -318,7 +306,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "jay-access-jwt-1",
"active": true,
"did": "jay-did",
"email": undefined,
"emailAuthFactor": false,
@@ -332,7 +319,6 @@ describe('session', () => {
},
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -346,7 +332,6 @@ describe('session', () => {
},
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@@ -389,7 +374,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "jay-did",
"email": undefined,
"emailAuthFactor": false,
@@ -403,7 +387,6 @@ describe('session', () => {
},
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -417,7 +400,6 @@ describe('session', () => {
},
{
"accessJwt": undefined,
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@@ -446,7 +428,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -478,7 +459,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -503,7 +483,6 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://alice.com'})
agent2.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-2',
@@ -525,7 +504,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -554,7 +532,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -599,7 +576,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -607,7 +583,6 @@ describe('session', () => {
}
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@@ -641,7 +616,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@@ -679,7 +653,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -696,7 +669,6 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('alice-did')
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2',
@@ -725,7 +697,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": "alice@foo.bar",
"emailAuthFactor": false,
@@ -749,7 +720,6 @@ describe('session', () => {
`)
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-3',
@@ -778,7 +748,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-3",
"active": true,
"did": "alice-did",
"email": "alice@foo.baz",
"emailAuthFactor": true,
@@ -802,7 +771,6 @@ describe('session', () => {
`)
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-4',
@@ -831,7 +799,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-4",
"active": true,
"did": "alice-did",
"email": "alice@foo.baz",
"emailAuthFactor": false,
@@ -860,7 +827,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -877,7 +843,6 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('alice-did')
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2',
@@ -908,7 +873,6 @@ describe('session', () => {
expect(lastState === state).toBe(true)
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-3',
@@ -932,7 +896,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -941,7 +904,6 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@@ -966,7 +928,6 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('bob-did')
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice-updated.test',
accessJwt: 'alice-access-jwt-2',
@@ -995,7 +956,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "bob-access-jwt-1",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@@ -1009,7 +969,6 @@ describe('session', () => {
},
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": "alice@foo.bar",
"emailAuthFactor": false,
@@ -1033,7 +992,6 @@ describe('session', () => {
`)
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob-updated.test',
accessJwt: 'bob-access-jwt-2',
@@ -1060,7 +1018,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "bob-access-jwt-2",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@@ -1074,7 +1031,6 @@ describe('session', () => {
},
{
"accessJwt": "alice-access-jwt-2",
"active": true,
"did": "alice-did",
"email": "alice@foo.bar",
"emailAuthFactor": false,
@@ -1127,7 +1083,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -1136,7 +1091,6 @@ describe('session', () => {
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@@ -1163,7 +1117,6 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe('bob-did')
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-2',
@@ -1189,7 +1142,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -1227,7 +1179,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -1256,7 +1207,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -1292,7 +1242,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -1321,7 +1270,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -1357,7 +1305,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
@@ -1386,7 +1333,6 @@ describe('session', () => {
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
@@ -1394,7 +1340,6 @@ describe('session', () => {
}
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
@@ -1417,7 +1362,6 @@ describe('session', () => {
const anotherTabAgent1 = new BskyAgent({service: 'https://jay.com'})
anotherTabAgent1.session = {
active: true,
did: 'jay-did',
handle: 'jay.test',
accessJwt: 'jay-access-jwt-1',
@@ -1425,7 +1369,6 @@ describe('session', () => {
}
const anotherTabAgent2 = new BskyAgent({service: 'https://alice.com'})
anotherTabAgent2.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-2',
@@ -1454,7 +1397,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "jay-access-jwt-1",
"active": true,
"did": "jay-did",
"email": undefined,
"emailAuthFactor": false,
@@ -1468,7 +1410,6 @@ describe('session', () => {
},
{
"accessJwt": "bob-access-jwt-2",
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
@@ -1493,7 +1434,6 @@ describe('session', () => {
const anotherTabAgent3 = new BskyAgent({service: 'https://clarence.com'})
anotherTabAgent3.session = {
active: true,
did: 'clarence-did',
handle: 'clarence.test',
accessJwt: 'clarence-access-jwt-2',
@@ -1517,7 +1457,6 @@ describe('session', () => {
"accounts": [
{
"accessJwt": "clarence-access-jwt-2",
"active": true,
"did": "clarence-did",
"email": undefined,
"emailAuthFactor": false,
+2 -7
View File
@@ -46,11 +46,6 @@ export async function createAgentAndResume(
emailConfirmed: storedAccount.emailConfirmed,
handle: storedAccount.handle,
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)) {
await networkRetry(1, () => agent.resumeSession(prevSession))
@@ -240,8 +235,8 @@ export function agentToSessionAccount(
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
signupQueued: isSignupQueued(agent.session.accessJwt),
active: agent.session.active,
status: agent.session.status as SessionAccount['status'],
// @ts-expect-error TODO remove when backend is ready
status: agent.session.status,
pdsUrl: agent.pdsUrl?.toString(),
}
}
+5 -6
View File
@@ -1,9 +1,9 @@
import React from 'react'
import {
cancelAnimation,
Easing,
SharedValue,
useSharedValue,
withSpring,
withTiming,
} from 'react-native-reanimated'
type StateContext = SharedValue<number>
@@ -22,10 +22,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const setMode = React.useCallback(
(v: boolean) => {
'worklet'
// Cancel any existing animation
cancelAnimation(mode)
mode.value = withSpring(v ? 1 : 0, {
overshootClamping: true,
mode.value = withTiming(v ? 1 : 0, {
duration: 400,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
})
},
[mode],
+4 -52
View File
@@ -17,7 +17,6 @@ import {
import {
KeyboardAvoidingView,
KeyboardStickyView,
useKeyboardContext,
} from 'react-native-keyboard-controller'
import Animated, {
interpolateColor,
@@ -108,9 +107,7 @@ export const ComposePost = observer(function ComposePost({
text: initText,
imageUris: initImageUris,
cancelRef,
isModalReady,
}: Props & {
isModalReady: boolean
cancelRef?: React.RefObject<CancelRef>
}) {
const {currentAccount} = useSession()
@@ -130,17 +127,6 @@ export const ComposePost = observer(function ComposePost({
const {closeAllDialogs} = useDialogStateControlContext()
const t = useTheme()
// Disable this in the composer to prevent any extra keyboard height being applied.
// See https://github.com/bluesky-social/social-app/pull/4399
const {setEnabled} = useKeyboardContext()
React.useEffect(() => {
if (!isAndroid) return
setEnabled(false)
return () => {
setEnabled(true)
}
}, [setEnabled])
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
const [isProcessing, setIsProcessing] = useState(false)
const [processingState, setProcessingState] = useState('')
@@ -168,7 +154,6 @@ export const ComposePost = observer(function ComposePost({
const [extGif, setExtGif] = useState<Gif>()
const [labels, setLabels] = useState<string[]>([])
const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
const gallery = useMemo(
() => new GalleryModel(initImageUris),
[initImageUris],
@@ -189,7 +174,9 @@ export const ComposePost = observer(function ComposePost({
const onPressCancel = useCallback(() => {
if (graphemeLength > 0 || !gallery.isEmpty || extGif) {
closeAllDialogs()
Keyboard.dismiss()
if (Keyboard) {
Keyboard.dismiss()
}
discardPromptControl.open()
} else {
onClose()
@@ -402,37 +389,6 @@ export const ComposePost = observer(function ComposePost({
bottomBarAnimatedStyle,
} = useAnimatedBorders()
// Backup focus on android, if the keyboard *still* refuses to show
useEffect(() => {
if (!isAndroid) return
if (!isModalReady) return
function tryFocus() {
if (!Keyboard.isVisible()) {
textInput.current?.blur()
textInput.current?.focus()
}
}
tryFocus()
// Retry with enough gap to avoid interrupting the previous attempt.
// Unfortunately we don't know which attempt will succeed.
const retryInterval = setInterval(tryFocus, 500)
function stopTrying() {
clearInterval(retryInterval)
}
// Deactivate this fallback as soon as anything happens.
const sub1 = Keyboard.addListener('keyboardDidShow', stopTrying)
const sub2 = Keyboard.addListener('keyboardDidHide', stopTrying)
return () => {
clearInterval(retryInterval)
sub1.remove()
sub2.remove()
}
}, [isModalReady])
return (
<>
<KeyboardAvoidingView
@@ -561,11 +517,7 @@ export const ComposePost = observer(function ComposePost({
ref={textInput}
richtext={richtext}
placeholder={selectTextInputPlaceholder}
// fixes autofocus on android
key={
isAndroid ? (isModalReady ? 'ready' : 'animating') : 'static'
}
autoFocus={isAndroid ? isModalReady : true}
autoFocus={true}
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish}
@@ -1,36 +1,30 @@
import React, {useState} from 'react'
import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native'
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import {Image} from 'expo-image'
import Animated, {
runOnJS,
useAnimatedReaction,
useAnimatedRef,
useAnimatedStyle,
useAnimatedReaction,
useSharedValue,
withDecay,
withSpring,
} from 'react-native-reanimated'
import {Image} from 'expo-image'
import type {Dimensions as ImageDimensions, ImageSource} from '../../@types'
import {GestureDetector, Gesture} from 'react-native-gesture-handler'
import useImageDimensions from '../../hooks/useImageDimensions'
import {
applyRounding,
createTransform,
readTransform,
applyRounding,
prependPan,
prependPinch,
prependTransform,
readTransform,
TransformMatrix,
} from '../../transforms'
import type {ImageSource, Dimensions as ImageDimensions} from '../../@types'
const windowDim = Dimensions.get('window')
const screenDim = Dimensions.get('screen')
const statusBarHeight = windowDim.height - screenDim.height
const SCREEN = {
width: windowDim.width,
height: windowDim.height + statusBarHeight,
}
const SCREEN = Dimensions.get('window')
const MIN_DOUBLE_TAP_SCALE = 2
const MAX_ORIGINAL_IMAGE_ZOOM = 2
+1 -10
View File
@@ -25,7 +25,6 @@ import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {CenteredView} from '#/view/com/util/Views'
import {FeedItem} from './FeedItem'
import hairlineWidth = StyleSheet.hairlineWidth
import {isWeb} from '#/platform/detection'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
@@ -183,15 +182,7 @@ export function Feed({
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={
/*
NOTE:
web's intersection observer struggles with the 2x threshold
and leads to missed pagination, so we keep it <1
-prf
*/
isWeb ? 0.6 : 2
}
onEndReachedThreshold={2}
onScrolledDownChange={onScrolledDownChange}
contentContainerStyle={s.contentContainer}
// @ts-ignore our .web version only -prf
+8 -7
View File
@@ -1,12 +1,11 @@
import * as React from 'react'
import {FlatList, ScrollView, StyleSheet, View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {TabBar} from './TabBar'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {ListMethods} from '../util/List'
import {TabBar} from './TabBar'
export interface PagerWithHeaderChildParams {
headerHeight: number
@@ -185,7 +184,8 @@ function PagerItem({
const styles = StyleSheet.create({
headerContainerDesktop: {
marginHorizontal: 'auto',
marginLeft: 'auto',
marginRight: 'auto',
width: 600,
borderLeftWidth: 1,
borderRightWidth: 1,
@@ -198,14 +198,15 @@ const styles = StyleSheet.create({
zIndex: 1,
},
tabBarContainerDesktop: {
marginHorizontal: 'auto',
paddingHorizontal: 2,
marginLeft: 'auto',
marginRight: 'auto',
width: 600,
borderLeftWidth: 1,
borderRightWidth: 1,
},
tabBarContainerMobile: {
paddingHorizontal: 0,
paddingLeft: 14,
paddingRight: 14,
},
loadingHeader: {
borderColor: 'transparent',
+1 -1
View File
@@ -191,7 +191,7 @@ const mobileStyles = StyleSheet.create({
},
contentContainer: {
backgroundColor: 'transparent',
paddingHorizontal: 6,
paddingHorizontal: 8,
},
item: {
paddingTop: 10,
+1 -25
View File
@@ -16,7 +16,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {isWeb} from '#/platform/detection'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {useComposerControls} from '#/state/shell/composer'
@@ -197,7 +196,6 @@ let FeedItemInner = ({
},
]
const [hover, setHover] = useState(false)
return (
<Link
testID={`feedItem-by-${post.author.handle}`}
@@ -206,29 +204,7 @@ let FeedItemInner = ({
noFeedback
accessible={false}
onBeforePress={onBeforePress}
dataSet={{feedContext}}
onPointerEnter={() => {
setHover(true)
}}
onPointerLeave={() => {
setHover(false)
}}>
{isWeb && (
<View
style={{
position: 'absolute',
backgroundColor: 'white', // TODO
opacity: hover ? 0.01 : 0, // TODO
left: 0,
right: 0,
bottom: 0,
top: 0,
pointerEvents: 'none',
// @ts-ignore web-only
transition: '0.05s ease-in-out opacity',
}}
/>
)}
dataSet={{feedContext}}>
<View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}>
<View style={{width: 52}}>
{isThreadChild && (
-1
View File
@@ -49,7 +49,6 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
anchorNoUnderline?: boolean
navigationAction?: 'push' | 'replace' | 'navigate'
onPointerEnter?: () => void
onPointerLeave?: () => void
onBeforePress?: () => void
}
-1
View File
@@ -25,7 +25,6 @@ export type ListProps<ItemT> = Omit<
onRefresh?: () => void
onItemSeen?: (item: ItemT) => void
containWeb?: boolean
desktopFixedHeight?: number | boolean
sideBorders?: boolean
// Web only prop to disable a perf optimization (which would otherwise be on).
disableContentVisibility?: boolean
+1 -1
View File
@@ -22,7 +22,7 @@ export type ListProps<ItemT> = Omit<
refreshing?: boolean
onRefresh?: () => void
onItemSeen?: (item: ItemT) => void
desktopFixedHeight?: number | boolean
desktopFixedHeight: any // TODO: Better types.
containWeb?: boolean
sideBorders?: boolean
disableContentVisibility?: boolean
+1 -7
View File
@@ -1,10 +1,6 @@
import React, {useCallback, useEffect} from 'react'
import {NativeScrollEvent} from 'react-native'
import {
cancelAnimation,
interpolate,
useSharedValue,
} from 'react-native-reanimated'
import {interpolate, useSharedValue} from 'react-native-reanimated'
import EventEmitter from 'eventemitter3'
import {ScrollProvider} from '#/lib/ScrollContext'
@@ -121,8 +117,6 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
const newValue = clamp(startMode.value + dProgress, 0, 1)
if (newValue !== mode.value) {
// Manually adjust the value. This won't be (and shouldn't be) animated.
// Cancel any any existing animation
cancelAnimation(mode)
mode.value = newValue
}
} else {
@@ -108,7 +108,6 @@ let RepostButton = ({
</Text>
</Button>
<Button
testID="quoteBtn"
style={[a.justify_start, a.px_md]}
label={_(msg`Quote post`)}
onPress={() => {
+2 -4
View File
@@ -181,11 +181,9 @@ export function QuoteEmbed({
}, [queryClient, quote.author, onOpen])
return (
<ContentHider
modui={moderation?.ui('contentList')}
style={[styles.container, pal.borderDark, style]}
childContainerStyle={[a.pt_sm]}>
<ContentHider modui={moderation?.ui('contentList')}>
<Link
style={[styles.container, pal.borderDark, style]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
href={itemHref}
title={itemTitle}
+1 -11
View File
@@ -1,4 +1,4 @@
import React, {useLayoutEffect, useState} from 'react'
import React, {useLayoutEffect} from 'react'
import {Modal, View} from 'react-native'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {RootSiblingParent} from 'react-native-root-siblings'
@@ -24,16 +24,8 @@ export const Composer = observer(function ComposerImpl({}: {
const t = useTheme()
const state = useComposerState()
const ref = useComposerCancelRef()
const [isModalReady, setIsModalReady] = useState(false)
const open = !!state
const [prevOpen, setPrevOpen] = useState(open)
if (open !== prevOpen) {
setPrevOpen(open)
if (!open) {
setIsModalReady(false)
}
}
return (
<Modal
@@ -42,12 +34,10 @@ export const Composer = observer(function ComposerImpl({}: {
visible={open}
presentationStyle="formSheet"
animationType="slide"
onShow={() => setIsModalReady(true)}
onRequestClose={() => ref.current?.onPressCancel()}>
<View style={[t.atoms.bg, a.flex_1]}>
<Providers open={open}>
<ComposePost
isModalReady={isModalReady}
cancelRef={ref}
replyTo={state?.replyTo}
onPost={state?.onPost}
-1
View File
@@ -56,7 +56,6 @@ export function Composer({}: {winHeight: number}) {
t.atoms.border_contrast_medium,
]}>
<ComposePost
isModalReady={true}
replyTo={state.replyTo}
quote={state.quote}
onPost={state.onPost}
+8 -35
View File
@@ -16,9 +16,6 @@ import {s} from '#/lib/styles'
import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {useGate} from 'lib/statsig/statsig'
import {useUnreadMessageCount} from 'state/queries/messages/list-converations'
import {useUnreadNotifications} from 'state/queries/notifications/unread'
import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import {Logo} from '#/view/icons/Logo'
@@ -49,15 +46,11 @@ export function BottomBarWeb() {
const {hasSession, currentAccount} = useSession()
const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets()
const gate = useGate()
const {footerMinimalShellTransform} = useMinimalShellMode()
const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const iconWidth = 26
const unreadMessageCount = useUnreadMessageCount()
const notificationCountStr = useUnreadNotifications()
const showSignIn = React.useCallback(() => {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'none'})
@@ -110,20 +103,10 @@ export function BottomBarWeb() {
{({isActive}) => {
const Icon = isActive ? MessageFilled : Message
return (
<>
<Icon
width={iconWidth - 1}
style={[styles.ctrlIcon, pal.text, styles.messagesIcon]}
/>
{unreadMessageCount.count > 0 &&
gate('show_notification_badge_mobile_web') && (
<View style={styles.notificationCount}>
<Text style={styles.notificationCountLabel}>
{unreadMessageCount.numUnread}
</Text>
</View>
)}
</>
<Icon
width={iconWidth - 1}
style={[styles.ctrlIcon, pal.text, styles.messagesIcon]}
/>
)
}}
</NavItem>
@@ -131,20 +114,10 @@ export function BottomBarWeb() {
{({isActive}) => {
const Icon = isActive ? BellFilled : Bell
return (
<>
<Icon
width={iconWidth}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
{notificationCountStr !== '' &&
gate('show_notification_badge_mobile_web') && (
<View style={styles.notificationCount}>
<Text style={styles.notificationCountLabel}>
{notificationCountStr}
</Text>
</View>
)}
</>
<Icon
width={iconWidth}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
)
}}
</NavItem>
+106 -12
View File
@@ -34,10 +34,10 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
"@atproto/api@^0.12.16":
version "0.12.16"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.16.tgz#f5b5e06d75d379dafe79521d727ed8ad5516d3fc"
integrity sha512-v3lA/m17nkawDXiqgwXyaUSzJPeXJBMH8QKOoYxcDqN+8yG9LFlGe2ecGarXcbGQjYT0GJTAAW3Y/AaCOEwuLg==
"@atproto/api@^0.12.14":
version "0.12.14"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.14.tgz#81252fd166ec8fe950056531e690d563437720fa"
integrity sha512-ZPh/afoRjFEQDQgMZW2FQiG5CDUifY7SxBqI0zVJUwed8Zi6fqYzGYM8fcDvD8yJfflRCqRxUE72g5fKiA1zAQ==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"
@@ -3425,6 +3425,32 @@
xcode "^3.0.1"
xml2js "0.6.0"
"@expo/config-plugins@~5.0.3":
version "5.0.4"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-5.0.4.tgz#216fea6558fe66615af1370de55193f4181cb23e"
integrity sha512-vzUcVpqOMs3h+hyRdhGwk+eGIOhXa5xYdd92yO17RMNHav3v/+ekMbs7XA2c3lepMO8Yd4/5hqmRw9ZTL6jGzg==
dependencies:
"@expo/config-types" "^47.0.0"
"@expo/json-file" "8.2.36"
"@expo/plist" "0.0.18"
"@expo/sdk-runtime-versions" "^1.0.0"
"@react-native/normalize-color" "^2.0.0"
chalk "^4.1.2"
debug "^4.3.1"
find-up "~5.0.0"
getenv "^1.0.0"
glob "7.1.6"
resolve-from "^5.0.0"
semver "^7.3.5"
slash "^3.0.0"
xcode "^3.0.1"
xml2js "0.4.23"
"@expo/config-types@^47.0.0":
version "47.0.0"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-47.0.0.tgz#99eeabe0bba7a776e0f252b78beb0c574692c38d"
integrity sha512-r0pWfuhkv7KIcXMUiNACJmJKKwlTBGMw9VZHNdppS8/0Nve8HZMTkNRFQzTHW1uH3pBj8jEXpyw/2vSWDHex9g==
"@expo/config-types@^50.0.0-alpha.1":
version "50.0.0"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-50.0.0.tgz#b534d3ec997ec60f8af24f6ad56244c8afc71a0b"
@@ -3452,6 +3478,23 @@
slugify "^1.3.4"
sucrase "3.34.0"
"@expo/config@~7.0.0":
version "7.0.3"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-7.0.3.tgz#c9c634e76186de25e296485e51418f1e52966e6e"
integrity sha512-joVtB5o+NF40Tmsdp65UzryRtbnCuMbXkVO4wJnNJO4aaK0EYLdHCYSewORVqNcDfGN0LphQr8VTG2npbd9CJA==
dependencies:
"@babel/code-frame" "~7.10.4"
"@expo/config-plugins" "~5.0.3"
"@expo/config-types" "^47.0.0"
"@expo/json-file" "8.2.36"
getenv "^1.0.0"
glob "7.1.6"
require-from-string "^2.0.2"
resolve-from "^5.0.0"
semver "7.3.2"
slugify "^1.3.4"
sucrase "^3.20.0"
"@expo/config@~8.5.0":
version "8.5.0"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-8.5.0.tgz#c618e016c3272335e33fec02fb7fd67e4dcc3342"
@@ -3598,6 +3641,15 @@
semver "^7.6.0"
tempy "0.3.0"
"@expo/json-file@8.2.36":
version "8.2.36"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.36.tgz#62a505cb7f30a34d097386476794680a3f7385ff"
integrity sha512-tOZfTiIFA5KmMpdW9KF7bc6CFiGjb0xnbieJhTGlHrLL+ps2G0OkqmuZ3pFEXBOMnJYUVpnSy++52LFxvpa5ZQ==
dependencies:
"@babel/code-frame" "~7.10.4"
json5 "^1.0.1"
write-file-atomic "^2.3.0"
"@expo/json-file@^8.2.37":
version "8.2.37"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.37.tgz#9c02d3b42134907c69cc0a027b18671b69344049"
@@ -3699,6 +3751,15 @@
split "^1.0.1"
sudo-prompt "9.1.1"
"@expo/plist@0.0.18":
version "0.0.18"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.18.tgz#9abcde78df703a88f6d9fa1a557ee2f045d178b0"
integrity sha512-+48gRqUiz65R21CZ/IXa7RNBXgAI/uPSdvJqoN9x1hfL44DNbUoWHgHiEXTx7XelcATpDwNTz6sHLfy0iNqf+w==
dependencies:
"@xmldom/xmldom" "~0.7.0"
base64-js "^1.2.3"
xmlbuilder "^14.0.0"
"@expo/plist@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.1.0.tgz#eabc95f951d14e10c87fd0443ee01d567371f058"
@@ -8532,7 +8593,7 @@
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99"
integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==
"@xmldom/xmldom@~0.7.7":
"@xmldom/xmldom@~0.7.0", "@xmldom/xmldom@~0.7.7":
version "0.7.13"
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.13.tgz#ff34942667a4e19a9f4a0996a76814daac364cf3"
integrity sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==
@@ -12014,7 +12075,15 @@ expo-clipboard@^6.0.3:
resolved "https://registry.yarnpkg.com/expo-clipboard/-/expo-clipboard-6.0.3.tgz#dfea74d4a004dce59ecefd063d6fb9f1c356a03f"
integrity sha512-RIKDsuHkYfaspifbFpVC8sBVFKR05L7Pj7mU2/XkbrW9m01OBNvdpGraXEMsTFCx97xMGsZpEw9pPquL4j4xVg==
expo-constants@16.0.1, expo-constants@^13.0.2, expo-constants@~16.0.0:
expo-constants@^13.0.2:
version "13.2.4"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-13.2.4.tgz#eab4a553f074b2c60ad7a158d3b82e3484a94606"
integrity sha512-Zobau8EuTk2GgafwkfGnWM6CmSLB7X8qnQXVuXe0nd3v92hfQUmRWGhJwH88uxXj3LrfqctM6PaJ8taG1vxfBw==
dependencies:
"@expo/config" "~7.0.0"
uuid "^3.3.2"
expo-constants@~16.0.0, expo-constants@~16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-16.0.1.tgz#1285e29c85513c6e88e118289e2baab72596d3f7"
integrity sha512-s6aTHtglp926EsugWtxN7KnpSsE9FCEjb7CgEjQQ78Gpu4btj4wB+IXot2tlqNwqv+x7xFe5veoPGfJDGF/kVg==
@@ -12056,7 +12125,14 @@ expo-dev-menu@5.0.14:
expo-dev-menu-interface "1.8.3"
semver "^7.5.4"
expo-device@6.0.2, expo-device@~4.1.1, expo-device@~6.0.2:
expo-device@~4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-4.1.1.tgz#5de94144113ffb7fa0f37fa3d70e452113954c10"
integrity sha512-It0SGtKcvzQSf+Co6zdPdB63zZvG2/rDolB1lqswMNKj03Y7KVU41s5tcQCqNczj7tmeN3CJy7A8YhYGKdb7gA==
dependencies:
ua-parser-js "^0.7.19"
expo-device@~6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-6.0.2.tgz#9bc3eccd16509c2819c225cc2ca8f7c3e3bdd11e"
integrity sha512-sCt91CuTmAuMXX4SlFOn4lIos2UIr8vb0jDstDDZXys6kErcj0uynC7bQAMreU5uRUTKMAl4MAMpKt9ufCXPBw==
@@ -12180,10 +12256,10 @@ expo-navigation-bar@~3.0.4:
"@react-native/normalize-colors" "~0.74.83"
debug "^4.3.2"
expo-notifications@~0.28.7:
version "0.28.7"
resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.28.7.tgz#14826c10d126b1dd16d1604add2a4766aecf7591"
integrity sha512-P68/IlnxTtKjfHy22nC+o/H4VSgBVrmTQcx42JCevTbVQB8JTITmR42hvpZh8GUecB7jNOsuVokTTDZDE6aSyg==
expo-notifications@~0.28.3:
version "0.28.3"
resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.28.3.tgz#9076c2bd69c3de3338a2e2161c8bd5f18cb440cb"
integrity sha512-Xaj82eQUJzJXa8+giZr708ih86GGtkGS8N01epoiDkTKC8Z9783UJ8Pf8+PSFSfHsY3Sd8TJpQrD9n7QnGHwGQ==
dependencies:
"@expo/image-utils" "^0.5.0"
"@ide/backoff" "^1.0.0"
@@ -15481,7 +15557,7 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^1.0.2:
json5@^1.0.1, json5@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
@@ -21250,6 +21326,11 @@ typescript@^5.3.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
ua-parser-js@^0.7.19:
version "0.7.37"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832"
integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==
ua-parser-js@^0.7.33:
version "0.7.35"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307"
@@ -21543,6 +21624,11 @@ utrie@^1.0.2:
dependencies:
base64-arraybuffer "^1.0.2"
uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
@@ -22295,6 +22381,14 @@ xml-name-validator@^4.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
xml2js@0.4.23:
version "0.4.23"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
dependencies:
sax ">=0.6.0"
xmlbuilder "~11.0.0"
xml2js@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282"