Compare commits

...

20 Commits

Author SHA1 Message Date
Eric Bailey bd8dac0154 bump picker deps 2023-09-14 15:17:57 -05:00
Ansh b9268d503a wrap impersonation text (#1447) 2023-09-13 11:12:58 -07:00
Ansh 9c72fcc9b4 share mute list link with did not handle (#1438) 2023-09-12 15:35:34 -07:00
Ansh 4977a5d2a3 Fix 651 Make followers and following button into a <Link> (#1429)
* make followers and following button into a Link

* make buttons into `<a>` tags
2023-09-12 13:47:55 -07:00
Paul Frazee 2b6fe4cb50 1.50 2023-09-11 18:23:40 -07:00
Paul Frazee 9b5bd53f84 Remove accessibilityRole=button on dropdown mask, it causes issues on firefox (#1436) 2023-09-11 18:09:02 -07:00
Paul Frazee 9e8b14f710 Add birth date gating to moderation settings (#1435)
* Add birth date preference, modal to set, link in settings, and age gate in moderation

* Styling fixes for android

* Fix types
2023-09-11 18:04:09 -07:00
Paul Frazee 0090371011 Paginate the PostThread to avoid rendering too many posts and crashing the app on large threads (#1432) 2023-09-11 17:41:00 -07:00
Eric Bailey 21371081c6 fix theme flashing (#1417)
* fix theme flashing

* remove memo

* use AppState listener
2023-09-11 14:39:31 -07:00
Paul Frazee f8c611118e Separate list deletion into chunks (close #1430) (#1431) 2023-09-11 14:28:30 -07:00
Paul Frazee 977f9228f8 Revert "Fix 1338 mark notifications read with timeout and on app state change (#1340)" (#1433)
This reverts commit d2e65d2a7f.
2023-09-11 14:28:13 -07:00
Paul Frazee 57c10cbcab Bump @atproto/api@0.6.12 (#1434) 2023-09-11 14:28:02 -07:00
Ansh 27cb1bd276 make sure click events on the emoji picker don't bubble up to the mask (#1427) 2023-09-11 13:31:14 -07:00
Ansh bcd63e710c Lytic analytics migration (#1428)
* add new api event proxy

* add new apiHost for analytics web
2023-09-11 11:41:27 -07:00
Ansh 01775e5bff add new api event proxy (#1400) 2023-09-11 22:36:49 +05:30
uakci 731d84faaa composer: inhibit Cmd-Enter from inputting newline (#1421) 2023-09-11 09:02:14 -07:00
Bossett 775aa87540 Update time.ts to handle very small or negative time differences (#1390)
* Update time.ts to handle very small or negative time differences

Right now, posts can appear to be from the future with a negative time difference (i.e. -3s appears). This change defines 'NOW' as less than 5 seconds old, and returns 'now' in that case.

It's not clear how localisation is handled - this may need translation.

* Add test for 'now' in time/ago(...)

Add tests for ago() for right now (i.e. 'now') and 10s ago to ensure the seconds case is still tested
2023-09-08 08:57:22 -07:00
dan 6d73ed96e1 Make Sentry optional (#1416)
* Move app.json -> app.config.js

* Make Sentry optional
2023-09-08 08:47:01 -07:00
dan adc5b75b2b Fix autocomplete avatar alignment (#1414) 2023-09-08 16:30:42 +01:00
dan 44a00e1b7e Set crop mode to None by default (#1415)
Fixes #1263

Previously, opening the crop tool had 1:1 selected by default. So if you opened it and pressed Done without changing anything, it would format the image as 1:1.

After this change, None is selected by default. So if you open the crop tool and press Done without changing anything, nothing will change.
2023-09-08 16:30:00 +01:00
31 changed files with 601 additions and 296 deletions
+3 -1
View File
@@ -175,6 +175,7 @@ describe('ago', () => {
1671461038,
'04 Dec 1995 00:12:00 GMT',
new Date(),
new Date().setSeconds(new Date().getSeconds() - 10),
new Date().setMinutes(new Date().getMinutes() - 10),
new Date().setHours(new Date().getHours() - 1),
new Date().setDate(new Date().getDate() - 1),
@@ -183,7 +184,8 @@ describe('ago', () => {
const outputs = [
new Date(1671461038).toLocaleDateString(),
new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(),
'0s',
'now',
'10s',
'10m',
'1h',
'1d',
+114
View File
@@ -0,0 +1,114 @@
module.exports = function () {
const hasSentryToken = !!process.env.SENTRY_AUTH_TOKEN
return {
expo: {
name: 'Bluesky',
slug: 'bluesky',
scheme: 'bluesky',
owner: 'blueskysocial',
version: '1.50.0',
runtimeVersion: {
policy: 'appVersion',
},
orientation: 'portrait',
icon: './assets/icon.png',
userInterfaceStyle: 'automatic',
splash: {
image: './assets/cloud-splash.png',
resizeMode: 'cover',
backgroundColor: '#ffffff',
},
ios: {
buildNumber: '1',
supportsTablet: false,
bundleIdentifier: 'xyz.blueskyweb.app',
config: {
usesNonExemptEncryption: false,
},
infoPlist: {
UIBackgroundModes: ['remote-notification'],
NSCameraUsageDescription:
'Used for profile pictures, posts, and other kinds of content.',
NSMicrophoneUsageDescription:
'Used for posts and other kinds of content.',
NSPhotoLibraryAddUsageDescription:
'Used to save images to your library.',
NSPhotoLibraryUsageDescription:
'Used for profile pictures, posts, and other kinds of content',
},
associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'],
},
androidStatusBar: {
barStyle: 'dark-content',
backgroundColor: '#ffffff',
},
android: {
versionCode: 37,
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff',
},
googleServicesFile: './google-services.json',
package: 'xyz.blueskyweb.app',
intentFilters: [
{
action: 'VIEW',
autoVerify: true,
data: [
{
scheme: 'https',
host: 'bsky.app',
},
],
category: ['BROWSABLE', 'DEFAULT'],
},
],
},
web: {
favicon: './assets/favicon.png',
},
updates: {
enabled: true,
fallbackToCacheTimeout: 1000,
url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
},
plugins: [
'expo-localization',
hasSentryToken && 'sentry-expo',
[
'expo-build-properties',
{
android: {
compileSdkVersion: 34,
targetSdkVersion: 34,
buildToolsVersion: '34.0.0',
kotlinVersion: '1.8.0',
},
},
],
[
'expo-updates',
{
username: 'blueskysocial',
},
],
].filter(Boolean),
extra: {
eas: {
projectId: '55bd077a-d905-4184-9c7f-94789ba0f302',
},
},
hooks: {
postPublish: [
{
file: 'sentry-expo/upload-sourcemaps',
config: {
organization: 'blueskyweb',
project: 'react-native',
},
},
],
},
},
}
}
-109
View File
@@ -1,109 +0,0 @@
{
"expo": {
"name": "Bluesky",
"slug": "bluesky",
"scheme": "bluesky",
"owner": "blueskysocial",
"version": "1.49.0",
"runtimeVersion": {
"policy": "appVersion"
},
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/cloud-splash.png",
"resizeMode": "cover",
"backgroundColor": "#ffffff"
},
"ios": {
"buildNumber": "1",
"supportsTablet": false,
"bundleIdentifier": "xyz.blueskyweb.app",
"config": {
"usesNonExemptEncryption": false
},
"infoPlist": {
"UIBackgroundModes": [
"remote-notification"
],
"NSCameraUsageDescription": "Used for profile pictures, posts, and other kinds of content.",
"NSMicrophoneUsageDescription": "Used for posts and other kinds of content.",
"NSPhotoLibraryAddUsageDescription": "Used to save images to your library.",
"NSPhotoLibraryUsageDescription": "Used for profile pictures, posts, and other kinds of content"
},
"associatedDomains": ["applinks:bsky.app", "applinks:staging.bsky.app"]
},
"androidStatusBar": {
"barStyle": "dark-content",
"backgroundColor": "#ffffff"
},
"android": {
"versionCode": 36,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"googleServicesFile": "./google-services.json",
"package": "xyz.blueskyweb.app",
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "bsky.app"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
},
"web": {
"favicon": "./assets/favicon.png"
},
"updates": {
"enabled": true,
"fallbackToCacheTimeout": 1000,
"url": "https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302"
},
"plugins": [
"expo-localization",
"sentry-expo",
[
"expo-build-properties",
{
"android": {
"compileSdkVersion": 34,
"targetSdkVersion": 34,
"buildToolsVersion": "34.0.0",
"kotlinVersion": "1.8.0"
}
}
],
[
"expo-updates",
{
"username": "blueskysocial"
}
]
],
"extra": {
"eas": {
"projectId": "55bd077a-d905-4184-9c7f-94789ba0f302"
}
},
"hooks": {
"postPublish": [
{
"file": "sentry-expo/upload-sourcemaps",
"config": {
"organization": "blueskyweb",
"project": "react-native"
}
}
]
}
}
}
+8 -8
View File
@@ -26,20 +26,20 @@
- Run once: `yarn e2e:build`
- Each test run: `yarn e2e:run`
- Tips
- Make sure you copy the `.env.example` to `.env` and add the appropriate tokens (e.g. `SENTRY_AUTH_TOKEN` can be created on the Sentry dashboard using [these instructions](https://docs.expo.dev/guides/using-sentry/#sign-up-for-a-sentry-account-and-create-a-project)). If this is not required, you can remove it from `eas.json` and `package.json`, as well as any mentions in the code. Please check the section below on how to remove Sentry from the codebase
- Make sure to copy the `.env.example` to `.env` and fill in any necessary tokens. (The Sentry token is NOT required; see instructions below if you want to enable Sentry.)
- If you want to use Expo EAS on your own builds without ejecting from Expo, make sure to change the `owner` as well as `extra.eas.projectId` properties. If you do not have an Expo account, you may remove these properties.
- `npx react-native info` Checks what has been installed.
- The android simulator won't be able to access localhost services unless you run `adb reverse tcp:{PORT} tcp:{PORT}`
- For instance, the locally-hosted dev-wallet will need `adb reverse tcp:3001 tcp:3001`
- For some reason, the typescript compiler chokes on platform-specific files (e.g. `foo.native.ts`) but only when compiling for Web thus far. Therefore we always have one version of the file which doesn't use a platform specifier, and that should be the Web version. ([More info](https://stackoverflow.com/questions/44001050/platform-specific-import-component-in-react-native-with-typescript).)
### Removing Sentry
If you are part of the Bluesky team, you should have access to our Sentry dashboard, and you shouldn't need to remove Sentry. Even if you are not part of the Bluesky team, you can create your own Sentry account and add the `SENTRY_AUTH_TOKEN` env var and add your sentry account detials to `app.json` to make the app build and run successfully. However, if that is not possible, follow these steps to remove Sentry from the project (please don't commit this code in any PR):
- `yarn remove sentry-expo @sentry/react-native`
- Remove `sentry-expo` plugin in `app.json` and also remove the `postPublish` hook in `app.json`
- Remove any mentions of `sentry` from the `App.native.tsx`, `App.web.tsx` and `Navigation.tsx` files. Also, delete `sentry.ts`
- Run `rm -rf ios android` or delete the existing `android` and `ios` folders in the project (don't worry! `yarn prebuild` gets these back)
- Run `yarn prebuild` and `yarn ios` and build the app!
### Adding Sentry
Adding Sentry is NOT required. You can keep `SENTRY_AUTH_TOKEN=` in `.env` which will build the app without Sentry.
However, if you're a part of the Bluesky team and want to enable Sentry, fill in `SENTRY_AUTH_TOKEN` in your `.env`. It can be created on the Sentry dashboard using [these instructions](https://docs.expo.dev/guides/using-sentry/#sign-up-for-a-sentry-account-and-create-a-project).
If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running `yarn ios` or `yarn android` again.
## Go-Server Build
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.49.0",
"version": "1.50.0",
"private": true,
"scripts": {
"prepare": "is-ci || husky install",
@@ -24,7 +24,7 @@
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
},
"dependencies": {
"@atproto/api": "^0.6.8",
"@atproto/api": "^0.6.12",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@emoji-mart/react": "^1.1.1",
@@ -79,8 +79,8 @@
"expo-dev-client": "2.4.7",
"expo-device": "~5.4.0",
"expo-image": "~1.3.2",
"expo-image-manipulator": "~11.3.0",
"expo-image-picker": "~14.3.2",
"expo-image-manipulator": "~11.5.0",
"expo-image-picker": "~14.5.0",
"expo-localization": "~14.3.0",
"expo-media-library": "~15.4.1",
"expo-notifications": "~0.20.1",
+28 -8
View File
@@ -1,5 +1,11 @@
import React, {ReactNode, createContext, useContext, useMemo} from 'react'
import {TextStyle, useColorScheme, ViewStyle} from 'react-native'
import React, {ReactNode, createContext, useContext} from 'react'
import {
AppState,
TextStyle,
useColorScheme,
ViewStyle,
ColorSchemeName,
} from 'react-native'
import {darkTheme, defaultTheme} from './themes'
export type ColorScheme = 'light' | 'dark'
@@ -91,14 +97,28 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
children,
}) => {
const colorSchemeFromRN = useColorScheme()
const [nativeColorScheme, setNativeColorScheme] =
React.useState<ColorSchemeName>(colorSchemeFromRN)
// if theme is 'system', use the device's configured color scheme
let colorScheme = theme === 'system' ? colorSchemeFromRN : theme
React.useEffect(() => {
const subscription = AppState.addEventListener('change', state => {
const isActive = state === 'active'
const value = useMemo(
() => (colorScheme === 'dark' ? darkTheme : defaultTheme),
[colorScheme],
)
if (!isActive) return
setNativeColorScheme(colorSchemeFromRN)
})
return () => subscription.remove()
}, [colorSchemeFromRN])
const value =
theme === 'system'
? nativeColorScheme === 'dark'
? darkTheme
: defaultTheme
: theme === 'dark'
? darkTheme
: defaultTheme
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
+1
View File
@@ -14,6 +14,7 @@ import {ScreenEvent, TrackEvent} from './types'
const segmentClient = createClient({
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
trackAppLifecycleEvents: false,
proxy: 'https://api.events.bsky.app/v1',
})
export const track = segmentClient?.track?.bind?.(segmentClient) as TrackEvent
+1 -1
View File
@@ -15,7 +15,7 @@ const segmentClient = createClient(
{
integrations: {
'Segment.io': {
apiHost: 'api.evt.bsky.app/v1',
apiHost: 'api.events.bsky.app/v1',
},
},
},
+6 -2
View File
@@ -56,8 +56,12 @@ interface TrackPropertiesMap {
}
// PROFILE HEADER events
'ProfileHeader:EditProfileButtonClicked': {}
'ProfileHeader:FollowersButtonClicked': {}
'ProfileHeader:FollowsButtonClicked': {}
'ProfileHeader:FollowersButtonClicked': {
handle: string
}
'ProfileHeader:FollowsButtonClicked': {
handle: string
}
'ProfileHeader:ShareButtonClicked': {}
'ProfileHeader:MuteAccountButtonClicked': {}
'ProfileHeader:UnmuteAccountButtonClicked': {}
+4 -1
View File
@@ -1,3 +1,4 @@
const NOW = 5
const MINUTE = 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
@@ -13,7 +14,9 @@ export function ago(date: number | string | Date): string {
ts = date
}
const diffSeconds = Math.floor((Date.now() - ts) / 1e3)
if (diffSeconds < MINUTE) {
if (diffSeconds < NOW) {
return `now`
} else if (diffSeconds < MINUTE) {
return `${diffSeconds}s`
} else if (diffSeconds < HOUR) {
return `${Math.floor(diffSeconds / MINUTE)}m`
+16 -6
View File
@@ -7,6 +7,7 @@ import {
AppBskyGraphListitem,
} from '@atproto/api'
import {Image as RNImage} from 'react-native-image-crop-picker'
import chunk from 'lodash.chunk'
import {RootStoreModel} from '../root-store'
import * as apilib from 'lib/api/index'
import {cleanError} from 'lib/strings/errors'
@@ -102,6 +103,10 @@ export class ListModel {
return this.list?.viewer?.muted
}
get creatorDid() {
return this.list?.creator.did
}
// public api
// =
@@ -211,12 +216,17 @@ export class ListModel {
rkey: urip.rkey,
}
}
await this.rootStore.agent.com.atproto.repo.applyWrites({
repo: this.rootStore.me.did,
writes: [createDel(this.uri)].concat(
records.map(record => createDel(record.uri)),
),
})
const writes = records
.map(record => createDel(record.uri))
.concat([createDel(this.uri)])
// apply in chunks
for (const writesChunk of chunk(writes, 10)) {
await this.rootStore.agent.com.atproto.repo.applyWrites({
repo: this.rootStore.me.did,
writes: writesChunk,
})
}
this.rootStore.emitListDeleted(this.uri)
}
+1 -1
View File
@@ -32,7 +32,7 @@ export class ImageModel implements Omit<RNImage, 'size'> {
// Web manipulation
prev?: RNImage
attributes: ImageManipulationAttributes = {
aspectRatio: '1:1',
aspectRatio: 'None',
scale: 1,
flipHorizontal: false,
flipVertical: false,
+1
View File
@@ -118,6 +118,7 @@ export class CreateAccountModel {
password: this.password,
inviteCode: this.inviteCode,
})
/* dont await */ this.rootStore.preferences.setBirthDate(this.birthDate)
track('Create Account')
} catch (e: any) {
this.rootStore.onboarding.skip() // undo starting the onboard
+15
View File
@@ -7,6 +7,7 @@ import {RootStoreModel} from '../root-store'
import {ModerationOpts} from '@atproto/api'
import {DEFAULT_FEEDS} from 'lib/constants'
import {deviceLocales} from 'platform/detection'
import {getAge} from 'lib/strings/time'
import {LANGUAGES} from '../../../locale/languages'
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
@@ -47,6 +48,7 @@ export class PreferencesModel {
contentLabels = new LabelPreferencesModel()
savedFeeds: string[] = []
pinnedFeeds: string[] = []
birthDate: Date | undefined = undefined
homeFeedRepliesEnabled: boolean = true
homeFeedRepliesThreshold: number = 2
homeFeedRepostsEnabled: boolean = true
@@ -60,6 +62,13 @@ export class PreferencesModel {
makeAutoObservable(this, {lock: false}, {autoBind: true})
}
get userAge(): number | undefined {
if (!this.birthDate) {
return undefined
}
return getAge(this.birthDate)
}
serialize() {
return {
contentLanguages: this.contentLanguages,
@@ -199,6 +208,7 @@ export class PreferencesModel {
) {
this.pinnedFeeds = prefs.feeds.pinned
}
this.birthDate = prefs.birthDate
})
// set defaults on missing items
@@ -430,6 +440,11 @@ export class PreferencesModel {
)
}
async setBirthDate(birthDate: Date) {
this.birthDate = birthDate
await this.rootStore.agent.setPersonalDetails({birthDate})
}
toggleHomeFeedRepliesEnabled() {
this.homeFeedRepliesEnabled = !this.homeFeedRepliesEnabled
}
+5
View File
@@ -136,6 +136,10 @@ export interface PostLanguagesSettingsModal {
name: 'post-languages-settings'
}
export interface BirthDateSettingsModal {
name: 'birth-date-settings'
}
export type Modal =
// Account
| AddAppPasswordModal
@@ -143,6 +147,7 @@ export type Modal =
| DeleteAccountModal
| EditProfileModal
| ProfilePreviewModal
| BirthDateSettingsModal
// Curation
| ContentFilteringSettingsModal
@@ -103,6 +103,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
handleKeyDown: (_, event) => {
if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') {
textInputWebEmitter.emit('publish')
return true
}
},
},
@@ -93,7 +93,7 @@ export const Autocomplete = observer(function AutocompleteImpl({
const styles = StyleSheet.create({
container: {
marginLeft: -54,
marginLeft: -50, // Composer avatar width
top: 10,
borderTopWidth: 1,
},
@@ -58,21 +58,27 @@ export function EmojiPicker({close}: {close: () => void}) {
// eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors
<TouchableWithoutFeedback onPress={close} accessibilityViewIsModal>
<View style={styles.mask}>
<View
style={[
styles.picker,
{
paddingTop: noPadding ? 0 : reducedPadding ? 150 : 325,
display: noPicker ? 'none' : 'flex',
},
]}>
<Picker
// @ts-ignore we set emojiMartData in `emoji-mart-data.js` file
data={window.emojiMartData}
onEmojiSelect={onInsert}
autoFocus={false}
/>
</View>
{/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */}
<TouchableWithoutFeedback
onPress={e => {
e.stopPropagation() // prevent event from bubbling up to the mask
}}>
<View
style={[
styles.picker,
{
paddingTop: noPadding ? 0 : reducedPadding ? 150 : 325,
display: noPicker ? 'none' : 'flex',
},
]}>
<Picker
// @ts-ignore we set emojiMartData in `emoji-mart-data.js` file
data={window.emojiMartData}
onEmojiSelect={onInsert}
autoFocus={false}
/>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
)
+132
View File
@@ -0,0 +1,132 @@
import React, {useState} from 'react'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {observer} from 'mobx-react-lite'
import {Text} from '../util/text/Text'
import {DateInput} from '../util/forms/DateInput'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
export const snapPoints = ['50%']
export const Component = observer(function Component({}: {}) {
const pal = usePalette('default')
const store = useStores()
const [date, setDate] = useState<Date>(
store.preferences.birthDate || new Date(),
)
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('')
const {isMobile} = useWebMediaQueries()
const onSave = async () => {
setError('')
setIsProcessing(true)
try {
await store.preferences.setBirthDate(date)
store.shell.closeModal()
} catch (e) {
setError(cleanError(String(e)))
} finally {
setIsProcessing(false)
}
}
return (
<View
testID="birthDateSettingsModal"
style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}>
<View style={styles.titleSection}>
<Text type="title-lg" style={[pal.text, styles.title]}>
My Birthday
</Text>
</View>
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
This information is not shared with other users.
</Text>
<View>
<DateInput
testID="birthdayInput"
value={date}
onChange={setDate}
buttonType="default-light"
buttonStyle={[pal.border, styles.dateInputButton]}
buttonLabelType="lg"
accessibilityLabel="Birthday"
accessibilityHint="Enter your birth date"
accessibilityLabelledBy="birthDate"
/>
</View>
{error ? (
<ErrorMessage message={error} style={styles.error} />
) : undefined}
<View style={[styles.btnContainer, pal.borderDark]}>
{isProcessing ? (
<View style={styles.btn}>
<ActivityIndicator color="#fff" />
</View>
) : (
<TouchableOpacity
testID="confirmBtn"
onPress={onSave}
style={styles.btn}
accessibilityRole="button"
accessibilityLabel="Save"
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>Save</Text>
</TouchableOpacity>
)}
</View>
</View>
)
})
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: isWeb ? 0 : 40,
},
titleSection: {
paddingTop: isWeb ? 0 : 4,
paddingBottom: isWeb ? 14 : 10,
},
title: {
textAlign: 'center',
fontWeight: '600',
marginBottom: 5,
},
error: {
borderRadius: 6,
marginTop: 10,
},
dateInputButton: {
borderWidth: 1,
borderRadius: 6,
paddingVertical: 14,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
padding: 14,
backgroundColor: colors.blue3,
},
btnContainer: {
paddingTop: 20,
paddingHorizontal: 20,
},
})
@@ -9,6 +9,7 @@ import {s, colors, gradients} from 'lib/styles'
import {Text} from '../util/text/Text'
import {TextLink} from '../util/Link'
import {ToggleButton} from '../util/forms/ToggleButton'
import {Button} from '../util/forms/Button'
import {usePalette} from 'lib/hooks/usePalette'
import {CONFIGURABLE_LABEL_GROUPS} from 'lib/labeling/const'
import {isIOS} from 'platform/detection'
@@ -27,22 +28,6 @@ export const Component = observer(
store.preferences.sync()
}, [store])
const onToggleAdultContent = React.useCallback(async () => {
if (isIOS) {
return
}
try {
await store.preferences.setAdultContentEnabled(
!store.preferences.adultContentEnabled,
)
} catch (e) {
Toast.show(
'There was an issue syncing your preferences with the server',
)
store.log.error('Failed to update preferences with server', {e})
}
}, [store])
const onPressDone = React.useCallback(() => {
store.shell.closeModal()
}, [store])
@@ -51,29 +36,7 @@ export const Component = observer(
<View testID="contentFilteringModal" style={[pal.view, styles.container]}>
<Text style={[pal.text, styles.title]}>Content Filtering</Text>
<ScrollView style={styles.scrollContainer}>
<View style={s.mb10}>
{isIOS ? (
store.preferences.adultContentEnabled ? null : (
<Text type="md" style={pal.textLight}>
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
text="bsky.app"
/>
.
</Text>
)
) : (
<ToggleButton
type="default-light"
label="Enable Adult Content"
isSelected={store.preferences.adultContentEnabled}
onPress={onToggleAdultContent}
style={styles.toggleBtn}
/>
)}
</View>
<AdultContentEnabledPref />
<ContentLabelPref
group="nsfw"
disabled={!store.preferences.adultContentEnabled}
@@ -121,6 +84,71 @@ export const Component = observer(
},
)
const AdultContentEnabledPref = observer(
function AdultContentEnabledPrefImpl() {
const store = useStores()
const pal = usePalette('default')
const onSetAge = () => store.shell.openModal({name: 'birth-date-settings'})
const onToggleAdultContent = async () => {
if (isIOS) {
return
}
try {
await store.preferences.setAdultContentEnabled(
!store.preferences.adultContentEnabled,
)
} catch (e) {
Toast.show(
'There was an issue syncing your preferences with the server',
)
store.log.error('Failed to update preferences with server', {e})
}
}
return (
<View style={s.mb10}>
{isIOS ? (
store.preferences.adultContentEnabled ? null : (
<Text type="md" style={pal.textLight}>
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
text="bsky.app"
/>
.
</Text>
)
) : typeof store.preferences.birthDate === 'undefined' ? (
<View style={[pal.viewLight, styles.agePrompt]}>
<Text type="md" style={[pal.text, {flex: 1}]}>
Confirm your age to enable adult content.
</Text>
<Button type="primary" label="Set Age" onPress={onSetAge} />
</View>
) : (store.preferences.userAge || 0) >= 18 ? (
<ToggleButton
type="default-light"
label="Enable Adult Content"
isSelected={store.preferences.adultContentEnabled}
onPress={onToggleAdultContent}
style={styles.toggleBtn}
/>
) : (
<View style={[pal.viewLight, styles.agePrompt]}>
<Text type="md" style={[pal.text, {flex: 1}]}>
You must be 18 or older to enable adult content.
</Text>
<Button type="primary" label="Set Age" onPress={onSetAge} />
</View>
)}
</View>
)
},
)
// TODO: Refactor this component to pass labels down to each tab
const ContentLabelPref = observer(function ContentLabelPrefImpl({
group,
@@ -277,6 +305,16 @@ const styles = StyleSheet.create({
borderTopWidth: 1,
},
agePrompt: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingLeft: 14,
paddingRight: 10,
paddingVertical: 8,
borderRadius: 8,
},
contentLabelPref: {
flexDirection: 'row',
justifyContent: 'space-between',
+4
View File
@@ -29,6 +29,7 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings'
const DEFAULT_SNAPPOINTS = ['90%']
@@ -132,6 +133,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'moderation-details') {
snapPoints = ModerationDetailsModal.snapPoints
element = <ModerationDetailsModal.Component {...activeModal} />
} else if (activeModal?.name === 'birth-date-settings') {
snapPoints = BirthDateSettingsModal.snapPoints
element = <BirthDateSettingsModal.Component />
} else {
return null
}
+3
View File
@@ -27,6 +27,7 @@ import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings'
export const ModalsContainer = observer(function ModalsContainer() {
const store = useStores()
@@ -107,6 +108,8 @@ function Modal({modal}: {modal: ModalIface}) {
element = <EditImageModal.Component {...modal} />
} else if (modal.name === 'moderation-details') {
element = <ModerationDetailsModal.Component {...modal} />
} else if (modal.name === 'birth-date-settings') {
element = <BirthDateSettingsModal.Component />
} else {
return null
}
+33 -6
View File
@@ -3,6 +3,7 @@ import {runInAction} from 'mobx'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
Pressable,
RefreshControl,
StyleSheet,
TouchableOpacity,
@@ -47,6 +48,10 @@ const CHILD_SPINNER = {
_reactKey: '__child_spinner__',
_isHighlightedPost: false,
}
const LOAD_MORE = {
_reactKey: '__load_more__',
_isHighlightedPost: false,
}
const BOTTOM_COMPONENT = {
_reactKey: '__bottom_component__',
_isHighlightedPost: false,
@@ -74,10 +79,14 @@ export const PostThread = observer(function PostThread({
const ref = useRef<FlatList>(null)
const hasScrolledIntoView = useRef<boolean>(false)
const [isRefreshing, setIsRefreshing] = React.useState(false)
const [maxVisible, setMaxVisible] = React.useState(100)
const navigation = useNavigation<NavigationProp>()
const posts = React.useMemo(() => {
if (view.thread) {
const arr = [TOP_COMPONENT].concat(Array.from(flattenThread(view.thread)))
let arr = [TOP_COMPONENT].concat(Array.from(flattenThread(view.thread)))
if (arr.length > maxVisible) {
arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
}
if (view.isLoadingFromCache) {
if (view.thread?.postRecord?.reply) {
arr.unshift(PARENT_SPINNER)
@@ -89,7 +98,7 @@ export const PostThread = observer(function PostThread({
return arr
}
return []
}, [view.isLoadingFromCache, view.thread])
}, [view.isLoadingFromCache, view.thread, maxVisible])
useSetTitle(
view.thread?.postRecord &&
`${sanitizeDisplayName(
@@ -178,7 +187,7 @@ export const PostThread = observer(function PostThread({
return <ComposePrompt onPressCompose={onPressReply} />
} else if (item === DELETED) {
return (
<View style={[pal.border, pal.viewLight, styles.missingItem]}>
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
<Text type="lg-bold" style={pal.textLight}>
Deleted post.
</Text>
@@ -186,12 +195,30 @@ export const PostThread = observer(function PostThread({
)
} else if (item === BLOCKED) {
return (
<View style={[pal.border, pal.viewLight, styles.missingItem]}>
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
<Text type="lg-bold" style={pal.textLight}>
Blocked post.
</Text>
</View>
)
} else if (item === LOAD_MORE) {
return (
<Pressable
onPress={() => setMaxVisible(n => n + 50)}
style={[pal.border, pal.view, styles.itemContainer]}
accessibilityLabel="Load more posts"
accessibilityHint="">
<View
style={[
pal.viewLight,
{paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6},
]}>
<Text type="lg-medium" style={pal.text}>
Load more posts
</Text>
</View>
</Pressable>
)
} else if (item === BOTTOM_COMPONENT) {
// HACK
// due to some complexities with how flatlist works, this is the easiest way
@@ -373,8 +400,8 @@ const styles = StyleSheet.create({
paddingVertical: 14,
borderRadius: 6,
},
missingItem: {
borderTop: 1,
itemContainer: {
borderTopWidth: 1,
paddingHorizontal: 18,
paddingVertical: 18,
},
+19 -24
View File
@@ -33,11 +33,11 @@ import {isNative} from 'platform/detection'
import {FollowState} from 'state/models/cache/my-follows'
import {shareUrl} from 'lib/sharing'
import {formatCount} from '../util/numeric/format'
import {navigate} from '../../../Navigation'
import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
import {BACK_HITSLOP} from 'lib/constants'
import {isInvalidHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {Link} from '../util/Link'
interface Props {
view: ProfileModel
@@ -152,21 +152,14 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
})
}, [track, store, view, onRefreshAll])
const onPressFollowers = React.useCallback(() => {
track('ProfileHeader:FollowersButtonClicked')
navigate('ProfileFollowers', {
name: isInvalidHandle(view.handle) ? view.did : view.handle,
})
store.shell.closeAllActiveElements() // for when used in the profile preview modal
}, [track, view, store.shell])
const onPressFollows = React.useCallback(() => {
track('ProfileHeader:FollowsButtonClicked')
navigate('ProfileFollows', {
name: isInvalidHandle(view.handle) ? view.did : view.handle,
})
store.shell.closeAllActiveElements() // for when used in the profile preview modal
}, [track, view, store.shell])
const trackPress = React.useCallback(
(f: 'Followers' | 'Follows') => {
track(`ProfileHeader:${f}ButtonClicked`, {
handle: view.handle,
})
},
[track, view],
)
const onPressShare = React.useCallback(() => {
track('ProfileHeader:ShareButtonClicked')
@@ -460,11 +453,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
{!blockHide && (
<>
<View style={styles.metricsLine}>
<TouchableOpacity
<Link
testID="profileHeaderFollowersButton"
style={[s.flexRow, s.mr10]}
onPress={onPressFollowers}
accessibilityRole="button"
href={makeProfileLink(view, 'followers')}
onPressOut={() => trackPress('Followers')}
asAnchor
accessibilityLabel={`${followers} ${pluralizedFollowers}`}
accessibilityHint={'Opens followers list'}>
<Text type="md" style={[s.bold, pal.text]}>
@@ -473,12 +467,13 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
<Text type="md" style={[pal.textLight]}>
{pluralizedFollowers}
</Text>
</TouchableOpacity>
<TouchableOpacity
</Link>
<Link
testID="profileHeaderFollowsButton"
style={[s.flexRow, s.mr10]}
onPress={onPressFollows}
accessibilityRole="button"
href={makeProfileLink(view, 'follows')}
onPressOut={() => trackPress('Follows')}
asAnchor
accessibilityLabel={`${following} following`}
accessibilityHint={'Opens following list'}>
<Text type="md" style={[s.bold, pal.text]}>
@@ -487,7 +482,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
<Text type="md" style={[pal.textLight]}>
following
</Text>
</TouchableOpacity>
</Link>
<Text type="md" style={[s.bold, pal.text]}>
{formatCount(view.postsCount)}{' '}
<Text type="md" style={[pal.textLight]}>
@@ -224,7 +224,6 @@ const DropdownItems = ({
{/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
<TouchableWithoutFeedback
onPress={onOuterPress}
accessibilityRole="button"
accessibilityLabel="Toggle dropdown"
accessibilityHint="">
<View style={[styles.bg]} />
+5 -5
View File
@@ -38,11 +38,11 @@ export function PostAlerts({
accessibilityHint=""
style={[styles.container, pal.viewLight, style]}>
<ShieldExclamation style={pal.text} size={16} />
<Text type="lg" style={pal.text}>
{desc.name}
</Text>
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
Learn More
<Text type="lg" style={[pal.text]}>
{desc.name}{' '}
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
Learn More
</Text>
</Text>
</Pressable>
)
+1 -14
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {AppState, FlatList, View} from 'react-native'
import {FlatList, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {observer} from 'mobx-react-lite'
import {
@@ -18,7 +18,6 @@ import {s} from 'lib/styles'
import {useAnalytics} from 'lib/analytics/analytics'
import {isWeb} from 'platform/detection'
const NOTIFICATION_MARK_READ_TIMEOUT = 5000
type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams,
'Notifications'
@@ -56,22 +55,10 @@ export const NotificationsScreen = withAuthRequired(
const softResetSub = store.onScreenSoftReset(onPressLoadLatest)
store.me.notifications.update()
screen('Notifications')
// marks notifications read if the user opens the notifications tab for x seconds
// but doesn't open any notifications or any other tab
const markReadTimeout = setTimeout(() => {
store.me.notifications.markAllRead()
}, NOTIFICATION_MARK_READ_TIMEOUT) // mark all notifications as read after 3s
// marks notification read if the user suspends the app while in the notification tab
// then opens the app into the notification tab and then closes the app again
const markReadOnBlur = AppState.addEventListener('change', event => {
if (event === 'background') store.me.notifications.markAllRead()
})
return () => {
softResetSub.remove()
markReadOnBlur.remove()
store.me.notifications.markAllRead()
clearTimeout(markReadTimeout) // in case we unmount before the timeout fires
}
}, [store, screen, onPressLoadLatest]),
)
+2 -2
View File
@@ -97,9 +97,9 @@ export const ProfileListScreen = withAuthRequired(
}, [store, list])
const onPressShareList = React.useCallback(() => {
const url = toShareUrl(`/profile/${name}/lists/${rkey}`)
const url = toShareUrl(`/profile/${list.creatorDid}/lists/${rkey}`)
shareUrl(url)
}, [name, rkey])
}, [list.creatorDid, rkey])
const renderEmptyState = React.useCallback(() => {
return <EmptyState icon="users-slash" message="This list is empty!" />
+59 -22
View File
@@ -40,6 +40,7 @@ import {AccountData} from 'state/models/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {NavigationProp} from 'lib/routes/types'
import {pluralize} from 'lib/strings/helpers'
import {HandIcon} from 'lib/icons'
import {formatCount} from 'view/com/util/numeric/format'
import Clipboard from '@react-native-clipboard/clipboard'
import {reset as resetNavigation} from '../../Navigation'
@@ -175,7 +176,7 @@ export const SettingsScreen = withAuthRequired(
Toast.show('Copied build version to clipboard')
}, [])
const openPreferencesModal = React.useCallback(() => {
const openHomeFeedPreferences = React.useCallback(() => {
navigation.navigate('PreferencesHomeFeed')
}, [navigation])
@@ -220,6 +221,19 @@ export const SettingsScreen = withAuthRequired(
</Text>
</Text>
</View>
<View style={[styles.infoLine]}>
<Text type="lg-medium" style={pal.text}>
Birthday:{' '}
</Text>
<Link
onPress={() =>
store.shell.openModal({name: 'birth-date-settings'})
}>
<Text type="lg" style={pal.link}>
Show
</Text>
</Link>
</View>
<View style={styles.spacer20} />
</>
) : null}
@@ -387,15 +401,15 @@ export const SettingsScreen = withAuthRequired(
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Advanced
Basics
</Text>
<TouchableOpacity
testID="preferencesHomeFeedButton"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={openPreferencesModal}
onPress={openHomeFeedPreferences}
accessibilityRole="button"
accessibilityHint="Open home feed preferences modal"
accessibilityLabel="Opens the home feed preferences modal">
accessibilityHint=""
accessibilityLabel="Opens the home feed preferences">
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="sliders"
@@ -406,23 +420,6 @@ export const SettingsScreen = withAuthRequired(
Home Feed Preferences
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="appPasswordBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={onPressAppPasswords}
accessibilityRole="button"
accessibilityHint="Open app password settings"
accessibilityLabel="Opens the app password settings page">
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="lock"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
<Text type="lg" style={pal.text}>
App passwords
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="savedFeedsBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
@@ -456,6 +453,44 @@ export const SettingsScreen = withAuthRequired(
Content languages
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="moderationBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={
isSwitching ? undefined : () => navigation.navigate('Moderation')
}
accessibilityRole="button"
accessibilityHint=""
accessibilityLabel="Opens moderation settings">
<View style={[styles.iconContainer, pal.btn]}>
<HandIcon style={pal.text} size={18} strokeWidth={6} />
</View>
<Text type="lg" style={pal.text}>
Moderation
</Text>
</TouchableOpacity>
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Advanced
</Text>
<TouchableOpacity
testID="appPasswordBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={onPressAppPasswords}
accessibilityRole="button"
accessibilityHint="Open app password settings"
accessibilityLabel="Opens the app password settings page">
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="lock"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
<Text type="lg" style={pal.text}>
App passwords
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="changeHandleBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
@@ -620,6 +655,8 @@ const styles = StyleSheet.create({
paddingBottom: 6,
},
infoLine: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 18,
paddingBottom: 6,
},
+1 -1
View File
@@ -311,7 +311,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
icon={
<HandIcon
strokeWidth={5.5}
style={pal.text as FontAwesomeIconStyle}
style={pal.text}
size={isDesktop ? 24 : 27}
/>
}
+35 -25
View File
@@ -45,14 +45,16 @@
tlds "^1.234.0"
typed-emitter "^2.1.0"
"@atproto/api@^0.6.8":
version "0.6.8"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.8.tgz#fe77f3ab2e7a815edca1357b0a89a7496be8f764"
integrity sha512-WmXpIbO79f85UA8AzvvSqKibojBXN1HT3KEHhUOqXJRW8X1trYijgWIXwhnxhoBQXgiQfzKG7HdORvRjmRSLoQ==
"@atproto/api@^0.6.12":
version "0.6.12"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.12.tgz#f39ad9d225aafc5fd90f07d0011d63435c657775"
integrity sha512-9R8F78553GI47Iq4FDVwL05LorWTQZQ6FmFsDF/+yryiA+a/VVyvYG4USSptURBZCRgZA5VgTW1We/PwAcDfEA==
dependencies:
"@atproto/common-web" "*"
"@atproto/syntax" "*"
"@atproto/xrpc" "*"
"@atproto/common-web" "^0.2.0"
"@atproto/lexicon" "^0.2.0"
"@atproto/syntax" "^0.1.0"
"@atproto/xrpc" "^0.3.0"
multiformats "^9.9.0"
tlds "^1.234.0"
typed-emitter "^2.1.0"
@@ -104,7 +106,7 @@
typed-emitter "^2.1.0"
uint8arrays "3.0.0"
"@atproto/common-web@*":
"@atproto/common-web@*", "@atproto/common-web@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.2.0.tgz#8420da28e89dac64ad2a11b6ff10a4023c67151f"
integrity sha512-ugKrT8CWf6PDsZ29VOhOCs5K4z9BAFIV7SxPXA+MHC7pINqQ+wyjIq+DtUI8kmUSce1dTqc/lMN1akf/W5whVQ==
@@ -203,7 +205,7 @@
axios "^0.27.2"
zod "^3.21.4"
"@atproto/lexicon@*":
"@atproto/lexicon@*", "@atproto/lexicon@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.2.0.tgz#e18590f248fa2795a67d7440adba73ab5a48dd1e"
integrity sha512-0OpLZ4o4pbRj2e19h2fO69781wPmL0S8WJaXey3y9FLjV19lj18IEbnlE/OQG8qZNDyGmgezs00L/o7qNCJorw==
@@ -317,7 +319,7 @@
uint8arrays "3.0.0"
zod "^3.21.4"
"@atproto/syntax@*":
"@atproto/syntax@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.1.0.tgz#f13b9dad8d13342cc54295ecd702ea13c82c9bf0"
integrity sha512-Ktui0qvIXt1o1Px1KKC0eqn69MfRHQ9ok5EwjcxIEPbJ8OY5XqkeyJneFDIWRJZiR6vqPHfjFYRUpTB+jNPfRQ==
@@ -356,6 +358,14 @@
"@atproto/lexicon" "*"
zod "^3.21.4"
"@atproto/xrpc@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.3.0.tgz#32dd3a15fedad401b94fdf38313aebff1c0b9d0d"
integrity sha512-PYE/4GT/sS4s/FxR5QO2YBaLU6GdhraYVfMOtfj+AN8YgZw1QoMHXkAv4v+BVH4T5HWU2XwZEBZvwfsr7YcewA==
dependencies:
"@atproto/lexicon" "*"
zod "^3.21.4"
"@aws-crypto/crc32@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa"
@@ -10273,24 +10283,24 @@ expo-font@~11.4.0:
dependencies:
fontfaceobserver "^2.1.0"
expo-image-loader@~4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-4.3.0.tgz#d897b4d4f1838faa8291c36f00b99c2b36a6ac5c"
integrity sha512-2kqJIO+oYM8J3GbvTUHLqTSpt1dLpOn/X0eB4U4RTuzz/faj8l/TyQELsMBLlGAkweNUuG9LqznbaBz+WuSFEw==
expo-image-loader@~4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-4.4.0.tgz#35909516311aa60fa4bf35aa6eb7624d7f7f788d"
integrity sha512-dq88tiRB8aB1U+O/oJI7IY4VU9Nu2iDjEwR8QDfLtD4xWd5lNGoyic5Vh7911E1qNJFqNMSVIVzOD5l1SQ3A6g==
expo-image-manipulator@~11.3.0:
version "11.3.0"
resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-11.3.0.tgz#3bf5e1d7331c182950f73bfacf2060cc4604170e"
integrity sha512-lw17v8VD5LqE+j3YMZNNlfM19OlkBPzLEoVyGcx9FtGC533le5dnXF+UiZTCbFIVr/xyBJTMse9G0oJ70q9MbQ==
expo-image-manipulator@~11.5.0:
version "11.5.0"
resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-11.5.0.tgz#2a0f4c069dc25722c2c8f0e15c0c42353f1115a4"
integrity sha512-W/mwSIuLyolDDSzIkENvwVoQpe6HJfpcAQbeu3VX3ZcTJrgvORM/tfuCJacCTHQ0iVIB49Txv65CgbtO+3Pd6w==
dependencies:
expo-image-loader "~4.3.0"
expo-image-loader "~4.4.0"
expo-image-picker@~14.3.2:
version "14.3.2"
resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-14.3.2.tgz#5ae2f36fe518a10c8fe82e5ff2ee735c14817cfc"
integrity sha512-xr/YeQMIYheXecWP033F2SPwpBlBR5xVCx7YSfSCTH8Y9pw7Z886agqKGbS9QBVGlzJ5qecJktZ6ASSzeslDVg==
expo-image-picker@~14.5.0:
version "14.5.0"
resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-14.5.0.tgz#fdc14c74ad338a6a630f8fc5c81e642d6bea343c"
integrity sha512-9icvfQuWMdNKufFqu+FxenKcfybz9ior+XYQCIl/q5+mpHPo2tKq8I30vu2BjsOJ0Cfo7V4vMeN7a9F/3ooUfQ==
dependencies:
expo-image-loader "~4.3.0"
expo-image-loader "~4.4.0"
expo-image@~1.3.2:
version "1.3.2"
@@ -14637,7 +14647,7 @@ multicast-dns@^7.2.5:
dns-packet "^5.2.2"
thunky "^1.0.2"
multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.6.4:
multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.6.4, multiformats@^9.9.0:
version "9.9.0"
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37"
integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==