diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000..b4213aea24
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+SENTRY_AUTH_TOKEN=
diff --git a/.eslintrc.js b/.eslintrc.js
index 93348b0d03..19fcf23083 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,6 +1,10 @@
module.exports = {
root: true,
- extends: '@react-native-community',
+ extends: [
+ '@react-native-community',
+ 'plugin:react-native-a11y/ios',
+ 'prettier',
+ ],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'detox'],
ignorePatterns: [
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000000..c608f66000
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,35 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+
+
+**To Reproduce**
+
+Steps to reproduce the behavior:
+
+1.
+
+**Expected behavior**
+
+
+
+**Screenshots**
+
+
+
+**Details**
+
+ - Platform:
+ - Platform version:
+ - App version:
+
+**Additional context**
+
+
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000000..0ac6b3c416
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,24 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: feature-request
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+
+
+
+**Describe the solution you'd like**
+
+
+
+**Describe alternatives you've considered**
+
+
+
+**Additional context**
+
+
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 77e58f5478..c5b9a324d4 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -18,8 +18,10 @@ jobs:
uses: actions/checkout@v3
- name: Yarn install
run: yarn
- - name: Typescript & Lint check
+ - name: Lint check
run: yarn lint
+ - name: Type check
+ run: yarn typecheck
testing:
name: Run tests
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index bab37d2ad4..2fa850bf76 100644
--- a/.gitignore
+++ b/.gitignore
@@ -92,4 +92,8 @@ web-build/
# Android & iOS folders
android/
-ios/
\ No newline at end of file
+ios/
+
+# environment variables
+.env
+.env.*
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 95f0ec02e8..241926db4e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,7 +17,7 @@ ENV CGO_ENABLED=1
COPY . .
#
-# Generate the Javascript webpack.
+# Generate the JavaScript webpack.
#
RUN mkdir --parents $NVM_DIR && \
wget \
@@ -35,11 +35,8 @@ RUN \. "$NVM_DIR/nvm.sh" && \
# DEBUG
RUN find ./bskyweb/static && find ./web-build/static
-# Copy the bundle js files.
-RUN cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/
-
#
-# Generate the bksyweb Go binary.
+# Generate the bskyweb Go binary.
#
RUN cd bskyweb/ && \
go mod download && \
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..8016366443
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,7 @@
+Copyright 2023 Bluesky PBLLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000..e93b6357a5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,35 @@
+
+SHELL = /bin/bash
+.SHELLFLAGS = -o pipefail -c
+
+.PHONY: help
+help: ## Print info about all commands
+ @echo "Commands:"
+ @echo
+ @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[01;32m%-20s\033[0m %s\n", $$1, $$2}'
+
+.PHONY: build-web
+build-web: ## Compile web bundle, copy to bskyweb directory
+ yarn build-web
+
+.PHONY: test
+test: ## Run all tests
+ yarn test
+
+.PHONY: lint
+lint: ## Run style checks and verify syntax
+ yarn run lint
+
+#.PHONY: fmt
+#fmt: ## Run syntax re-formatting
+# yarn prettier
+
+.PHONY: deps
+deps: ## Installs dependent libs using 'yarn install'
+ yarn install --frozen-lockfile
+
+.PHONY: nvm-setup
+nvm-setup: ## Use NVM to install and activate node+yarn
+ nvm install 18
+ nvm use 18
+ npm install --global yarn
diff --git a/README.md b/README.md
index 3595641e2f..ead1e677d7 100644
--- a/README.md
+++ b/README.md
@@ -1,50 +1,61 @@
-# Bluesky
+# Bluesky Social App
-## Build instructions
+Welcome friends! This is the codebase for the Bluesky Social app. It serves as a resource to engineers building on the [AT Protocol](https://atproto.com).
-- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
-- Setup your environment [for e2e testing using detox](https://wix.github.io/Detox/docs/introduction/getting-started):
- - yarn global add detox-cli
- - brew tap wix/brew
- - brew install applesimutils
-- After initial setup:
- - `npx expo prebuild` -> you will also need to run this anytime `app.json` or `package.json` changes
-- Start the dev servers
- - `git clone git@github.com:bluesky-social/atproto.git`
- - `cd atproto`
- - `yarn`
- - `cd packages/dev-env && yarn start`
-- Run the dev app
- - iOS: `yarn ios`
- - Android: `yarn android`
- - Web: `yarn web`
-- Run e2e tests
- - Start in various console tabs:
- - `yarn e2e:server`
- - `yarn e2e:metro`
- - Run once: `yarn e2e:build`
- - Each test run: `yarn e2e:run`
-- Tips
- - `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 localhosted 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 bee the Web version. ([More info](https://stackoverflow.com/questions/44001050/platform-specific-import-component-in-react-native-with-typescript).)
+- **Web: [bsky.app](https://bsky.app)**
+- **iOS: [App Store](https://apps.apple.com/us/app/bluesky-social/id6444370199)**
+- **Android: [Play Store](https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&hl=en_US&gl=US)**
-## Various notes
+Links:
-### Debugging
+- [Build instructions](./docs/build.md)
+- [ATProto repo](https://github.com/bluesky-social/atproto)
+- [ATProto docs](https://atproto.com)
-- Note that since 0.70, debugging using the old debugger (which shows up using CMD+D) doesn't work anymore. Follow the instructions below to debug the code: https://reactnative.dev/docs/next/hermes#debugging-js-on-hermes-using-google-chromes-devtools
+## Rules & guidelines
-### Running E2E Tests
+---
-- Make sure you've setup your environment following above
-- Make sure Metro and the dev server are running
-- Run `yarn e2e`
-- Find the artifacts in the `artifact` folder
+ℹ️ While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review.
-### Polyfills
+---
-`./platform/polyfills.*.ts` adds polyfills to the environment. Currently this includes:
+**Rules:**
-- TextEncoder / TextDecoder
+- We may not respond to your issue or PR.
+- We may close an issue or PR without much feedback.
+- We may lock discussions or contributions if our attention is getting DDOSed.
+- We're not going to provide support for build issues.
+
+**Guidelines:**
+
+- Check for existing issues before filing a new one please.
+- Open an issue and give some time for discussion before submitting a PR.
+- Stay away from PRs like...
+ - Changing "Post" to "Skeet."
+ - Refactoring the codebase, eg to replace mobx with redux or something.
+ - Adding entirely new features without prior discussion.
+
+Remember, we serve a wide community of users. Our day to day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
+
+## Forking guidelines
+
+You have our blessing 🪄✨ to fork this application! However, it's very important to be clear to users when you're giving them a fork.
+
+Please be sure to:
+
+- Change all branding in the repository and UI to clearly differentiate from Bluesky.
+- Change any support links (feedback, email, terms of service, etc) to your own systems.
+- Replace any analytics or error-collection systems with your own so we don't get super confused.
+
+## Security disclosures
+
+If you discover any security issues, please send an email to security@bsky.app. The email is automatically CCed to the entire team and we'll respond promptly.
+
+## License (MIT)
+
+See [./LICENSE](./LICENSE) for the full license.
+
+## P.S.
+
+We ❤️ you and all of the ways you support us. Thank you for making Bluesky a great place!
diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts
index 7bcad47f32..6ddfe3ca02 100644
--- a/__e2e__/mock-server.ts
+++ b/__e2e__/mock-server.ts
@@ -63,6 +63,232 @@ async function main() {
},
})
}
+ if ('labels' in url.query) {
+ console.log('Generating naughty users with labels')
+
+ const anchorPost = await server.mocker.createPost(
+ 'alice',
+ 'Anchor post',
+ )
+
+ for (const user of [
+ 'csam-account',
+ 'csam-profile',
+ 'csam-posts',
+ 'porn-account',
+ 'porn-profile',
+ 'porn-posts',
+ 'nudity-account',
+ 'nudity-profile',
+ 'nudity-posts',
+ 'unknown-account',
+ 'unknown-profile',
+ 'unknown-posts',
+ 'always-filter-account',
+ 'always-filter-profile',
+ 'always-filter-posts',
+ 'always-warn-account',
+ 'always-warn-profile',
+ 'always-warn-posts',
+ 'muted-account',
+ 'muted-by-list-account',
+ ]) {
+ await server.mocker.createUser(user)
+ await server.mocker.follow('alice', user)
+ await server.mocker.follow(user, 'alice')
+ await server.mocker.createPost(user, `Unlabeled post from ${user}`)
+ await server.mocker.createReply(
+ user,
+ `Unlabeled reply from ${user}`,
+ anchorPost,
+ )
+ await server.mocker.like(user, anchorPost)
+ }
+
+ await server.mocker.labelAccount('csam', 'csam-account')
+ await server.mocker.labelProfile('csam', 'csam-profile')
+ await server.mocker.labelPost(
+ 'csam',
+ await server.mocker.createPost('csam-posts', 'csam post'),
+ )
+ await server.mocker.labelPost(
+ 'csam',
+ await server.mocker.createQuotePost(
+ 'csam-posts',
+ 'csam quote post',
+ anchorPost,
+ ),
+ )
+ await server.mocker.labelPost(
+ 'csam',
+ await server.mocker.createReply(
+ 'csam-posts',
+ 'csam reply',
+ anchorPost,
+ ),
+ )
+
+ await server.mocker.labelAccount('porn', 'porn-account')
+ await server.mocker.labelProfile('porn', 'porn-profile')
+ await server.mocker.labelPost(
+ 'porn',
+ await server.mocker.createPost('porn-posts', 'porn post'),
+ )
+ await server.mocker.labelPost(
+ 'porn',
+ await server.mocker.createQuotePost(
+ 'porn-posts',
+ 'porn quote post',
+ anchorPost,
+ ),
+ )
+ await server.mocker.labelPost(
+ 'porn',
+ await server.mocker.createReply(
+ 'porn-posts',
+ 'porn reply',
+ anchorPost,
+ ),
+ )
+
+ await server.mocker.labelAccount('nudity', 'nudity-account')
+ await server.mocker.labelProfile('nudity', 'nudity-profile')
+ await server.mocker.labelPost(
+ 'nudity',
+ await server.mocker.createPost('nudity-posts', 'nudity post'),
+ )
+ await server.mocker.labelPost(
+ 'nudity',
+ await server.mocker.createQuotePost(
+ 'nudity-posts',
+ 'nudity quote post',
+ anchorPost,
+ ),
+ )
+ await server.mocker.labelPost(
+ 'nudity',
+ await server.mocker.createReply(
+ 'nudity-posts',
+ 'nudity reply',
+ anchorPost,
+ ),
+ )
+
+ await server.mocker.labelAccount(
+ 'not-a-real-label',
+ 'unknown-account',
+ )
+ await server.mocker.labelProfile(
+ 'not-a-real-label',
+ 'unknown-profile',
+ )
+ await server.mocker.labelPost(
+ 'not-a-real-label',
+ await server.mocker.createPost('unknown-posts', 'unknown post'),
+ )
+ await server.mocker.labelPost(
+ 'not-a-real-label',
+ await server.mocker.createQuotePost(
+ 'unknown-posts',
+ 'unknown quote post',
+ anchorPost,
+ ),
+ )
+ await server.mocker.labelPost(
+ 'not-a-real-label',
+ await server.mocker.createReply(
+ 'unknown-posts',
+ 'unknown reply',
+ anchorPost,
+ ),
+ )
+
+ await server.mocker.labelAccount('!filter', 'always-filter-account')
+ await server.mocker.labelProfile('!filter', 'always-filter-profile')
+ await server.mocker.labelPost(
+ '!filter',
+ await server.mocker.createPost(
+ 'always-filter-posts',
+ 'always-filter post',
+ ),
+ )
+ await server.mocker.labelPost(
+ '!filter',
+ await server.mocker.createQuotePost(
+ 'always-filter-posts',
+ 'always-filter quote post',
+ anchorPost,
+ ),
+ )
+ await server.mocker.labelPost(
+ '!filter',
+ await server.mocker.createReply(
+ 'always-filter-posts',
+ 'always-filter reply',
+ anchorPost,
+ ),
+ )
+
+ await server.mocker.labelAccount('!warn', 'always-warn-account')
+ await server.mocker.labelProfile('!warn', 'always-warn-profile')
+ await server.mocker.labelPost(
+ '!warn',
+ await server.mocker.createPost(
+ 'always-warn-posts',
+ 'always-warn post',
+ ),
+ )
+ await server.mocker.labelPost(
+ '!warn',
+ await server.mocker.createQuotePost(
+ 'always-warn-posts',
+ 'always-warn quote post',
+ anchorPost,
+ ),
+ )
+ await server.mocker.labelPost(
+ '!warn',
+ await server.mocker.createReply(
+ 'always-warn-posts',
+ 'always-warn reply',
+ anchorPost,
+ ),
+ )
+
+ await server.mocker.users.alice.agent.mute('muted-account.test')
+ await server.mocker.createPost('muted-account', 'muted post')
+ await server.mocker.createQuotePost(
+ 'muted-account',
+ 'muted quote post',
+ anchorPost,
+ )
+ await server.mocker.createReply(
+ 'muted-account',
+ 'muted reply',
+ anchorPost,
+ )
+
+ const list = await server.mocker.createMuteList(
+ 'alice',
+ 'Muted Users',
+ )
+ await server.mocker.addToMuteList(
+ 'alice',
+ list,
+ server.mocker.users['muted-by-list-account'].did,
+ )
+ await server.mocker.createPost('muted-by-list-account', 'muted post')
+ await server.mocker.createQuotePost(
+ 'muted-by-list-account',
+ 'account quote post',
+ anchorPost,
+ )
+ await server.mocker.createReply(
+ 'muted-by-list-account',
+ 'account reply',
+ anchorPost,
+ )
+ }
}
console.log('Ready')
return res.writeHead(200).end(server.pdsUrl)
diff --git a/__e2e__/tests/create-account.test.ts b/__e2e__/tests/create-account.test.ts
index 38466ed8e8..7db4e912a6 100644
--- a/__e2e__/tests/create-account.test.ts
+++ b/__e2e__/tests/create-account.test.ts
@@ -20,7 +20,6 @@ describe('Create account', () => {
await element(by.id('nextBtn')).tap()
await element(by.id('emailInput')).typeText('example@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
- await element(by.id('is13Input')).tap()
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('e2e-test')
diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts
index 1ec1774f3e..7fa9ff28c5 100644
--- a/__e2e__/tests/home-screen.test.ts
+++ b/__e2e__/tests/home-screen.test.ts
@@ -57,7 +57,9 @@ describe('Home screen', () => {
.tap()
await element(by.id('postDropdownReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).toBeVisible()
- await element(by.id('reportPostRadios-spam')).tap()
+ await element(
+ by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+ ).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
})
diff --git a/__e2e__/tests/invite-codes.test.ts b/__e2e__/tests/invite-codes.test.ts
index e3bb5d7f22..846d3b7688 100644
--- a/__e2e__/tests/invite-codes.test.ts
+++ b/__e2e__/tests/invite-codes.test.ts
@@ -37,7 +37,6 @@ describe('invite-codes', () => {
await element(by.id('inviteCodeInput')).typeText(inviteCode)
await element(by.id('emailInput')).typeText('example@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
- await element(by.id('is13Input')).tap()
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('e2e-test')
diff --git a/__e2e__/tests/mute-lists.test.ts b/__e2e__/tests/mute-lists.test.ts
new file mode 100644
index 0000000000..e931625139
--- /dev/null
+++ b/__e2e__/tests/mute-lists.test.ts
@@ -0,0 +1,141 @@
+/* eslint-env detox/detox */
+
+import {openApp, login, createServer, sleep} from '../util'
+
+describe('Profile screen', () => {
+ let service: string
+ beforeAll(async () => {
+ service = await createServer('?users&follows&labels')
+ await openApp({
+ permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
+ })
+ })
+
+ it('Login and view my mutelists', async () => {
+ await expect(element(by.id('signInButton'))).toBeVisible()
+ await login(service, 'alice', 'hunter2')
+ await element(by.id('viewHeaderDrawerBtn')).tap()
+ await expect(element(by.id('drawer'))).toBeVisible()
+ await element(by.id('menuItemButton-Moderation')).tap()
+ await element(by.id('mutelistsBtn')).tap()
+ await expect(element(by.id('list-Muted Users'))).toBeVisible()
+ await element(by.id('list-Muted Users')).tap()
+ await expect(
+ element(by.id('user-muted-by-list-account.test')),
+ ).toBeVisible()
+ })
+
+ it('Toggle subscription', async () => {
+ await element(by.id('unsubscribeListBtn')).tap()
+ await element(by.id('subscribeListBtn')).tap()
+ })
+
+ it('Edit display name and description via the edit mutelist modal', async () => {
+ await element(by.id('editListBtn')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
+ await element(by.id('editNameInput')).clearText()
+ await element(by.id('editNameInput')).typeText('Bad Ppl')
+ await element(by.id('editDescriptionInput')).clearText()
+ await element(by.id('editDescriptionInput')).typeText('They bad')
+ await element(by.id('saveBtn')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
+ await expect(element(by.id('listName'))).toHaveText('Bad Ppl')
+ await expect(element(by.id('listDescription'))).toHaveText('They bad')
+ // have to wait for the toast to clear
+ await waitFor(element(by.id('editListBtn')))
+ .toBeVisible()
+ .withTimeout(5000)
+ })
+
+ it('Remove description via the edit mutelist modal', async () => {
+ await element(by.id('editListBtn')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
+ await element(by.id('editDescriptionInput')).clearText()
+ await element(by.id('saveBtn')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
+ await expect(element(by.id('listDescription'))).not.toBeVisible()
+ // have to wait for the toast to clear
+ await waitFor(element(by.id('editListBtn')))
+ .toBeVisible()
+ .withTimeout(5000)
+ })
+
+ it('Set avi via the edit mutelist modal', async () => {
+ await expect(element(by.id('userAvatarFallback'))).toExist()
+ await element(by.id('editListBtn')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
+ await element(by.id('changeAvatarBtn')).tap()
+ await element(by.id('changeAvatarLibraryBtn')).tap()
+ await sleep(3e3)
+ await element(by.id('saveBtn')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
+ await expect(element(by.id('userAvatarImage'))).toExist()
+ // have to wait for the toast to clear
+ await waitFor(element(by.id('editListBtn')))
+ .toBeVisible()
+ .withTimeout(5000)
+ })
+
+ it('Remove avi via the edit mutelist modal', async () => {
+ await expect(element(by.id('userAvatarImage'))).toExist()
+ await element(by.id('editListBtn')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
+ await element(by.id('changeAvatarBtn')).tap()
+ await element(by.id('changeAvatarRemoveBtn')).tap()
+ await element(by.id('saveBtn')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
+ await expect(element(by.id('userAvatarFallback'))).toExist()
+ // have to wait for the toast to clear
+ await waitFor(element(by.id('editListBtn')))
+ .toBeVisible()
+ .withTimeout(5000)
+ })
+
+ it('Delete the mutelist', async () => {
+ await element(by.id('deleteListBtn')).tap()
+ await element(by.id('confirmBtn')).tap()
+ await expect(element(by.id('emptyMuteLists'))).toBeVisible()
+ })
+
+ it('Create a new mutelist', async () => {
+ await element(by.id('emptyMuteLists-button')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
+ await element(by.id('editNameInput')).typeText('Bad Ppl')
+ await element(by.id('editDescriptionInput')).typeText('They bad')
+ await element(by.id('saveBtn')).tap()
+ await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
+ await expect(element(by.id('listName'))).toHaveText('Bad Ppl')
+ await expect(element(by.id('listDescription'))).toHaveText('They bad')
+ // have to wait for the toast to clear
+ await waitFor(element(by.id('editListBtn')))
+ .toBeVisible()
+ .withTimeout(5000)
+ })
+
+ it('Shows the mutelist on my profile', async () => {
+ await element(by.id('bottomBarProfileBtn')).tap()
+ await element(by.id('selector-2')).tap()
+ await element(by.id('list-Bad Ppl')).tap()
+ })
+
+ it('Adds and removes users on mutelists', async () => {
+ await element(by.id('bottomBarSearchBtn')).tap()
+ await element(by.id('searchTextInput')).typeText('bob')
+ await element(by.id('searchAutoCompleteResult-bob.test')).tap()
+ await expect(element(by.id('profileView'))).toBeVisible()
+
+ await element(by.id('profileHeaderDropdownBtn')).tap()
+ await element(by.id('profileHeaderDropdownListAddRemoveBtn')).tap()
+ await expect(element(by.id('listAddRemoveUserModal'))).toBeVisible()
+ await element(by.id('toggleBtn-Bad Ppl')).tap()
+ await element(by.id('saveBtn')).tap()
+ await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible()
+
+ await element(by.id('profileHeaderDropdownBtn')).tap()
+ await element(by.id('profileHeaderDropdownListAddRemoveBtn')).tap()
+ await expect(element(by.id('listAddRemoveUserModal'))).toBeVisible()
+ await element(by.id('toggleBtn-Bad Ppl')).tap()
+ await element(by.id('saveBtn')).tap()
+ await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible()
+ })
+})
diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts
index cf9debb59a..a7bb93656b 100644
--- a/__e2e__/tests/profile-screen.test.ts
+++ b/__e2e__/tests/profile-screen.test.ts
@@ -120,7 +120,9 @@ describe('Profile screen', () => {
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.id('profileHeaderDropdownReportBtn')).tap()
await expect(element(by.id('reportAccountModal'))).toBeVisible()
- await element(by.id('reportAccountRadios-spam')).tap()
+ await element(
+ by.id('reportAccountRadios-com.atproto.moderation.defs#reasonSpam'),
+ ).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportAccountModal'))).not.toBeVisible()
})
@@ -166,7 +168,9 @@ describe('Profile screen', () => {
await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
await element(by.id('postDropdownReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).toBeVisible()
- await element(by.id('reportPostRadios-spam')).tap()
+ await element(
+ by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+ ).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
})
diff --git a/__e2e__/tests/thread-muting.test.ts b/__e2e__/tests/thread-muting.test.ts
new file mode 100644
index 0000000000..a5cefdb26b
--- /dev/null
+++ b/__e2e__/tests/thread-muting.test.ts
@@ -0,0 +1,116 @@
+/* eslint-env detox/detox */
+
+import {openApp, login, createServer} from '../util'
+
+describe('Thread muting', () => {
+ let service: string
+ beforeAll(async () => {
+ service = await createServer('?users&follows')
+ await openApp({permissions: {notifications: 'YES'}})
+ })
+
+ it('Login, create a thread, and log out', async () => {
+ await login(service, 'alice', 'hunter2')
+ await element(by.id('homeScreenFeedTabs-Following')).tap()
+ await element(by.id('composeFAB')).tap()
+ await element(by.id('composerTextInput')).typeText('Test thread')
+ await element(by.id('composerPublishBtn')).tap()
+ await expect(element(by.id('composeFAB'))).toBeVisible()
+ await element(by.id('viewHeaderDrawerBtn')).tap()
+ await element(by.id('menuItemButton-Settings')).tap()
+ await element(by.id('signOutBtn')).tap()
+ })
+
+ it('Login, reply to the thread, and log out', async () => {
+ await login(service, 'bob', 'hunter2')
+ await element(by.id('homeScreenFeedTabs-Following')).tap()
+ const alicePosts = by.id('feedItem-by-alice.test')
+ await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
+ await element(by.id('composerTextInput')).typeText('Reply 1')
+ await element(by.id('composerPublishBtn')).tap()
+ await expect(element(by.id('composeFAB'))).toBeVisible()
+ await element(by.id('viewHeaderDrawerBtn')).tap()
+ await element(by.id('menuItemButton-Settings')).tap()
+ await element(by.id('signOutBtn')).tap()
+ })
+
+ it('Login, confirm notification exists, mute thread, and log out', async () => {
+ await login(service, 'alice', 'hunter2')
+
+ await element(by.id('bottomBarNotificationsBtn')).tap()
+ const bobNotifs = by.id('feedItem-by-bob.test')
+ await expect(
+ element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
+ ).toHaveText('Reply 1')
+ await element(by.id('postDropdownBtn').withAncestor(bobNotifs))
+ .atIndex(0)
+ .tap()
+ await element(by.id('postDropdownMuteThreadBtn')).tap()
+ // have to wait for the toast to clear
+ await waitFor(element(by.id('viewHeaderDrawerBtn')))
+ .toBeVisible()
+ .withTimeout(5000)
+
+ await element(by.id('viewHeaderDrawerBtn')).tap()
+ await element(by.id('menuItemButton-Settings')).tap()
+ await element(by.id('signOutBtn')).tap()
+ })
+
+ it('Login, reply to the thread twice, and log out', async () => {
+ await login(service, 'bob', 'hunter2')
+
+ await element(by.id('bottomBarProfileBtn')).tap()
+ await element(by.id('selector-1')).tap()
+ const bobPosts = by.id('feedItem-by-bob.test')
+ await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap()
+ await element(by.id('composerTextInput')).typeText('Reply 2')
+ await element(by.id('composerPublishBtn')).tap()
+ await expect(element(by.id('composeFAB'))).toBeVisible()
+
+ const alicePosts = by.id('feedItem-by-alice.test')
+ await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
+ await element(by.id('composerTextInput')).typeText('Reply 3')
+ await element(by.id('composerPublishBtn')).tap()
+ await expect(element(by.id('composeFAB'))).toBeVisible()
+
+ await element(by.id('bottomBarHomeBtn')).tap()
+ await element(by.id('viewHeaderDrawerBtn')).tap()
+ await element(by.id('menuItemButton-Settings')).tap()
+ await element(by.id('signOutBtn')).tap()
+ })
+
+ it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => {
+ await login(service, 'alice', 'hunter2')
+
+ await element(by.id('bottomBarNotificationsBtn')).tap()
+ const bobNotifs = by.id('feedItem-by-bob.test')
+ await expect(
+ element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
+ ).not.toExist()
+
+ await element(by.id('bottomBarHomeBtn')).tap()
+ const alicePosts = by.id('feedItem-by-alice.test')
+ await element(by.id('postDropdownBtn').withAncestor(alicePosts))
+ .atIndex(0)
+ .tap()
+ await element(by.id('postDropdownMuteThreadBtn')).tap()
+
+ // TODO
+ // the swipe down to trigger PTR isnt working and I dont want to block on this
+ // -prf
+ // await element(by.id('bottomBarNotificationsBtn')).tap()
+ // await element(by.id('notifsFeed')).swipe('down', 'fast')
+ // await waitFor(element(by.id('postText').withAncestor(bobNotifs)))
+ // .toBeVisible()
+ // .withTimeout(5000)
+ // await expect(
+ // element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
+ // ).toHaveText('Reply 2')
+ // await expect(
+ // element(by.id('postText').withAncestor(bobNotifs)).atIndex(1),
+ // ).toHaveText('Reply 3')
+ // await expect(
+ // element(by.id('postText').withAncestor(bobNotifs)).atIndex(2),
+ // ).toHaveText('Reply 1')
+ })
+})
diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts
index f84c339cef..8d3eacc884 100644
--- a/__e2e__/tests/thread-screen.test.ts
+++ b/__e2e__/tests/thread-screen.test.ts
@@ -106,7 +106,9 @@ describe('Thread screen', () => {
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await element(by.id('postDropdownReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).toBeVisible()
- await element(by.id('reportPostRadios-spam')).tap()
+ await element(
+ by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+ ).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
})
@@ -116,7 +118,9 @@ describe('Thread screen', () => {
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await element(by.id('postDropdownReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).toBeVisible()
- await element(by.id('reportPostRadios-spam')).tap()
+ await element(
+ by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
+ ).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
})
diff --git a/__e2e__/util.ts b/__e2e__/util.ts
index 78d9f9f5d1..f5bb728151 100644
--- a/__e2e__/util.ts
+++ b/__e2e__/util.ts
@@ -1,10 +1,24 @@
import {resolveConfig} from 'detox/internals'
+import {execSync} from 'child_process'
const platform = device.getPlatform()
export async function openApp(opts: any) {
opts = opts || {}
const config = await resolveConfig()
+
+ if (device.getPlatform() === 'ios') {
+ // disable password autofill
+ execSync(
+ `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/UserSettings.plist`,
+ )
+ execSync(
+ `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/EffectiveUserSettings.plist`,
+ )
+ execSync(
+ `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`,
+ )
+ }
if (config.configurationName.split('.').includes('debug')) {
return await openAppForDebugBuild(platform, opts)
} else {
@@ -42,6 +56,7 @@ export async function login(
await device.takeScreenshot('2- opened service selector')
}
await element(by.id('customServerTextInput')).typeText(service)
+ await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('customServerSelectBtn')).tap()
if (takeScreenshots) {
await device.takeScreenshot('3- input custom service')
diff --git a/__mocks__/expo-localization.js b/__mocks__/expo-localization.js
new file mode 100644
index 0000000000..8bd537cf6c
--- /dev/null
+++ b/__mocks__/expo-localization.js
@@ -0,0 +1 @@
+export const getLocales = jest.fn().mockResolvedValue([])
diff --git a/__tests__/lib/link-meta.test.ts b/__tests__/lib/link-meta.test.ts
index f0ca7a9d4b..504b11c22e 100644
--- a/__tests__/lib/link-meta.test.ts
+++ b/__tests__/lib/link-meta.test.ts
@@ -1,106 +1,4 @@
-import {
- LikelyType,
- getLinkMeta,
- getLikelyType,
-} from '../../src/lib/link-meta/link-meta'
-import {exampleComHtml} from './__mocks__/exampleComHtml'
-import {BskyAgent} from '@atproto/api'
-import {DEFAULT_SERVICE, RootStoreModel} from '../../src/state'
-
-describe('getLinkMeta', () => {
- let rootStore: RootStoreModel
-
- beforeEach(() => {
- rootStore = new RootStoreModel(new BskyAgent({service: DEFAULT_SERVICE}))
- })
-
- const inputs = [
- '',
- 'httpbadurl',
- 'https://example.com',
- 'https://example.com/index.html',
- 'https://example.com/image.png',
- 'https://example.com/video.avi',
- 'https://example.com/audio.ogg',
- 'https://example.com/text.txt',
- 'https://example.com/javascript.js',
- 'https://bsky.app/',
- 'https://bsky.app/index.html',
- ]
- const outputs = [
- {
- error: 'Invalid URL',
- likelyType: LikelyType.Other,
- url: '',
- },
- {
- error: 'Invalid URL',
- likelyType: LikelyType.Other,
- url: 'httpbadurl',
- },
- {
- likelyType: LikelyType.HTML,
- url: 'https://example.com',
- title: 'Example Domain',
- description: 'An example website',
- },
- {
- likelyType: LikelyType.HTML,
- url: 'https://example.com/index.html',
- title: 'Example Domain',
- description: 'An example website',
- },
- {
- likelyType: LikelyType.Image,
- url: 'https://example.com/image.png',
- },
- {
- likelyType: LikelyType.Video,
- url: 'https://example.com/video.avi',
- },
- {
- likelyType: LikelyType.Audio,
- url: 'https://example.com/audio.ogg',
- },
- {
- likelyType: LikelyType.Text,
- url: 'https://example.com/text.txt',
- },
- {
- likelyType: LikelyType.Other,
- url: 'https://example.com/javascript.js',
- },
- {
- likelyType: LikelyType.AtpData,
- url: '/',
- },
- {
- likelyType: LikelyType.AtpData,
- url: '/index.html',
- },
- {
- likelyType: LikelyType.Other,
- url: '',
- title: '',
- },
- ]
- it('correctly handles a set of text inputs', async () => {
- for (let i = 0; i < inputs.length; i++) {
- global.fetch = jest.fn().mockImplementationOnce(() => {
- return new Promise((resolve, _reject) => {
- resolve({
- ok: true,
- status: 200,
- text: () => exampleComHtml,
- })
- })
- })
- const input = inputs[i]
- const output = await getLinkMeta(rootStore, input)
- expect(output).toEqual(outputs[i])
- }
- })
-})
+import {LikelyType, getLikelyType} from '../../src/lib/link-meta/link-meta'
describe('getLikelyType', () => {
it('correctly handles non-parsed url', async () => {
diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts
index f25bd02a78..936708cf26 100644
--- a/__tests__/lib/string.test.ts
+++ b/__tests__/lib/string.test.ts
@@ -48,7 +48,7 @@ describe('detectLinkables', () => {
'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/ ',
'https://foo.com https://bar.com/whatever https://baz.com',
'punctuation https://foo.com, https://bar.com/whatever; https://baz.com.',
- 'parenthentical (https://foo.com)',
+ 'parenthetical (https://foo.com)',
'except for https://foo.com/thing_(cool)',
]
const outputs = [
@@ -112,7 +112,7 @@ describe('detectLinkables', () => {
{link: 'https://baz.com'},
'.',
],
- ['parenthentical (', {link: 'https://foo.com'}, ')'],
+ ['parenthetical (', {link: 'https://foo.com'}, ')'],
['except for ', {link: 'https://foo.com/thing_(cool)'}],
]
it('correctly handles a set of text inputs', () => {
diff --git a/app.json b/app.json
index ba2c6958fe..d427e04d0e 100644
--- a/app.json
+++ b/app.json
@@ -2,11 +2,15 @@
"expo": {
"name": "Bluesky",
"slug": "bluesky",
+ "scheme": "bluesky",
"owner": "blueskysocial",
- "version": "1.18.0",
+ "version": "1.32.0",
+ "runtimeVersion": {
+ "policy": "appVersion"
+ },
"orientation": "portrait",
"icon": "./assets/icon.png",
- "userInterfaceStyle": "light",
+ "userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/cloud-splash.png",
"resizeMode": "cover",
@@ -26,27 +30,51 @@
],
"BGTaskSchedulerPermittedIdentifiers": [
"com.transistorsoft.fetch"
- ]
- }
+ ],
+ "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": 3,
+ "versionCode": 18,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
- "package": "xyz.blueskyweb.app"
+ "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",
"react-native-background-fetch",
+ "sentry-expo",
[
"expo-build-properties",
{
@@ -66,6 +94,17 @@
"eas": {
"projectId": "55bd077a-d905-4184-9c7f-94789ba0f302"
}
+ },
+ "hooks": {
+ "postPublish": [
+ {
+ "file": "sentry-expo/upload-sourcemaps",
+ "config": {
+ "organization": "blueskyweb",
+ "project": "react-native"
+ }
+ }
+ ]
}
}
}
diff --git a/bskyweb/.gitignore b/bskyweb/.gitignore
index b2a31beb38..1d945e1dab 100644
--- a/bskyweb/.gitignore
+++ b/bskyweb/.gitignore
@@ -4,5 +4,11 @@ test-coverage.out
# Don't check in the binary.
/bskyweb
+# Don't accidentally commit JS-generated code
+static/js/*.js
+static/js/*.map
+static/js/*.js.LICENSE.txt
+templates/scripts.html
+
# Don't ignore this file
!.gitignore
diff --git a/bskyweb/Makefile b/bskyweb/Makefile
index 957f043f52..7561a14544 100644
--- a/bskyweb/Makefile
+++ b/bskyweb/Makefile
@@ -39,4 +39,4 @@ check: ## Compile everything, checking syntax (does not output binaries)
.PHONY: run-dev-bskyweb
run-dev-bskyweb: .env ## Runs 'bskyweb' for local dev
- GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve
+ GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve --debug
diff --git a/bskyweb/README.md b/bskyweb/README.md
index a74cda0d5d..d60647379b 100644
--- a/bskyweb/README.md
+++ b/bskyweb/README.md
@@ -2,7 +2,7 @@
### SPA Bundle (monolithic static javascript file)
-To build the SPA bundle (`bundle.web.js`), first get a Javascript development
+To build the SPA bundle (`bundle.web.js`), first get a JavaScript development
environment set up. Either follow the top-level README, or something quick
like:
diff --git a/bskyweb/cmd/bskyweb/.gitignore b/bskyweb/cmd/bskyweb/.gitignore
new file mode 100644
index 0000000000..c810652a10
--- /dev/null
+++ b/bskyweb/cmd/bskyweb/.gitignore
@@ -0,0 +1 @@
+/bskyweb
diff --git a/bskyweb/cmd/bskyweb/mailmodo.go b/bskyweb/cmd/bskyweb/mailmodo.go
index 67ee6a2d68..e892971f9c 100644
--- a/bskyweb/cmd/bskyweb/mailmodo.go
+++ b/bskyweb/cmd/bskyweb/mailmodo.go
@@ -14,13 +14,15 @@ type Mailmodo struct {
httpClient *http.Client
APIKey string
BaseURL string
+ ListName string
}
-func NewMailmodo(apiKey string) *Mailmodo {
+func NewMailmodo(apiKey, listName string) *Mailmodo {
return &Mailmodo{
APIKey: apiKey,
BaseURL: "https://api.mailmodo.com/api/v1",
httpClient: &http.Client{},
+ ListName: listName,
}
}
@@ -56,9 +58,9 @@ func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod str
return nil
}
-func (m *Mailmodo) AddToList(ctx context.Context, listName, email string) error {
+func (m *Mailmodo) AddToList(ctx context.Context, email string) error {
return m.request(ctx, "POST", "addToList", map[string]any{
- "listName": listName,
+ "listName": m.ListName,
"email": email,
"data": map[string]any{
"email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))),
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index 0c8e9f8d2a..e8a71dfe47 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -2,11 +2,17 @@ package main
import (
"context"
+ "encoding/json"
+ "errors"
"fmt"
"io/fs"
+ "io/ioutil"
"net/http"
"os"
+ "os/signal"
"strings"
+ "syscall"
+ "time"
comatproto "github.com/bluesky-social/indigo/api/atproto"
appbsky "github.com/bluesky-social/indigo/api/bsky"
@@ -15,13 +21,18 @@ import (
"github.com/bluesky-social/social-app/bskyweb"
"github.com/flosch/pongo2/v6"
+ "github.com/klauspost/compress/gzhttp"
+ "github.com/klauspost/compress/gzip"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/urfave/cli/v2"
)
type Server struct {
- xrpcc *xrpc.Client
+ echo *echo.Echo
+ httpd *http.Server
+ mailmodo *Mailmodo
+ xrpcc *xrpc.Client
}
func serve(cctx *cli.Context) error {
@@ -33,8 +44,11 @@ func serve(cctx *cli.Context) error {
mailmodoAPIKey := cctx.String("mailmodo-api-key")
mailmodoListName := cctx.String("mailmodo-list-name")
+ // Echo
+ e := echo.New()
+
// Mailmodo client.
- mailmodo := NewMailmodo(mailmodoAPIKey)
+ mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName)
// create a new session
// TODO: does this work with no auth at all?
@@ -47,7 +61,7 @@ func serve(cctx *cli.Context) error {
}
auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{
- Identifier: &xrpcc.Auth.Handle,
+ Identifier: xrpcc.Auth.Handle,
Password: atpPassword,
})
if err != nil {
@@ -58,10 +72,76 @@ func serve(cctx *cli.Context) error {
xrpcc.Auth.Did = auth.Did
xrpcc.Auth.Handle = auth.Handle
- server := Server{xrpcc}
+ // httpd
+ var (
+ httpTimeout = 2 * time.Minute
+ httpMaxHeaderBytes = 2 * (1024 * 1024)
+ gzipMinSizeBytes = 1024 * 2
+ gzipCompressionLevel = gzip.BestSpeed
+ gzipExceptMIMETypes = []string{"image/png"}
+ )
+ // Wrap the server handler in a gzip handler to compress larger responses.
+ gzipHandler, err := gzhttp.NewWrapper(
+ gzhttp.MinSize(gzipMinSizeBytes),
+ gzhttp.CompressionLevel(gzipCompressionLevel),
+ gzhttp.ExceptContentTypes(gzipExceptMIMETypes),
+ )
+ if err != nil {
+ return err
+ }
+
+ //
+ // server
+ //
+ server := &Server{
+ echo: e,
+ mailmodo: mailmodo,
+ xrpcc: xrpcc,
+ }
+
+ // Create the HTTP server.
+ server.httpd = &http.Server{
+ Handler: gzipHandler(server),
+ Addr: httpAddress,
+ WriteTimeout: httpTimeout,
+ ReadTimeout: httpTimeout,
+ MaxHeaderBytes: httpMaxHeaderBytes,
+ }
+
+ e.HideBanner = true
+ // SECURITY: Do not modify without due consideration.
+ e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
+ ContentTypeNosniff: "nosniff",
+ XFrameOptions: "SAMEORIGIN",
+ HSTSMaxAge: 31536000, // 365 days
+ // TODO:
+ // ContentSecurityPolicy
+ // XSSProtection
+ }))
+ e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
+ // Don't log requests for static content.
+ Skipper: func(c echo.Context) bool {
+ return strings.HasPrefix(c.Request().URL.Path, "/static")
+ },
+ }))
+ e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug)
+ e.HTTPErrorHandler = server.errorHandler
+
+ // redirect trailing slash to non-trailing slash.
+ // all of our current endpoints have no trailing slash.
+ e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{
+ RedirectCode: http.StatusFound,
+ }))
+
+ //
+ // configure routes
+ //
+
+ // static files
staticHandler := http.FileServer(func() http.FileSystem {
if debug {
+ log.Debugf("serving static file from the local file system")
return http.FS(os.DirFS("static"))
}
fsys, err := fs.Sub(bskyweb.StaticFS, "static")
@@ -70,28 +150,28 @@ func serve(cctx *cli.Context) error {
}
return http.FS(fsys)
}())
-
- e := echo.New()
- e.HideBanner = true
- e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
- // Don't log requests for static content.
- Skipper: func(c echo.Context) bool {
- return strings.HasPrefix(c.Request().URL.Path, "/static")
- },
- Format: "method=${method} path=${uri} status=${status} latency=${latency_human}\n",
- }))
- e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug)
- e.HTTPErrorHandler = customHTTPErrorHandler
-
- // configure routes
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)))
+ e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
+ e.GET("/security.txt", func(c echo.Context) error {
+ return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
+ })
+
+ // home
e.GET("/", server.WebHome)
// generic routes
e.GET("/search", server.WebGeneric)
+ e.GET("/search/feeds", server.WebGeneric)
+ e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric)
+ e.GET("/moderation", server.WebGeneric)
+ e.GET("/moderation/mute-lists", server.WebGeneric)
+ e.GET("/moderation/muted-accounts", server.WebGeneric)
+ e.GET("/moderation/blocked-accounts", server.WebGeneric)
e.GET("/settings", server.WebGeneric)
+ e.GET("/settings/app-passwords", server.WebGeneric)
+ e.GET("/settings/saved-feeds", server.WebGeneric)
e.GET("/sys/debug", server.WebGeneric)
e.GET("/sys/log", server.WebGeneric)
e.GET("/support", server.WebGeneric)
@@ -104,6 +184,9 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handle", server.WebProfile)
e.GET("/profile/:handle/follows", server.WebGeneric)
e.GET("/profile/:handle/followers", server.WebGeneric)
+ e.GET("/profile/:handle/lists/:rkey", server.WebGeneric)
+ e.GET("/profile/:handle/feed/:rkey", server.WebGeneric)
+ e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric)
// post endpoints; only first populates info
e.GET("/profile/:handle/post/:rkey", server.WebPost)
@@ -111,19 +194,54 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric)
// Mailmodo
- e.POST("/waitlist", func(c echo.Context) error {
- email := strings.TrimSpace(c.FormValue("email"))
- if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, email); err != nil {
- return err
- }
- return c.JSON(http.StatusOK, map[string]bool{"success": true})
- })
+ e.POST("/api/waitlist", server.apiWaitlist)
+ // Start the server.
log.Infof("starting server address=%s", httpAddress)
- return e.Start(httpAddress)
+ go func() {
+ if err := server.httpd.ListenAndServe(); err != nil {
+ if !errors.Is(err, http.ErrServerClosed) {
+ log.Errorf("HTTP server shutting down unexpectedly: %s", err)
+ }
+ }
+ }()
+
+ // Wait for a signal to exit.
+ log.Info("registering OS exit signal handler")
+ quit := make(chan struct{})
+ exitSignals := make(chan os.Signal, 1)
+ signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM)
+ go func() {
+ sig := <-exitSignals
+ log.Infof("received OS exit signal: %s", sig)
+
+ // Shut down the HTTP server.
+ if err := server.Shutdown(); err != nil {
+ log.Errorf("HTTP server shutdown error: %s", err)
+ }
+
+ // Trigger the return that causes an exit.
+ close(quit)
+ }()
+ <-quit
+ log.Infof("graceful shutdown complete")
+ return nil
}
-func customHTTPErrorHandler(err error, c echo.Context) {
+func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ srv.echo.ServeHTTP(rw, req)
+}
+
+func (srv *Server) Shutdown() error {
+ log.Info("shutting down")
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ return srv.httpd.Shutdown(ctx)
+}
+
+func (srv *Server) errorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
@@ -167,7 +285,13 @@ func (srv *Server) WebPost(c echo.Context) error {
if err != nil {
log.Warnf("failed to fetch post: %s\t%v", uri, err)
} else {
- data["postView"] = tpv.Thread.FeedDefs_ThreadViewPost.Post
+ req := c.Request()
+ postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
+ data["postView"] = postView
+ data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
+ if postView.Embed != nil && postView.Embed.EmbedImages_View != nil {
+ data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb
+ }
}
}
@@ -185,9 +309,44 @@ func (srv *Server) WebProfile(c echo.Context) error {
if err != nil {
log.Warnf("failed to fetch handle: %s\t%v", handle, err)
} else {
+ req := c.Request()
data["profileView"] = pv
+ data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
}
}
return c.Render(http.StatusOK, "profile.html", data)
}
+
+func (srv *Server) apiWaitlist(c echo.Context) error {
+ type jsonError struct {
+ Error string `json:"error"`
+ }
+
+ // Read the API request.
+ type apiRequest struct {
+ Email string `json:"email"`
+ }
+
+ bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024)
+ payload, err := ioutil.ReadAll(bodyReader)
+ if err != nil {
+ return err
+ }
+ var req apiRequest
+ if err := json.Unmarshal(payload, &req); err != nil {
+ return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"})
+ }
+
+ if req.Email == "" {
+ return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."})
+ }
+
+ if err := srv.mailmodo.AddToList(c.Request().Context(), req.Email); err != nil {
+ log.Errorf("adding email to waitlist failed: %s", err)
+ return c.JSON(http.StatusBadRequest, jsonError{
+ Error: "Storing email in waitlist failed. Please enter a valid email address.",
+ })
+ }
+ return c.JSON(http.StatusOK, map[string]bool{"success": true})
+}
diff --git a/bskyweb/go.mod b/bskyweb/go.mod
index 9014fa1b55..5f06bfc459 100644
--- a/bskyweb/go.mod
+++ b/bskyweb/go.mod
@@ -3,18 +3,19 @@ module github.com/bluesky-social/social-app/bskyweb
go 1.20
require (
- github.com/bluesky-social/indigo v0.0.0-20230403211508-3cb4320bd5c8
+ github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319
github.com/flosch/pongo2/v6 v6.0.0
github.com/ipfs/go-log v1.0.5
github.com/joho/godotenv v1.5.1
+ github.com/klauspost/compress v1.16.5
github.com/labstack/echo/v4 v4.10.2
- github.com/urfave/cli/v2 v2.25.1
+ github.com/urfave/cli/v2 v2.25.3
)
require (
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
- github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
@@ -26,7 +27,7 @@ require (
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-block-format v0.1.2 // indirect
- github.com/ipfs/go-cid v0.4.0 // indirect
+ github.com/ipfs/go-cid v0.4.1 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-ipfs-blockstore v1.3.0 // indirect
github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect
@@ -68,22 +69,22 @@ require (
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
- github.com/whyrusleeping/cbor-gen v0.0.0-20230331140348-1f892b517e70 // indirect
+ github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0 // indirect
github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
- go.opentelemetry.io/otel v1.14.0 // indirect
- go.opentelemetry.io/otel/trace v1.14.0 // indirect
- go.uber.org/atomic v1.10.0 // indirect
+ go.opentelemetry.io/otel v1.15.1 // indirect
+ go.opentelemetry.io/otel/trace v1.15.1 // indirect
+ go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.24.0 // indirect
- golang.org/x/crypto v0.7.0 // indirect
- golang.org/x/net v0.8.0 // indirect
- golang.org/x/sys v0.6.0 // indirect
- golang.org/x/text v0.8.0 // indirect
+ golang.org/x/crypto v0.8.0 // indirect
+ golang.org/x/net v0.9.0 // indirect
+ golang.org/x/sys v0.7.0 // indirect
+ golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
gorm.io/driver/postgres v1.5.0 // indirect
- gorm.io/driver/sqlite v1.4.4 // indirect
- gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 // indirect
+ gorm.io/driver/sqlite v1.5.0 // indirect
+ gorm.io/gorm v1.25.0 // indirect
lukechampine.com/blake3 v1.1.7 // indirect
)
diff --git a/bskyweb/go.sum b/bskyweb/go.sum
index 9155dc34d4..ae5d7defb5 100644
--- a/bskyweb/go.sum
+++ b/bskyweb/go.sum
@@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-github.com/bluesky-social/indigo v0.0.0-20230403211508-3cb4320bd5c8 h1:6A8D48CksjnlmJb8r5WaFAB4J2osllkmQoQhhiREMHw=
-github.com/bluesky-social/indigo v0.0.0-20230403211508-3cb4320bd5c8/go.mod h1:9dcnKLtEnDPBdWm5/BLJHvbaPndCdKzAaBK7sbGt3S4=
+github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 h1:VCNXRXpgyK3xkaQ8fzL5WzswerwLycke4B9ggLs1uOA=
+github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319/go.mod h1:Hc09SUJXAIujaAvq7JXxi8ZQQI887grzPkHgn4JyE1Q=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -12,8 +12,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -55,8 +56,8 @@ github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUP
github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog=
github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I=
-github.com/ipfs/go-cid v0.4.0 h1:a4pdZq0sx6ZSxbCizebnKiMCx/xI/aBBFlB73IgH4rA=
-github.com/ipfs/go-cid v0.4.0/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
+github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
+github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk=
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
@@ -96,7 +97,6 @@ github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -105,6 +105,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
+github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
@@ -209,8 +211,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw=
-github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
+github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
+github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
@@ -218,8 +220,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
-github.com/whyrusleeping/cbor-gen v0.0.0-20230331140348-1f892b517e70 h1:iNBzUKTsJc9RqStEVX2VYgVHATTU39IuB7g0e8OPWXU=
-github.com/whyrusleeping/cbor-gen v0.0.0-20230331140348-1f892b517e70/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ=
+github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0 h1:XYEgH2nJgsrcrj32p+SAbx6T3s/6QknOXezXtz7kzbg=
+github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ=
github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220 h1:EO/9z3yDvx1van1/0esdcqhalZZQGRj3I1BPTWr5k3A=
github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220/go.mod h1:qPtRyexGM5XMHFIfjH+EiA/A/1n2JakWEdMPC53pJAE=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
@@ -228,14 +230,14 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
-go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
-go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
-go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
+go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8=
+go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc=
+go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY=
+go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
-go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@@ -255,8 +257,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -273,8 +276,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -297,8 +301,9 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -307,8 +312,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -343,11 +349,11 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
-gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
-gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
-gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
-gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g=
+gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
+gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
+gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
diff --git a/bskyweb/static/.well-known/apple-app-site-association b/bskyweb/static/.well-known/apple-app-site-association
new file mode 100644
index 0000000000..232acdf255
--- /dev/null
+++ b/bskyweb/static/.well-known/apple-app-site-association
@@ -0,0 +1,13 @@
+{
+ "applinks": {
+ "apps": [],
+ "details": [
+ {
+ "appID": "B3LX46C5HS.xyz.blueskyweb.app",
+ "paths": [
+ "*"
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/bskyweb/static/.well-known/assetlinks.json b/bskyweb/static/.well-known/assetlinks.json
new file mode 100644
index 0000000000..5ca12d5b31
--- /dev/null
+++ b/bskyweb/static/.well-known/assetlinks.json
@@ -0,0 +1,11 @@
+[
+ {
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "xyz.blueskyweb.app",
+ "sha256_cert_fingerprints":
+ ["C1:4D:3C:6B:B5:D6:D9:AE:CF:C5:0B:BC:C1:9B:29:6D:D4:E6:87:46:36:D5:4C:1A:64:1C:14:08:BF:7E:F9:62", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
+ }
+ }
+]
diff --git a/bskyweb/static/.well-known/security.txt b/bskyweb/static/.well-known/security.txt
new file mode 100644
index 0000000000..8173cb72d6
--- /dev/null
+++ b/bskyweb/static/.well-known/security.txt
@@ -0,0 +1,4 @@
+Contact: mailto:security@bsky.app
+Preferred-Languages: en
+Canonical: https://bsky.app/.well-known/security.txt
+Acknowledgements: https://github.com/bluesky-social/atproto/blob/main/CONTRIBUTORS.md
diff --git a/bskyweb/static/robots.txt b/bskyweb/static/robots.txt
index d3475984e8..4f8510d18d 100644
--- a/bskyweb/static/robots.txt
+++ b/bskyweb/static/robots.txt
@@ -1 +1,9 @@
-# hello friends!
+# Hello Friends!
+# If you are considering bulk or automated crawling, you may want to look in
+# to our protocol (API), including a firehose of updates. See: https://atproto.com/
+
+# By default, may crawl anything on this domain. HTTP 429 ("backoff") status
+# codes are used for rate-limiting. Up to a handful concurrent requests should
+# be ok.
+User-Agent: *
+Allow: /
diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html
index 28b92958ef..b5ed329cd0 100644
--- a/bskyweb/templates/base.html
+++ b/bskyweb/templates/base.html
@@ -1,9 +1,8 @@
-
-
-
+
+
{%- block head_title -%}Bluesky{%- endblock -%}
@@ -57,10 +56,6 @@
}
}*/
- /* Remove focus state on inputs */
- *:focus {
- outline: 0;
- }
/* Remove default link styling */
a {
color: inherit;
@@ -68,6 +63,9 @@
a[role="link"]:hover {
text-decoration: underline;
}
+ a[role="link"][data-no-underline="1"]:hover {
+ text-decoration: none;
+ }
/* Styling hacks */
*[data-word-wrap] {
@@ -99,58 +97,35 @@
color: #0085ff;
cursor: pointer;
}
+ /* OLLIE: TODO -- this is not accessible */
+ /* Remove focus state on inputs */
+ .ProseMirror-focused {
+ outline: 0;
+ }
+ textarea:focus,
+ input:focus {
+ outline: 0;
+ }
.tippy-content .items {
- border-radius: 6px;
- background: #F3F3F8;
- border: 1px solid #e0d9d9;
- padding: 3px 3px;
- }
- .tippy-content .items .item {
- display: block;
- background: transparent;
- color: #8a8c9a;
- border: 0;
- font: 17px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- padding: 7px 10px 8px;
- width: 100%;
- text-align: left;
- box-sizing: border-box;
- letter-spacing: 0.2px;
- }
- .tippy-content .items .item.is-selected {
- background: #fff;
- border-radius: 4px;
- color: #333;
+ width: fit-content;
}
{% include "scripts.html" %}
-
-
-
- {% block head_page_meta -%}
-
-
-
-
-
-
-
-
- {%- endblock %}
-
-
-
+
+
+
+ {% block html_head_extra -%}{%- endblock %}
+
- {% block head_metadata %}{% endblock %}
{%- block body_all %}
- {%- block noscript_extra %}{% endblock -%}
- Javascript Required
- This is a heavily interactive web application, and Javascript is required. Simple HTML interfaces are possible, but that is not what this is.
+
JavaScript Required
+ This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
Learn more about Bluesky at blueskyweb.xyz and atproto.com .
+ {% block noscript_extra %}{% endblock %}
{% endblock -%}
diff --git a/bskyweb/templates/home.html b/bskyweb/templates/home.html
index 631f281c01..7beea49d9f 100644
--- a/bskyweb/templates/home.html
+++ b/bskyweb/templates/home.html
@@ -2,6 +2,16 @@
{% block head_title %}Bluesky{% endblock %}
+{% block html_head_extra -%}
+
+
+
+
+
+
+
+{%- endblock %}
+
{% block noscript_extra %}
This is the home page.
{% endblock %}
diff --git a/bskyweb/templates/post.html b/bskyweb/templates/post.html
index a24f64ab5a..05d62a1c8f 100644
--- a/bskyweb/templates/post.html
+++ b/bskyweb/templates/post.html
@@ -1,25 +1,48 @@
{% extends "base.html" %}
-{% block head_page_meta -%}
-
+{% block head_title %}
{%- if postView -%}
-
-
- {%- if postView.Author.DisplayName -%}
-
-
- {%- else -%}
-
-
- {%- endif -%}
- {%- if postView.Record.Text -%}
-
-
- {%- endif -%}
+ @{{ postView.Author.Handle }} on Bluesky
+{%- else -%}
+ Bluesky
{%- endif -%}
+{% endblock %}
+
+{% block html_head_extra -%}
+{%- if postView -%}
+
+
+ {%- if requestURI %}
+
+ {% endif -%}
+ {%- if postView.Author.DisplayName %}
+
+ {% else %}
+
+ {% endif -%}
+ {%- if postView.Record.Val.Text %}
+
+
+ {% endif -%}
+ {%- if imgThumbUrl %}
+
+
+ {%- elif postView.Author.Avatar %}
+ {# Don't use avatar image in cards; usually looks bad #}
+
+ {% endif %}
+
+
+
+{% endif -%}
{%- endblock %}
{% block noscript_extra -%}
-
{{ postView.Author.DisplayName }} / {{ postView.Author.Handle }}
-
{{ postView.Record.Text }}
+
+
Post
+
{{ postView.Author.DisplayName }}
+
{{ postView.Author.Handle }}
+
{{ postView.Author.Did }}
+
{{ postView.Record.Text }}
+
{%- endblock %}
diff --git a/bskyweb/templates/profile.html b/bskyweb/templates/profile.html
index 260f211a66..4d4f679466 100644
--- a/bskyweb/templates/profile.html
+++ b/bskyweb/templates/profile.html
@@ -1,25 +1,48 @@
{% extends "base.html" %}
-{% block head_page_meta -%}
-
+{% block head_title %}
{%- if profileView -%}
-
-
- {%- if profileView.DisplayName -%}
-
-
- {%- else -%}
-
-
- {%- endif -%}
-
- {%- if profileView.Avatar -%}
-
- {%- endif -%}
+ @{{ profileView.Handle }} on Bluesky
+{%- else -%}
+ Bluesky
{%- endif -%}
+{% endblock %}
+
+{% block html_head_extra -%}
+{%- if profileView -%}
+
+
+ {%- if requestURI %}
+
+ {% endif -%}
+ {%- if profileView.DisplayName %}
+
+ {% else %}
+
+ {% endif -%}
+ {%- if profileView.Description %}
+
+
+ {% endif -%}
+ {%- if profileView.Banner %}
+
+
+ {%- elif profileView.Avatar -%}
+ {# Don't use avatar image in cards; usually looks bad #}
+
+ {% endif %}
+
+
+
+{% endif -%}
{%- endblock %}
{% block noscript_extra -%}
-{{ profileView.DisplayName }} / {{ profileView.Handle }}
-
{{ profileView.Description }}
+
+
Profile
+
{{ profileView.DisplayName }}
+
{{ profileView.Handle }}
+
{{ profileView.Did }}
+
{{ profileView.Description }}
+
{%- endblock %}
diff --git a/docs/build.md b/docs/build.md
new file mode 100644
index 0000000000..318f20bbd0
--- /dev/null
+++ b/docs/build.md
@@ -0,0 +1,122 @@
+# Build instructions
+
+## App Build
+
+- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
+- Setup your environment [for e2e testing using detox](https://wix.github.io/Detox/docs/introduction/getting-started):
+ - yarn global add detox-cli
+ - brew tap wix/brew
+ - brew install applesimutils
+- After initial setup:
+ - `npx expo prebuild` -> you will also need to run this anytime `app.json` or `package.json` changes
+- Start the dev servers
+ - `git clone git@github.com:bluesky-social/atproto.git`
+ - `cd atproto`
+ - `yarn`
+ - `cd packages/dev-env && yarn start`
+- Run the dev app
+ - iOS: `yarn ios`
+ - Android: `yarn android`
+ - Web: `yarn web`
+- If you are cloning or forking this repo as an open source developer, please check the tips below as well
+- Run e2e tests
+ - Start in various console tabs:
+ - `yarn e2e:mock-server`
+ - `yarn e2e:metro`
+ - 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
+ - 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!
+
+## Go-Server Build
+
+### Prerequisites
+
+- [Go](https://go.dev/)
+- [Yarn](https://yarnpkg.com/)
+
+### Steps
+
+To run the build with Go, use staging credentials, your own, or any other account you create.
+
+```
+cd social-app
+yarn && yarn build-web
+cp ./web-build/static/js/*.* bskyweb/static/js/
+cd bskyweb/
+go mod tidy
+go build -v -tags timetzdata -o bskyweb ./cmd/bskyweb
+./bskyweb serve --pds-host=https://staging.bsky.dev --handle= --password=
+```
+
+On build success, access the application at [http://localhost:8100/](http://localhost:8100/). Subsequent changes require re-running the above steps in order to be reflected.
+
+## Various notes
+
+### Debugging
+
+- Note that since 0.70, debugging using the old debugger (which shows up using CMD+D) doesn't work anymore. Follow the instructions below to debug the code: https://reactnative.dev/docs/next/hermes#debugging-js-on-hermes-using-google-chromes-devtools
+
+### Developer Menu
+
+To open the [Developer Menu](https://docs.expo.dev/debugging/tools/#developer-menu) on an `expo-dev-client` app you can do the following:
+
+- Android Device: Shake the device vertically, or if your device is connected via USB, run adb shell input keyevent 82 in your terminal
+- Android Emulator: Either press Cmd ⌘ + m or Ctrl + m or run adb shell input keyevent 82 in your terminal
+- iOS Device: Shake the device, or touch 3 fingers to the screen
+- iOS Simulator: Press Ctrl + Cmd ⌘ + z on a Mac in the emulator to simulate the shake gesture, or press Cmd ⌘ + d
+
+### Running E2E Tests
+
+- Make sure you've setup your environment following above
+- Make sure Metro and the dev server are running
+- Run `yarn e2e`
+- Find the artifacts in the `artifact` folder
+
+### Polyfills
+
+`./platform/polyfills.*.ts` adds polyfills to the environment. Currently this includes:
+
+- TextEncoder / TextDecoder
+
+### Sentry sourcemaps
+
+Sourcemaps should automatically be updated when a signed build is created using `eas build` and published using `eas submit` due to the postPublish hook setup in `app.json`. However, if an update is created and published OTA using `eas update`, we need to the take the following steps to upload sourcemaps to Sentry:
+
+- Run eas update. This will generate a dist folder in your project root, which contains your JavaScript bundles and source maps. This command will also output the 'Android update ID' and 'iOS update ID' that we'll need in the next step.
+- Copy or rename the bundle names in the `dist/bundles` folder to match `index.android.bundle` (Android) or `main.jsbundle` (iOS).
+- Next, you can use the Sentry CLI to upload your bundles and source maps:
+ - release name should be set to `${bundleIdentifier}@${version}+${buildNumber}` (iOS) or `${androidPackage}@${version}+${versionCode}` (Android), so for example `com.domain.myapp@1.0.0+1`.
+ - `dist` should be set to the Update ID that `eas update` generated.
+- Command for Android:
+ `node_modules/@sentry/cli/bin/sentry-cli releases \
+files \
+upload-sourcemaps \
+--dist \
+--rewrite \
+dist/bundles/index.android.bundle dist/bundles/android-.map`
+- Command for iOS:
+ `node_modules/@sentry/cli/bin/sentry-cli releases \
+files \
+upload-sourcemaps \
+--dist \
+--rewrite \
+dist/bundles/main.jsbundle dist/bundles/ios-.map`
+
+### OTA updates
+To create OTA updates, run `eas update` along with the `--branch` flag to indicate which branch you want to push the update to, and the `--message` flag to indicate a message for yourself and your team that shows up on https://expo.dev. ALl the channels (which make up the options for the `--branch` flag) are given in `eas.json`. [See more here](https://docs.expo.dev/eas-update/getting-started/)
+
+The clients which can receive an OTA update is governed by the `runtimeVersion` property in `app.json`. Right now, it is set so that only apps with the same `appVersion` (same as `version` property in `app.json`) can receive the update and install it. However, we can manually set `"runtimeVersion": "1.34.0"` or anything along those lines as well. This is useful if very little native code changes from update-to-update. If we are manually setting `runtimeVersion`, we should increment the version each time native code is changed. [See more here](https://docs.expo.dev/eas-update/runtime-versions/)
diff --git a/eas.json b/eas.json
index 93db51cbea..60c8be378f 100644
--- a/eas.json
+++ b/eas.json
@@ -8,6 +8,7 @@
"developmentClient": true,
"distribution": "internal",
"ios": {
+ "simulator": true,
"resourceClass": "medium"
},
"channel": "development"
diff --git a/jest/test-pds.ts b/jest/test-pds.ts
index 649638989e..3c14c55314 100644
--- a/jest/test-pds.ts
+++ b/jest/test-pds.ts
@@ -2,6 +2,7 @@ import {AddressInfo} from 'net'
import os from 'os'
import net from 'net'
import path from 'path'
+import fs from 'fs'
import * as crypto from '@atproto/crypto'
import {PDS, ServerConfig, Database, MemoryBlobStore} from '@atproto/pds'
import * as plc from '@did-plc/lib'
@@ -12,6 +13,7 @@ const ADMIN_PASSWORD = 'admin-pass'
const SECOND = 1000
const MINUTE = SECOND * 60
const HOUR = MINUTE * 60
+const DAY = HOUR * 24
export interface TestUser {
email: string
@@ -65,6 +67,8 @@ export async function createServer(
adminPassword: ADMIN_PASSWORD,
inviteRequired,
didPlcUrl: plcUrl,
+ didCacheMaxTTL: DAY,
+ didCacheStaleTTL: HOUR,
jwtSecret: 'jwt-secret',
availableUserDomains: ['.test'],
appUrlPasswordReset: 'app://forgot-password',
@@ -104,9 +108,13 @@ export async function createServer(
await pds.start()
const pdsUrl = `http://localhost:${port}`
+ const profilePic = fs.readFileSync(
+ path.join(__dirname, '..', 'assets', 'default-avatar.jpg'),
+ )
+
return {
pdsUrl,
- mocker: new Mocker(pdsUrl),
+ mocker: new Mocker(pds, pdsUrl, profilePic),
async close() {
await pds.destroy()
await plcServer.destroy()
@@ -118,7 +126,11 @@ class Mocker {
agent: BskyAgent
users: Record = {}
- constructor(public service: string) {
+ constructor(
+ public pds: PDS,
+ public service: string,
+ public profilePic: Uint8Array,
+ ) {
this.agent = new BskyAgent({service})
}
@@ -152,6 +164,15 @@ class Mocker {
handle: name + '.test',
password: 'hunter2',
})
+ await agent.upsertProfile(async () => {
+ const blob = await agent.uploadBlob(this.profilePic, {
+ encoding: 'image/jpeg',
+ })
+ return {
+ displayName: name,
+ avatar: blob.data.blob,
+ }
+ })
this.users[name] = {
did: res.data.did,
email,
@@ -192,6 +213,159 @@ class Mocker {
await this.follow('carla', 'alice')
await this.follow('carla', 'bob')
}
+
+ async createPost(user: string, text: string) {
+ const agent = this.users[user]?.agent
+ if (!agent) {
+ throw new Error(`Not a user: ${user}`)
+ }
+ return await agent.post({
+ text,
+ createdAt: new Date().toISOString(),
+ })
+ }
+
+ async createQuotePost(
+ user: string,
+ text: string,
+ {uri, cid}: {uri: string; cid: string},
+ ) {
+ const agent = this.users[user]?.agent
+ if (!agent) {
+ throw new Error(`Not a user: ${user}`)
+ }
+ return await agent.post({
+ text,
+ embed: {$type: 'app.bsky.embed.record', record: {uri, cid}},
+ createdAt: new Date().toISOString(),
+ })
+ }
+
+ async createReply(
+ user: string,
+ text: string,
+ {uri, cid}: {uri: string; cid: string},
+ ) {
+ const agent = this.users[user]?.agent
+ if (!agent) {
+ throw new Error(`Not a user: ${user}`)
+ }
+ return await agent.post({
+ text,
+ reply: {root: {uri, cid}, parent: {uri, cid}},
+ createdAt: new Date().toISOString(),
+ })
+ }
+
+ async like(user: string, {uri, cid}: {uri: string; cid: string}) {
+ const agent = this.users[user]?.agent
+ if (!agent) {
+ throw new Error(`Not a user: ${user}`)
+ }
+ return await agent.like(uri, cid)
+ }
+
+ async labelAccount(label: string, user: string) {
+ const did = this.users[user]?.did
+ if (!did) {
+ throw new Error(`Invalid user: ${user}`)
+ }
+ const ctx = this.pds.ctx
+ if (!ctx) {
+ throw new Error('Invalid PDS')
+ }
+
+ await ctx.db.db
+ .insertInto('label')
+ .values([
+ {
+ src: ctx.cfg.labelerDid,
+ uri: did,
+ cid: '',
+ val: label,
+ neg: 0,
+ cts: new Date().toISOString(),
+ },
+ ])
+ .execute()
+ }
+
+ async labelProfile(label: string, user: string) {
+ const agent = this.users[user]?.agent
+ const did = this.users[user]?.did
+ if (!did) {
+ throw new Error(`Invalid user: ${user}`)
+ }
+
+ const profile = await agent.app.bsky.actor.profile.get({
+ repo: user + '.test',
+ rkey: 'self',
+ })
+
+ const ctx = this.pds.ctx
+ if (!ctx) {
+ throw new Error('Invalid PDS')
+ }
+ await ctx.db.db
+ .insertInto('label')
+ .values([
+ {
+ src: ctx.cfg.labelerDid,
+ uri: profile.uri,
+ cid: profile.cid,
+ val: label,
+ neg: 0,
+ cts: new Date().toISOString(),
+ },
+ ])
+ .execute()
+ }
+
+ async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) {
+ const ctx = this.pds.ctx
+ if (!ctx) {
+ throw new Error('Invalid PDS')
+ }
+ await ctx.db.db
+ .insertInto('label')
+ .values([
+ {
+ src: ctx.cfg.labelerDid,
+ uri,
+ cid,
+ val: label,
+ neg: 0,
+ cts: new Date().toISOString(),
+ },
+ ])
+ .execute()
+ }
+
+ async createMuteList(user: string, name: string): Promise {
+ const res = await this.users[user]?.agent.app.bsky.graph.list.create(
+ {repo: this.users[user]?.did},
+ {
+ purpose: 'app.bsky.graph.defs#modlist',
+ name,
+ createdAt: new Date().toISOString(),
+ },
+ )
+ await this.users[user]?.agent.app.bsky.graph.muteActorList({
+ list: res.uri,
+ })
+ return res.uri
+ }
+
+ async addToMuteList(owner: string, list: string, subject: string) {
+ await this.users[owner]?.agent.app.bsky.graph.listitem.create(
+ {repo: this.users[owner]?.did},
+ {
+ list,
+ subject,
+ createdAt: new Date().toISOString(),
+ },
+ )
+ }
}
const checkAvailablePort = (port: number) =>
diff --git a/package.json b/package.json
index 57a6d02ac0..22b894efde 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
- "version": "1.18.0",
+ "version": "1.32.0",
"private": true,
"scripts": {
"postinstall": "patch-package",
@@ -8,7 +8,7 @@
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
- "build-web": "expo export:web && node ./scripts/post-web-build.js",
+ "build-web": "expo export:web && node ./scripts/post-web-build.js && cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/",
"start": "expo start --dev-client",
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
"test": "jest --forceExit --testTimeout=20000 --bail",
@@ -16,21 +16,23 @@
"test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "jest --coverage",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
+ "typecheck": "tsc --project ./tsconfig.check.json",
"e2e:mock-server": "ts-node __e2e__/mock-server.ts",
"e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:build": "detox build -c ios.sim.debug",
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
},
"dependencies": {
- "@atproto/api": "0.2.7",
+ "@atproto/api": "0.3.8",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
+ "@expo/html-elements": "^0.4.2",
"@expo/webpack-config": "^18.0.1",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.0",
- "@gorhom/bottom-sheet": "^4",
+ "@gorhom/bottom-sheet": "^4.4.7",
"@mattermost/react-native-paste-input": "^0.6.0",
"@miblanchard/react-native-slider": "^2.2.0",
"@notifee/react-native": "^7.4.0",
@@ -38,6 +40,7 @@
"@react-native-camera-roll/camera-roll": "^5.2.2",
"@react-native-clipboard/clipboard": "^1.10.0",
"@react-native-community/blur": "^4.3.0",
+ "@react-native-community/datetimepicker": "6.7.3",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/drawer": "^6.6.2",
"@react-navigation/native": "^6.1.6",
@@ -46,8 +49,11 @@
"@segment/analytics-react": "^1.0.0-rc1",
"@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5",
+ "@sentry/react-native": "4.13.0",
"@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220",
+ "@tiptap/extension-hard-break": "^2.0.3",
+ "@tiptap/extension-history": "^2.0.3",
"@tiptap/extension-link": "^2.0.0-beta.220",
"@tiptap/extension-mention": "^2.0.0-beta.220",
"@tiptap/extension-paragraph": "^2.0.0-beta.220",
@@ -56,30 +62,35 @@
"@tiptap/pm": "^2.0.0-beta.220",
"@tiptap/react": "^2.0.0-beta.220",
"@tiptap/suggestion": "^2.0.0-beta.220",
+ "@types/node": "^18.16.2",
"@zxing/text-encoding": "^0.9.0",
"await-lock": "^2.2.2",
"base64-js": "^1.5.1",
"email-validator": "^2.0.4",
- "expo": "~48.0.11",
+ "eslint-plugin-react-native-a11y": "^3.3.0",
+ "expo": "~48.0.18",
+ "expo-application": "~5.1.1",
"expo-build-properties": "~0.5.1",
"expo-camera": "~13.2.1",
+ "expo-constants": "~14.2.1",
"expo-dev-client": "~2.1.1",
- "expo-image": "~1.0.0",
- "expo-image-picker": "~14.1.1",
+ "expo-device": "~5.2.1",
+ "expo-image": "^1.2.3",
+ "expo-image-manipulator": "^11.1.1",
+ "expo-image-picker": "^14.1.1",
"expo-localization": "~14.1.1",
"expo-media-library": "~15.2.3",
- "expo-splash-screen": "~0.18.1",
+ "expo-sharing": "~11.2.2",
+ "expo-splash-screen": "~0.18.2",
"expo-status-bar": "~1.4.4",
"expo-system-ui": "~2.2.1",
"expo-updates": "~0.16.4",
"fast-text-encoding": "^1.0.6",
"graphemer": "^1.4.0",
- "he": "^1.2.0",
"history": "^5.3.0",
"js-sha256": "^0.9.0",
"lande": "^1.0.10",
"lodash.chunk": "^4.2.0",
- "lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.omit": "^4.5.0",
@@ -97,9 +108,10 @@
"react-avatar-editor": "^13.0.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0",
- "react-native": "0.71.6",
+ "react-native": "0.71.8",
"react-native-appstate-hook": "^1.0.6",
"react-native-background-fetch": "^4.1.8",
+ "react-native-draggable-flatlist": "^4.0.1",
"react-native-drawer-layout": "^3.2.0",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.9.0",
@@ -110,7 +122,7 @@
"react-native-linear-gradient": "^2.6.2",
"react-native-pager-view": "6.1.4",
"react-native-progress": "bluesky-social/react-native-progress",
- "react-native-reanimated": "~2.14.4",
+ "react-native-reanimated": "^3.3.0",
"react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "^4.4.1",
"react-native-screens": "^3.13.1",
@@ -123,12 +135,13 @@
"react-native-web-linear-gradient": "^1.1.2",
"react-responsive": "^9.0.2",
"rn-fetch-blob": "^0.12.0",
+ "sentry-expo": "~6.1.0",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"zod": "^3.20.2"
},
"devDependencies": {
- "@atproto/pds": "^0.1.4",
+ "@atproto/pds": "^0.1.10",
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
@@ -140,7 +153,6 @@
"@types/he": "^1.1.2",
"@types/jest": "^29.4.0",
"@types/lodash.chunk": "^4.2.7",
- "@types/lodash.clonedeep": "^4.5.7",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.omit": "^4.5.7",
@@ -195,7 +207,7 @@
"node"
],
"transformIgnorePatterns": [
- "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg)"
+ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo)"
],
"modulePathIgnorePatterns": [
"__tests__/.*/__mocks__",
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 2e78a3d786..a02ca62c82 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -1,9 +1,10 @@
import 'react-native-url-polyfill/auto'
import React, {useState, useEffect} from 'react'
+import 'lib/sentry' // must be relatively on top
+import {withSentry} from 'lib/sentry'
import {Linking} from 'react-native'
import {RootSiblingParent} from 'react-native-root-siblings'
import * as SplashScreen from 'expo-splash-screen'
-import {SafeAreaProvider} from 'react-native-safe-area-context'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {observer} from 'mobx-react-lite'
import {ThemeProvider} from 'lib/ThemeContext'
@@ -16,6 +17,8 @@ import * as analytics from 'lib/analytics/analytics'
import * as Toast from './view/com/util/Toast'
import {handleLink} from './Navigation'
+SplashScreen.preventAutoHideAsync()
+
const App = observer(() => {
const [rootStore, setRootStore] = useState(
undefined,
@@ -48,14 +51,12 @@ const App = observer(() => {
return null
}
return (
-
+
-
-
-
+
@@ -64,4 +65,4 @@ const App = observer(() => {
)
})
-export default App
+export default withSentry(App)
diff --git a/src/App.web.tsx b/src/App.web.tsx
index c83ebb30bb..b0f949b8b7 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -1,4 +1,5 @@
import React, {useState, useEffect} from 'react'
+import 'lib/sentry' // must be relatively on top
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {RootSiblingParent} from 'react-native-root-siblings'
import * as view from './view/index'
@@ -29,7 +30,7 @@ const App = observer(() => {
}
return (
-
+
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index e868dd3b08..77e7cfa0be 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -1,15 +1,20 @@
import * as React from 'react'
import {StyleSheet} from 'react-native'
+import {observer} from 'mobx-react-lite'
import {
NavigationContainer,
createNavigationContainerRef,
+ CommonActions,
StackActions,
+ DefaultTheme,
+ DarkTheme,
} from '@react-navigation/native'
import {createNativeStackNavigator} from '@react-navigation/native-stack'
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
import {
HomeTabNavigatorParams,
SearchTabNavigatorParams,
+ FeedsTabNavigatorParams,
NotificationsTabNavigatorParams,
FlatNavigatorParams,
AllNavigatorParams,
@@ -23,15 +28,24 @@ import {colors} from 'lib/styles'
import {isNative} from 'platform/detection'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {router} from './routes'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useStores} from './state'
import {HomeScreen} from './view/screens/Home'
import {SearchScreen} from './view/screens/Search'
+import {FeedsScreen} from './view/screens/Feeds'
import {NotificationsScreen} from './view/screens/Notifications'
+import {ModerationScreen} from './view/screens/Moderation'
+import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists'
+import {DiscoverFeedsScreen} from 'view/screens/DiscoverFeeds'
import {NotFoundScreen} from './view/screens/NotFound'
import {SettingsScreen} from './view/screens/Settings'
import {ProfileScreen} from './view/screens/Profile'
import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
+import {CustomFeedScreen} from './view/screens/CustomFeed'
+import {CustomFeedLikedByScreen} from './view/screens/CustomFeedLikedBy'
+import {ProfileListScreen} from './view/screens/ProfileList'
import {PostThreadScreen} from './view/screens/PostThread'
import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
@@ -42,13 +56,18 @@ import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
import {TermsOfServiceScreen} from './view/screens/TermsOfService'
import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines'
import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from './state'
+import {AppPasswords} from 'view/screens/AppPasswords'
+import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
+import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
+import {SavedFeeds} from 'view/screens/SavedFeeds'
+import {getRoutingInstrumentation} from 'lib/sentry'
+import {bskyTitle} from 'lib/strings/headings'
const navigationRef = createNavigationContainerRef()
const HomeTab = createNativeStackNavigator()
const SearchTab = createNativeStackNavigator()
+const FeedsTab = createNativeStackNavigator()
const NotificationsTab =
createNativeStackNavigator()
const MyProfileTab = createNativeStackNavigator()
@@ -58,30 +77,140 @@ const Tab = createBottomTabNavigator()
/**
* These "common screens" are reused across stacks.
*/
-function commonScreens(Stack: typeof HomeTab) {
+function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
+ const title = (page: string) => bskyTitle(page, unreadCountLabel)
+
return (
<>
-
-
-
+
+
+
+
+
+
+
+ ({title: title(`@${route.params.name}`)})}
+ />
({
+ title: title(`People following @${route.params.name}`),
+ })}
+ />
+ ({
+ title: title(`People followed by @${route.params.name}`),
+ })}
+ />
+
+ ({title: title(`Post by @${route.params.name}`)})}
+ />
+ ({title: title(`Post by @${route.params.name}`)})}
+ />
+ ({title: title(`Post by @${route.params.name}`)})}
+ />
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
-
>
)
}
@@ -96,14 +225,15 @@ function TabsNavigator() {
+
+
-
)
@@ -143,6 +273,23 @@ function SearchTabNavigator() {
)
}
+function FeedsTabNavigator() {
+ const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
+ return (
+
+
+ {commonScreens(FeedsTab as typeof HomeTab)}
+
+ )
+}
+
function NotificationsTabNavigator() {
const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
return (
@@ -163,7 +310,7 @@ function NotificationsTabNavigator() {
)
}
-function MyProfileTabNavigator() {
+const MyProfileTabNavigator = observer(() => {
const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
const store = useStores()
return (
@@ -180,21 +327,23 @@ function MyProfileTabNavigator() {
// @ts-ignore // TODO: fix this broken type in ProfileScreen
component={ProfileScreen}
initialParams={{
- name: store.me.handle,
+ name: store.me.did,
hideBackButton: true,
}}
/>
{commonScreens(MyProfileTab as typeof HomeTab)}
)
-}
+})
/**
* The FlatNavigator is used by Web to represent the routes
* in a single ("flat") stack.
*/
-function FlatNavigator() {
+const FlatNavigator = observer(() => {
const pal = usePalette('default')
+ const unreadCountLabel = useStores().me.notifications.unreadCountLabel
+ const title = (page: string) => bskyTitle(page, unreadCountLabel)
return (
-
-
-
- {commonScreens(Flat as typeof HomeTab)}
+
+
+
+
+ {commonScreens(Flat as typeof HomeTab, unreadCountLabel)}
)
-}
+})
/**
* The RoutesContainer should wrap all components which need access
@@ -244,7 +410,16 @@ const LINKING = {
if (name === 'Notifications') {
return buildStateObject('NotificationsTab', 'Notifications', params)
}
- return buildStateObject('HomeTab', name, params)
+ if (name === 'Home') {
+ return buildStateObject('HomeTab', 'Home', params)
+ }
+ // if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work
+ return buildStateObject('HomeTab', name, params, [
+ {
+ name: 'Home',
+ params: {},
+ },
+ ])
} else {
return buildStateObject('Flat', name, params)
}
@@ -252,8 +427,19 @@ const LINKING = {
}
function RoutesContainer({children}: React.PropsWithChildren<{}>) {
+ const theme = useColorSchemeStyle(DefaultTheme, DarkTheme)
return (
-
+ {
+ // Register the navigation container with the Sentry instrumentation (only works on native)
+ if (isNative) {
+ const routingInstrumentation = getRoutingInstrumentation()
+ routingInstrumentation.registerNavigationContainer(navigationRef)
+ }
+ }}>
{children}
)
@@ -277,7 +463,20 @@ function navigate(
function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') {
if (navigationRef.isReady()) {
navigate(tabName)
- navigationRef.dispatch(StackActions.popToTop())
+ if (navigationRef.canGoBack()) {
+ navigationRef.dispatch(StackActions.popToTop()) //we need to check .canGoBack() before calling it
+ }
+ }
+}
+
+function reset() {
+ if (navigationRef.isReady()) {
+ navigationRef.dispatch(
+ CommonActions.reset({
+ index: 0,
+ routes: [{name: isNative ? 'HomeTab' : 'Home'}],
+ }),
+ )
}
}
@@ -319,13 +518,14 @@ const styles = StyleSheet.create({
backgroundColor: colors.black,
},
bgLight: {
- backgroundColor: colors.gray1,
+ backgroundColor: colors.white,
},
})
export {
navigate,
resetToTab,
+ reset,
handleLink,
TabsNavigator,
FlatNavigator,
diff --git a/src/lib/ThemeContext.tsx b/src/lib/ThemeContext.tsx
index ef17c1e7a1..e68ba5246e 100644
--- a/src/lib/ThemeContext.tsx
+++ b/src/lib/ThemeContext.tsx
@@ -78,7 +78,7 @@ export interface Theme {
}
export interface ThemeProviderProps {
- theme?: ColorScheme
+ theme?: 'light' | 'dark' | 'system'
}
export const ThemeContext = createContext(defaultTheme)
@@ -89,11 +89,14 @@ export const ThemeProvider: React.FC = ({
theme,
children,
}) => {
- const colorScheme = useColorScheme()
+ const colorSchemeFromRN = useColorScheme()
+
+ // if theme is 'system', use the device's configured color scheme
+ let colorScheme = theme === 'system' ? colorSchemeFromRN : theme
const value = useMemo(
- () => ((theme || colorScheme) === 'dark' ? darkTheme : defaultTheme),
- [colorScheme, theme],
+ () => (colorScheme === 'dark' ? darkTheme : defaultTheme),
+ [colorScheme],
)
return {children}
diff --git a/src/lib/api/api-polyfill.ts b/src/lib/api/api-polyfill.ts
index 7c38625a20..ea1d975985 100644
--- a/src/lib/api/api-polyfill.ts
+++ b/src/lib/api/api-polyfill.ts
@@ -11,7 +11,7 @@ export function doPolyfill() {
interface FetchHandlerResponse {
status: number
headers: Record
- body: ArrayBuffer | undefined
+ body: any
}
async function fetchHandler(
diff --git a/src/lib/api/build-suggested-posts.ts b/src/lib/api/build-suggested-posts.ts
deleted file mode 100644
index 554869d989..0000000000
--- a/src/lib/api/build-suggested-posts.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import {RootStoreModel} from 'state/index'
-import {
- AppBskyFeedDefs,
- AppBskyFeedGetAuthorFeed as GetAuthorFeed,
-} from '@atproto/api'
-type ReasonRepost = AppBskyFeedDefs.ReasonRepost
-
-async function getMultipleAuthorsPosts(
- rootStore: RootStoreModel,
- authors: string[],
- cursor: string | undefined = undefined,
- limit: number = 10,
-) {
- const responses = await Promise.all(
- authors.map((actor, index) =>
- rootStore.agent
- .getAuthorFeed({
- actor,
- limit,
- cursor: cursor ? cursor.split(',')[index] : undefined,
- })
- .catch(_err => ({success: false, headers: {}, data: {feed: []}})),
- ),
- )
- return responses
-}
-
-function mergePosts(
- responses: GetAuthorFeed.Response[],
- {repostsOnly, bestOfOnly}: {repostsOnly?: boolean; bestOfOnly?: boolean},
-) {
- let posts: AppBskyFeedDefs.FeedViewPost[] = []
-
- if (bestOfOnly) {
- for (const res of responses) {
- if (res.success) {
- // filter the feed down to the post with the most likes
- res.data.feed = res.data.feed.reduce(
- (acc: AppBskyFeedDefs.FeedViewPost[], v) => {
- if (
- !acc?.[0] &&
- !v.reason &&
- !v.reply &&
- isRecentEnough(v.post.indexedAt)
- ) {
- return [v]
- }
- if (
- acc &&
- !v.reason &&
- !v.reply &&
- (v.post.likeCount || 0) > (acc[0]?.post.likeCount || 0) &&
- isRecentEnough(v.post.indexedAt)
- ) {
- return [v]
- }
- return acc
- },
- [],
- )
- }
- }
- }
-
- // merge into one array
- for (const res of responses) {
- if (res.success) {
- posts = posts.concat(res.data.feed)
- }
- }
-
- // filter down to reposts of other users
- const uris = new Set()
- posts = posts.filter(p => {
- if (repostsOnly && !isARepostOfSomeoneElse(p)) {
- return false
- }
- if (uris.has(p.post.uri)) {
- return false
- }
- uris.add(p.post.uri)
- return true
- })
-
- // sort by index time
- posts.sort((a, b) => {
- return (
- Number(new Date(b.post.indexedAt)) - Number(new Date(a.post.indexedAt))
- )
- })
-
- return posts
-}
-
-function isARepostOfSomeoneElse(post: AppBskyFeedDefs.FeedViewPost): boolean {
- return (
- post.reason?.$type === 'app.bsky.feed.defs#reasonRepost' &&
- post.post.author.did !== (post.reason as ReasonRepost).by.did
- )
-}
-
-function getCombinedCursors(responses: GetAuthorFeed.Response[]) {
- let hasCursor = false
- const cursors = responses.map(r => {
- if (r.data.cursor) {
- hasCursor = true
- return r.data.cursor
- }
- return ''
- })
- if (!hasCursor) {
- return undefined
- }
- const combinedCursors = cursors.join(',')
- return combinedCursors
-}
-
-function isCombinedCursor(cursor: string) {
- return cursor.includes(',')
-}
-
-const TWO_DAYS_AGO = Date.now() - 1e3 * 60 * 60 * 48
-function isRecentEnough(date: string) {
- try {
- const d = Number(new Date(date))
- return d > TWO_DAYS_AGO
- } catch {
- return false
- }
-}
-
-export {
- getMultipleAuthorsPosts,
- mergePosts,
- getCombinedCursors,
- isCombinedCursor,
-}
diff --git a/src/lib/api/debug-appview-proxy-header.ts b/src/lib/api/debug-appview-proxy-header.ts
new file mode 100644
index 0000000000..39890b7c3c
--- /dev/null
+++ b/src/lib/api/debug-appview-proxy-header.ts
@@ -0,0 +1,63 @@
+/**
+ * APP-700
+ *
+ * This is a temporary debug setting we're running on the Web build to
+ * help the protocol team test some changes.
+ *
+ * It should be removed in ~2 weeks. It should only be used on the Web
+ * version of the app.
+ */
+
+import {useState, useCallback} from 'react'
+import {BskyAgent} from '@atproto/api'
+import {isWeb} from 'platform/detection'
+
+export function useDebugHeaderSetting(agent: BskyAgent): [boolean, () => void] {
+ const [enabled, setEnabled] = useState(isEnabled())
+
+ const toggle = useCallback(() => {
+ if (!isWeb || typeof window === 'undefined') {
+ return
+ }
+ if (!enabled) {
+ localStorage.setItem('set-header-x-appview-proxy', 'yes')
+ agent.api.xrpc.setHeader('x-appview-proxy', 'true')
+ setEnabled(true)
+ } else {
+ localStorage.removeItem('set-header-x-appview-proxy')
+ agent.api.xrpc.unsetHeader('x-appview-proxy')
+ setEnabled(false)
+ }
+ }, [setEnabled, enabled, agent])
+
+ return [enabled, toggle]
+}
+
+export function setDebugHeader(agent: BskyAgent, enabled: boolean) {
+ if (!isWeb || typeof window === 'undefined') {
+ return
+ }
+ if (enabled) {
+ localStorage.setItem('set-header-x-appview-proxy', 'yes')
+ agent.api.xrpc.setHeader('x-appview-proxy', 'true')
+ } else {
+ localStorage.removeItem('set-header-x-appview-proxy')
+ agent.api.xrpc.unsetHeader('x-appview-proxy')
+ }
+}
+
+export function applyDebugHeader(agent: BskyAgent) {
+ if (!isWeb) {
+ return
+ }
+ if (isEnabled()) {
+ agent.api.xrpc.setHeader('x-appview-proxy', 'true')
+ }
+}
+
+function isEnabled() {
+ if (!isWeb || typeof window === 'undefined') {
+ return false
+ }
+ return localStorage.getItem('set-header-x-appview-proxy') === 'yes'
+}
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts
index 2429419d88..3ff156dd67 100644
--- a/src/lib/api/feed-manip.ts
+++ b/src/lib/api/feed-manip.ts
@@ -1,4 +1,4 @@
-import {AppBskyFeedDefs} from '@atproto/api'
+import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import lande from 'lande'
import {hasProp} from 'lib/type-guards'
import {LANGUAGES_MAP_CODE2} from '../../locale/languages'
@@ -48,6 +48,13 @@ export class FeedViewPostsSlice {
return this.items[0]
}
+ get isReply() {
+ return (
+ AppBskyFeedPost.isRecord(this.rootItem.post.record) &&
+ !!this.rootItem.post.record.reply
+ )
+ }
+
containsUri(uri: string) {
return !!this.items.find(item => item.post.uri === uri)
}
@@ -67,9 +74,12 @@ export class FeedViewPostsSlice {
}
flattenReplyParent() {
- if (this.items[0].reply?.parent) {
- this.isFlattenedReply = true
- this.items.splice(0, 0, {post: this.items[0].reply?.parent})
+ if (this.items[0].reply) {
+ const reply = this.items[0].reply
+ if (AppBskyFeedDefs.isPostView(reply.parent)) {
+ this.isFlattenedReply = true
+ this.items.splice(0, 0, {post: reply.parent})
+ }
}
}
}
@@ -123,22 +133,20 @@ export class FeedTuner {
// turn non-threads with reply parents into threads
for (const slice of slices) {
- if (
- !slice.isThread &&
- !slice.items[0].reason &&
- slice.items[0].reply?.parent &&
- !this.seenUris.has(slice.items[0].reply?.parent.uri) &&
- !soonToBeSeenUris.has(slice.items[0].reply?.parent.uri)
- ) {
- const uri = slice.items[0].reply?.parent.uri
- slice.flattenReplyParent()
- soonToBeSeenUris.add(uri)
+ if (!slice.isThread && !slice.items[0].reason && slice.items[0].reply) {
+ const reply = slice.items[0].reply
+ if (
+ AppBskyFeedDefs.isPostView(reply.parent) &&
+ !this.seenUris.has(reply.parent.uri) &&
+ !soonToBeSeenUris.has(reply.parent.uri)
+ ) {
+ const uri = reply.parent.uri
+ slice.flattenReplyParent()
+ soonToBeSeenUris.add(uri)
+ }
}
}
- // sort by slice roots' timestamps
- slices.sort((a, b) => b.ts.localeCompare(a.ts))
-
for (const slice of slices) {
for (const item of slice.items) {
this.seenUris.add(item.post.uri)
@@ -176,9 +184,10 @@ export class FeedTuner {
): FeedViewPostsSlice[] {
// remove any replies without at least 2 likes
for (let i = slices.length - 1; i >= 0; i--) {
- if (slices[i].isFullThread || !slices[i].rootItem.reply) {
+ if (slices[i].isFullThread || !slices[i].isReply) {
continue
}
+
const item = slices[i].rootItem
const isRepost = Boolean(item.reason)
if (!isRepost && (item.post.likeCount || 0) < 2) {
@@ -194,7 +203,9 @@ export class FeedTuner {
tuner: FeedTuner,
slices: FeedViewPostsSlice[],
): FeedViewPostsSlice[] => {
- const origSlices = slices.concat()
+ if (!langsCode2.length) {
+ return slices
+ }
for (let i = slices.length - 1; i >= 0; i--) {
let hasPreferredLang = false
for (const item of slices[i].items) {
@@ -202,29 +213,40 @@ export class FeedTuner {
hasProp(item.post.record, 'text') &&
typeof item.post.record.text === 'string'
) {
- const res = lande(item.post.record.text)
- const contentLangCode3 = res[0][0]
- if (langsCode3.includes(contentLangCode3)) {
+ // Treat empty text the same as no text.
+ if (item.post.record.text.length === 0) {
hasPreferredLang = true
break
}
+
+ const res = lande(item.post.record.text)
+
+ if (langsCode3.includes(res[0][0])) {
+ hasPreferredLang = true
+ break
+ }
+ } else {
+ // no text? roll with it
+ hasPreferredLang = true
+ break
}
}
if (!hasPreferredLang) {
slices.splice(i, 1)
}
}
- if (slices.length) {
- return slices
- }
- // fallback: give everything if the language filter left nothing
- return origSlices
+ return slices
}
}
}
function getSelfReplyUri(item: FeedViewPost): string | undefined {
- return item.reply?.parent.author.did === item.post.author.did
- ? item.reply?.parent.uri
- : undefined
+ if (item.reply) {
+ if (AppBskyFeedDefs.isPostView(item.reply.parent)) {
+ return item.reply.parent.author.did === item.post.author.did
+ ? item.reply.parent.uri
+ : undefined
+ }
+ }
+ return undefined
}
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 1b12f29c5b..6235ca3433 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -10,15 +10,16 @@ import {
import {AtUri} from '@atproto/api'
import {RootStoreModel} from 'state/models/root-store'
import {isNetworkError} from 'lib/strings/errors'
-import {Image} from 'lib/media/types'
import {LinkMeta} from '../link-meta/link-meta'
import {isWeb} from 'platform/detection'
+import {ImageModel} from 'state/models/media/image'
export interface ExternalEmbedDraft {
uri: string
isLoading: boolean
meta?: LinkMeta
- localThumb?: Image
+ embed?: AppBskyEmbedRecord.Main
+ localThumb?: ImageModel
}
export async function resolveName(store: RootStoreModel, didOrHandle: string) {
@@ -61,7 +62,7 @@ interface PostOpts {
cid: string
}
extLink?: ExternalEmbedDraft
- images?: string[]
+ images?: ImageModel[]
knownHandles?: Set
onStateChange?: (state: string) => void
}
@@ -109,10 +110,12 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
const images: AppBskyEmbedImages.Image[] = []
for (const image of opts.images) {
opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
- const res = await uploadBlob(store, image, 'image/jpeg')
+ await image.compress()
+ const path = image.compressed?.path ?? image.path
+ const res = await uploadBlob(store, path, 'image/jpeg')
images.push({
image: res.data.blob,
- alt: '', // TODO supply alt text
+ alt: image.altText ?? '',
})
}
@@ -134,40 +137,54 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
}
if (opts.extLink && !opts.images?.length) {
- let thumb
- if (opts.extLink.localThumb) {
- opts.onStateChange?.('Uploading link thumbnail...')
- let encoding
- if (opts.extLink.localThumb.mime) {
- encoding = opts.extLink.localThumb.mime
- } else if (opts.extLink.localThumb.path.endsWith('.png')) {
- encoding = 'image/png'
- } else if (
- opts.extLink.localThumb.path.endsWith('.jpeg') ||
- opts.extLink.localThumb.path.endsWith('.jpg')
- ) {
- encoding = 'image/jpeg'
- } else {
- store.log.warn(
- 'Unexpected image format for thumbnail, skipping',
- opts.extLink.localThumb.path,
- )
+ if (opts.extLink.embed) {
+ embed = opts.extLink.embed
+ } else {
+ let thumb
+ if (opts.extLink.localThumb) {
+ opts.onStateChange?.('Uploading link thumbnail...')
+ let encoding
+ if (opts.extLink.localThumb.mime) {
+ encoding = opts.extLink.localThumb.mime
+ } else if (opts.extLink.localThumb.path.endsWith('.png')) {
+ encoding = 'image/png'
+ } else if (
+ opts.extLink.localThumb.path.endsWith('.jpeg') ||
+ opts.extLink.localThumb.path.endsWith('.jpg')
+ ) {
+ encoding = 'image/jpeg'
+ } else {
+ store.log.warn(
+ 'Unexpected image format for thumbnail, skipping',
+ opts.extLink.localThumb.path,
+ )
+ }
+ if (encoding) {
+ const thumbUploadRes = await uploadBlob(
+ store,
+ opts.extLink.localThumb.path,
+ encoding,
+ )
+ thumb = thumbUploadRes.data.blob
+ }
}
- if (encoding) {
- const thumbUploadRes = await uploadBlob(
- store,
- opts.extLink.localThumb.path,
- encoding,
- )
- thumb = thumbUploadRes.data.blob
- }
- }
- if (opts.quote) {
- embed = {
- $type: 'app.bsky.embed.recordWithMedia',
- record: embed,
- media: {
+ if (opts.quote) {
+ embed = {
+ $type: 'app.bsky.embed.recordWithMedia',
+ record: embed,
+ media: {
+ $type: 'app.bsky.embed.external',
+ external: {
+ uri: opts.extLink.uri,
+ title: opts.extLink.meta?.title || '',
+ description: opts.extLink.meta?.description || '',
+ thumb,
+ },
+ } as AppBskyEmbedExternal.Main,
+ } as AppBskyEmbedRecordWithMedia.Main
+ } else {
+ embed = {
$type: 'app.bsky.embed.external',
external: {
uri: opts.extLink.uri,
@@ -175,18 +192,8 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
description: opts.extLink.meta?.description || '',
thumb,
},
- } as AppBskyEmbedExternal.Main,
- } as AppBskyEmbedRecordWithMedia.Main
- } else {
- embed = {
- $type: 'app.bsky.embed.external',
- external: {
- uri: opts.extLink.uri,
- title: opts.extLink.meta?.title || '',
- description: opts.extLink.meta?.description || '',
- thumb,
- },
- } as AppBskyEmbedExternal.Main
+ } as AppBskyEmbedExternal.Main
+ }
}
}
diff --git a/src/lib/app-info.ts b/src/lib/app-info.ts
index 1ced274e79..3f026d3fe6 100644
--- a/src/lib/app-info.ts
+++ b/src/lib/app-info.ts
@@ -1,4 +1,5 @@
import VersionNumber from 'react-native-version-number'
+import * as Updates from 'expo-updates'
+export const updateChannel = Updates.channel
-export const appVersion = VersionNumber.appVersion
-export const buildVersion = VersionNumber.buildVersion
+export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})`
diff --git a/src/lib/app-info.web.ts b/src/lib/app-info.web.ts
index a2b6858da1..5739b8783a 100644
--- a/src/lib/app-info.web.ts
+++ b/src/lib/app-info.web.ts
@@ -1,3 +1,2 @@
-// TODO
-export const appVersion = 'TODO'
-export const buildVersion = 'TODO'
+import {version} from '../../package.json'
+export const appVersion = version
diff --git a/src/lib/async/revertible.ts b/src/lib/async/revertible.ts
index 3c8e3e8f9e..43383b61e8 100644
--- a/src/lib/async/revertible.ts
+++ b/src/lib/async/revertible.ts
@@ -4,6 +4,22 @@ import set from 'lodash.set'
const ongoingActions = new Set()
+/**
+ * This is a TypeScript function that optimistically updates data on the client-side before sending a
+ * request to the server and rolling back changes if the request fails.
+ * @param {T} model - The object or record that needs to be updated optimistically.
+ * @param preUpdate - `preUpdate` is a function that is called before the server update is executed. It
+ * can be used to perform any necessary actions or updates on the model or UI before the server update
+ * is initiated.
+ * @param serverUpdate - `serverUpdate` is a function that returns a Promise representing the server
+ * update operation. This function is called after the previous state of the model has been recorded
+ * and the `preUpdate` function has been executed. If the server update is successful, the `postUpdate`
+ * function is called with the result
+ * @param [postUpdate] - `postUpdate` is an optional callback function that will be called after the
+ * server update is successful. It takes in the response from the server update as its parameter. If
+ * this parameter is not provided, nothing will happen after the server update.
+ * @returns A Promise that resolves to `void`.
+ */
export const updateDataOptimistically = async <
T extends Record,
U,
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index d49d8c75cf..0a8c32cd63 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -4,6 +4,28 @@ export const FEEDBACK_FORM_URL =
export const MAX_DISPLAY_NAME = 64
export const MAX_DESCRIPTION = 256
+export const MAX_GRAPHEME_LENGTH = 300
+
+// Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html
+// but increasing limit per user feedback
+export const MAX_ALT_TEXT = 1000
+
+export function IS_LOCAL_DEV(url: string) {
+ return url.includes('localhost')
+}
+
+export function IS_STAGING(url: string) {
+ return !IS_LOCAL_DEV(url) && !IS_PROD(url)
+}
+
+export function IS_PROD(url: string) {
+ // NOTE
+ // until open federation, "production" is defined as the main server
+ // this definition will not work once federation is enabled!
+ // -prf
+ return url.startsWith('https://bsky.social')
+}
+
export const PROD_TEAM_HANDLES = [
'jay.bsky.social',
'pfrazee.com',
@@ -29,135 +51,46 @@ export function TEAM_HANDLES(serviceUrl: string) {
}
}
-export const PROD_SUGGESTED_FOLLOWS = [
- 'christina',
- 'wesam',
- 'jim',
- 'ab',
- 'karalabe',
- 'clun',
- 'staltz',
- 'gillian',
- 'karpathy',
- 'zoink',
- 'john',
- 'round',
- 'vex',
- 'umang',
- 'atroyn',
- 'poisonivy',
- 'wongmjane',
- 'lari',
- 'arunwadhwa',
- 'trav',
- 'fred',
- 'offscript',
- 'satnam',
- 'ella',
- 'caspian',
- 'spencer',
- 'nickgrossman',
- 'koji',
- 'avy',
- 'seymourstein',
- 'joelg',
- 'stig',
- 'rabble',
- 'hunterwalk',
- 'evan',
- 'aviral',
- 'tami',
- 'generativist',
- 'gord',
- 'ninjapleasedj',
- 'robotics',
- 'noahjnelson',
- 'vijay',
- 'scottbeale',
- 'daybreakjung',
- 'shelby',
- 'joel',
- 'space',
- 'rish',
- 'simon',
- 'kelly',
- 'maxbittker',
- 'sylphrenetic',
- 'caleb',
- 'jik',
- 'james',
- 'neil',
- 'tippenein',
- 'mandel',
- 'sharding',
- 'tyler',
- 'raymond',
- 'visakanv',
- 'saz',
- 'steph',
- 'ratzlaff',
- 'beth',
- 'weisser',
- 'katherine',
- 'annagat',
- 'an',
- 'kunal',
- 'josh',
- 'lurkshark',
- 'amir',
- 'amyxzh',
- 'danielle',
- 'jack-frazee',
- 'daniellefong',
- 'dystopiabreaker',
- 'morgan',
- 'vibes',
- 'cat',
- 'yuriy',
- 'alvinreyes',
- 'skoot',
- 'patricia',
- 'ara4n',
- 'case',
- 'armand',
- 'ivan',
- 'nicholas',
- 'kelsey',
- 'ericlee',
- 'emily',
- 'jake',
- 'jennijuju',
- 'ian5v',
- 'bnewbold',
- 'jasmine',
- 'chris',
- 'mtclai',
- 'willscott',
- 'michael',
- 'kwkroeger',
- 'broox',
- 'iamrosewang',
- 'jack-morrison',
- 'pwang',
- 'martin',
- 'jack',
- 'jay',
-]
- .map(handle => `${handle}.bsky.social`)
- .concat(['pfrazee.com', 'divy.zone', 'dholms.xyz', 'why.bsky.world'])
-export const STAGING_SUGGESTED_FOLLOWS = ['arcalinea', 'paul', 'paul2'].map(
- handle => `${handle}.staging.bsky.dev`,
-)
-export const DEV_SUGGESTED_FOLLOWS = ['alice', 'bob', 'carla'].map(
- handle => `${handle}.test`,
-)
-export function SUGGESTED_FOLLOWS(serviceUrl: string) {
- if (serviceUrl.includes('localhost')) {
- return DEV_SUGGESTED_FOLLOWS
- } else if (serviceUrl.includes('staging')) {
- return STAGING_SUGGESTED_FOLLOWS
+export const STAGING_DEFAULT_FEED = (rkey: string) =>
+ `at://did:plc:wqzurwm3kmaig6e6hnc2gqwo/app.bsky.feed.generator/${rkey}`
+export const PROD_DEFAULT_FEED = (rkey: string) =>
+ `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
+export async function DEFAULT_FEEDS(
+ serviceUrl: string,
+ resolveHandle: (name: string) => Promise,
+) {
+ if (IS_LOCAL_DEV(serviceUrl)) {
+ // local dev
+ const aliceDid = await resolveHandle('alice.test')
+ return {
+ pinned: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`],
+ saved: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`],
+ }
+ } else if (IS_STAGING(serviceUrl)) {
+ // staging
+ return {
+ pinned: [STAGING_DEFAULT_FEED('whats-hot')],
+ saved: [
+ STAGING_DEFAULT_FEED('bsky-team'),
+ STAGING_DEFAULT_FEED('with-friends'),
+ STAGING_DEFAULT_FEED('whats-hot'),
+ STAGING_DEFAULT_FEED('hot-classic'),
+ ],
+ }
} else {
- return PROD_SUGGESTED_FOLLOWS
+ // production
+ return {
+ pinned: [
+ PROD_DEFAULT_FEED('whats-hot'),
+ PROD_DEFAULT_FEED('with-friends'),
+ ],
+ saved: [
+ PROD_DEFAULT_FEED('bsky-team'),
+ PROD_DEFAULT_FEED('with-friends'),
+ PROD_DEFAULT_FEED('whats-hot'),
+ PROD_DEFAULT_FEED('hot-classic'),
+ ],
+ }
}
}
@@ -166,3 +99,18 @@ export const POST_IMG_MAX = {
height: 2000,
size: 1000000,
}
+
+export const STAGING_LINK_META_PROXY =
+ 'https://cardyb.staging.bsky.dev/v1/extract?url='
+
+export const PROD_LINK_META_PROXY = 'https://cardyb.bsky.app/v1/extract?url='
+
+export function LINK_META_PROXY(serviceUrl: string) {
+ if (IS_LOCAL_DEV(serviceUrl)) {
+ return STAGING_LINK_META_PROXY
+ } else if (IS_STAGING(serviceUrl)) {
+ return STAGING_LINK_META_PROXY
+ } else {
+ return PROD_LINK_META_PROXY
+ }
+}
diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts
new file mode 100644
index 0000000000..516940c1ce
--- /dev/null
+++ b/src/lib/haptics.ts
@@ -0,0 +1,40 @@
+import {isIOS, isWeb} from 'platform/detection'
+import ReactNativeHapticFeedback, {
+ HapticFeedbackTypes,
+} from 'react-native-haptic-feedback'
+
+const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537s
+
+export class Haptics {
+ static default() {
+ if (isWeb) {
+ return
+ }
+ ReactNativeHapticFeedback.trigger(hapticImpact)
+ }
+ static impact(type: HapticFeedbackTypes = hapticImpact) {
+ if (isWeb) {
+ return
+ }
+ ReactNativeHapticFeedback.trigger(type)
+ }
+ static selection() {
+ if (isWeb) {
+ return
+ }
+ ReactNativeHapticFeedback.trigger('selection')
+ }
+ static notification = (type: 'success' | 'warning' | 'error') => {
+ if (isWeb) {
+ return
+ }
+ switch (type) {
+ case 'success':
+ return ReactNativeHapticFeedback.trigger('notificationSuccess')
+ case 'warning':
+ return ReactNativeHapticFeedback.trigger('notificationWarning')
+ case 'error':
+ return ReactNativeHapticFeedback.trigger('notificationError')
+ }
+ }
+}
diff --git a/src/lib/hooks/useCustomFeed.ts b/src/lib/hooks/useCustomFeed.ts
new file mode 100644
index 0000000000..d7a27050dc
--- /dev/null
+++ b/src/lib/hooks/useCustomFeed.ts
@@ -0,0 +1,27 @@
+import {useEffect, useState} from 'react'
+import {useStores} from 'state/index'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+
+export function useCustomFeed(uri: string): CustomFeedModel | undefined {
+ const store = useStores()
+ const [item, setItem] = useState()
+ useEffect(() => {
+ async function fetchView() {
+ const res = await store.agent.app.bsky.feed.getFeedGenerator({
+ feed: uri,
+ })
+ const view = res.data.view
+ return view
+ }
+ async function buildFeedItem() {
+ const view = await fetchView()
+ if (view) {
+ const temp = new CustomFeedModel(store, view)
+ setItem(temp)
+ }
+ }
+ buildFeedItem()
+ }, [store, uri])
+
+ return item
+}
diff --git a/src/lib/hooks/useDraggableScrollView.ts b/src/lib/hooks/useDraggableScrollView.ts
new file mode 100644
index 0000000000..b0f7465d79
--- /dev/null
+++ b/src/lib/hooks/useDraggableScrollView.ts
@@ -0,0 +1,84 @@
+import {useEffect, useRef, useMemo, ForwardedRef} from 'react'
+import {Platform, findNodeHandle} from 'react-native'
+import type {ScrollView} from 'react-native'
+import {mergeRefs} from 'lib/merge-refs'
+
+type Props = {
+ cursor?: string
+ outerRef?: ForwardedRef
+}
+
+export function useDraggableScroll({
+ outerRef,
+ cursor = 'grab',
+}: Props = {}) {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ if (Platform.OS !== 'web' || !ref.current) {
+ return
+ }
+ const slider = findNodeHandle(ref.current) as unknown as HTMLDivElement
+ if (!slider) {
+ return
+ }
+ let isDragging = false
+ let isMouseDown = false
+ let startX = 0
+ let scrollLeft = 0
+
+ const mouseDown = (e: MouseEvent) => {
+ isMouseDown = true
+ startX = e.pageX - slider.offsetLeft
+ scrollLeft = slider.scrollLeft
+
+ slider.style.cursor = cursor
+ }
+
+ const mouseUp = () => {
+ if (isDragging) {
+ slider.addEventListener('click', e => e.stopPropagation(), {once: true})
+ }
+
+ isMouseDown = false
+ isDragging = false
+ slider.style.cursor = 'default'
+ }
+
+ const mouseMove = (e: MouseEvent) => {
+ if (!isMouseDown) {
+ return
+ }
+
+ // Require n pixels momement before start of drag (3 in this case )
+ const x = e.pageX - slider.offsetLeft
+ if (Math.abs(x - startX) < 3) {
+ return
+ }
+
+ isDragging = true
+ e.preventDefault()
+ const walk = x - startX
+ slider.scrollLeft = scrollLeft - walk
+ }
+
+ slider.addEventListener('mousedown', mouseDown)
+ window.addEventListener('mouseup', mouseUp)
+ window.addEventListener('mousemove', mouseMove)
+
+ return () => {
+ slider.removeEventListener('mousedown', mouseDown)
+ window.removeEventListener('mouseup', mouseUp)
+ window.removeEventListener('mousemove', mouseMove)
+ }
+ }, [cursor])
+
+ const refs = useMemo(
+ () => mergeRefs(outerRef ? [ref, outerRef] : [ref]),
+ [ref, outerRef],
+ )
+
+ return {
+ refs,
+ }
+}
diff --git a/src/lib/hooks/useNavigationTabState.ts b/src/lib/hooks/useNavigationTabState.ts
index fb36621523..3a05fe524f 100644
--- a/src/lib/hooks/useNavigationTabState.ts
+++ b/src/lib/hooks/useNavigationTabState.ts
@@ -6,14 +6,16 @@ export function useNavigationTabState() {
const res = {
isAtHome: getTabState(state, 'Home') !== TabState.Outside,
isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
+ isAtFeeds: getTabState(state, 'Feeds') !== TabState.Outside,
isAtNotifications:
getTabState(state, 'Notifications') !== TabState.Outside,
isAtMyProfile: getTabState(state, 'MyProfile') !== TabState.Outside,
}
if (
!res.isAtHome &&
- !res.isAtNotifications &&
!res.isAtSearch &&
+ !res.isAtFeeds &&
+ !res.isAtNotifications &&
!res.isAtMyProfile
) {
// HACK for some reason useNavigationState will give us pre-hydration results
diff --git a/src/lib/hooks/useOTAUpdate.ts b/src/lib/hooks/useOTAUpdate.ts
new file mode 100644
index 0000000000..ae6035223a
--- /dev/null
+++ b/src/lib/hooks/useOTAUpdate.ts
@@ -0,0 +1,74 @@
+import * as Updates from 'expo-updates'
+import {useCallback, useEffect} from 'react'
+import {AppState} from 'react-native'
+import {useStores} from 'state/index'
+
+export function useOTAUpdate() {
+ const store = useStores()
+
+ // HELPER FUNCTIONS
+ const showUpdatePopup = useCallback(() => {
+ store.shell.openModal({
+ name: 'confirm',
+ title: 'Update Available',
+ message:
+ 'A new version of the app is available. Please update to continue using the app.',
+ onPressConfirm: async () => {
+ Updates.reloadAsync().catch(err => {
+ throw err
+ })
+ },
+ })
+ }, [store.shell])
+ const checkForUpdate = useCallback(async () => {
+ store.log.debug('useOTAUpdate: Checking for update...')
+ try {
+ // Check if new OTA update is available
+ const update = await Updates.checkForUpdateAsync()
+ // If updates aren't available stop the function execution
+ if (!update.isAvailable) {
+ return
+ }
+ // Otherwise fetch the update in the background, so even if the user rejects switching to latest version it will be done automatically on next relaunch.
+ await Updates.fetchUpdateAsync()
+ // show a popup modal
+ showUpdatePopup()
+ } catch (e) {
+ console.error('useOTAUpdate: Error while checking for update', e)
+ store.log.error('useOTAUpdate: Error while checking for update', e)
+ }
+ }, [showUpdatePopup, store.log])
+ const updateEventListener = useCallback(
+ (event: Updates.UpdateEvent) => {
+ store.log.debug('useOTAUpdate: Listening for update...')
+ if (event.type === Updates.UpdateEventType.ERROR) {
+ throw new Error(event.message)
+ } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) {
+ // Handle no update available
+ // do nothing
+ } else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
+ // Handle update available
+ // open modal, ask for user confirmation, and reload the app
+ showUpdatePopup()
+ }
+ },
+ [showUpdatePopup, store.log],
+ )
+
+ useEffect(() => {
+ // ADD EVENT LISTENERS
+ const updateEventSubscription = Updates.addListener(updateEventListener)
+ const appStateSubscription = AppState.addEventListener('change', state => {
+ if (state === 'active' && !__DEV__) {
+ checkForUpdate()
+ }
+ })
+
+ // REMOVE EVENT LISTENERS (CLEANUP)
+ return () => {
+ updateEventSubscription.remove()
+ appStateSubscription.remove()
+ }
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
+ // disable exhaustive deps because we don't want to run this effect again
+}
diff --git a/src/lib/hooks/useOnMainScroll.ts b/src/lib/hooks/useOnMainScroll.ts
index 41b35dd4f6..12e42aca5f 100644
--- a/src/lib/hooks/useOnMainScroll.ts
+++ b/src/lib/hooks/useOnMainScroll.ts
@@ -1,25 +1,56 @@
-import {useState} from 'react'
+import {useState, useCallback, useRef} from 'react'
import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native'
import {RootStoreModel} from 'state/index'
+import {s} from 'lib/styles'
+import {isDesktopWeb} from 'platform/detection'
+
+const DY_LIMIT = isDesktopWeb ? 30 : 10
export type OnScrollCb = (
event: NativeSyntheticEvent,
) => void
+export type ResetCb = () => void
-export function useOnMainScroll(store: RootStoreModel) {
- let [lastY, setLastY] = useState(0)
- let isMinimal = store.shell.minimalShellMode
- return function onMainScroll(event: NativeSyntheticEvent) {
- const y = event.nativeEvent.contentOffset.y
- const dy = y - (lastY || 0)
- setLastY(y)
+export function useOnMainScroll(
+ store: RootStoreModel,
+): [OnScrollCb, boolean, ResetCb] {
+ let lastY = useRef(0)
+ let [isScrolledDown, setIsScrolledDown] = useState(false)
+ return [
+ useCallback(
+ (event: NativeSyntheticEvent) => {
+ const y = event.nativeEvent.contentOffset.y
+ const dy = y - (lastY.current || 0)
+ lastY.current = y
- if (!isMinimal && y > 10 && dy > 10) {
- store.shell.setMinimalShellMode(true)
- isMinimal = true
- } else if (isMinimal && (y <= 10 || dy < -10)) {
+ if (!store.shell.minimalShellMode && y > 10 && dy > DY_LIMIT) {
+ store.shell.setMinimalShellMode(true)
+ } else if (
+ store.shell.minimalShellMode &&
+ (y <= 10 || dy < DY_LIMIT * -1)
+ ) {
+ store.shell.setMinimalShellMode(false)
+ }
+
+ if (
+ !isScrolledDown &&
+ event.nativeEvent.contentOffset.y > s.window.height
+ ) {
+ setIsScrolledDown(true)
+ } else if (
+ isScrolledDown &&
+ event.nativeEvent.contentOffset.y < s.window.height
+ ) {
+ setIsScrolledDown(false)
+ }
+ },
+ [store, isScrolledDown],
+ ),
+ isScrolledDown,
+ useCallback(() => {
+ setIsScrolledDown(false)
store.shell.setMinimalShellMode(false)
- isMinimal = false
- }
- }
+ lastY.current = 1e8 // NOTE we set this very high so that the onScroll logic works right -prf
+ }, [store, setIsScrolledDown]),
+ ]
}
diff --git a/src/lib/hooks/usePermissions.ts b/src/lib/hooks/usePermissions.ts
index 7849949c5c..9cb4a80dd1 100644
--- a/src/lib/hooks/usePermissions.ts
+++ b/src/lib/hooks/usePermissions.ts
@@ -1,12 +1,8 @@
-import {Alert} from 'react-native'
import {Camera} from 'expo-camera'
import * as MediaLibrary from 'expo-media-library'
import {Linking} from 'react-native'
import {isWeb} from 'platform/detection'
-
-const openSettings = () => {
- Linking.openURL('app-settings:')
-}
+import {Alert} from 'view/com/util/Alert'
const openPermissionAlert = (perm: string) => {
Alert.alert(
@@ -17,7 +13,7 @@ const openPermissionAlert = (perm: string) => {
text: 'Cancel',
style: 'cancel',
},
- {text: 'Open Settings', onPress: () => openSettings()},
+ {text: 'Open Settings', onPress: () => Linking.openSettings()},
],
)
}
diff --git a/src/lib/hooks/useSetTitle.ts b/src/lib/hooks/useSetTitle.ts
new file mode 100644
index 0000000000..c5c7a5ca16
--- /dev/null
+++ b/src/lib/hooks/useSetTitle.ts
@@ -0,0 +1,20 @@
+import {useEffect} from 'react'
+import {useNavigation} from '@react-navigation/native'
+
+import {NavigationProp} from 'lib/routes/types'
+import {bskyTitle} from 'lib/strings/headings'
+import {useStores} from 'state/index'
+
+/**
+ * Requires consuming component to be wrapped in `observer`:
+ * https://stackoverflow.com/a/71488009
+ */
+export function useSetTitle(title?: string) {
+ const navigation = useNavigation()
+ const {unreadCountLabel} = useStores().me.notifications
+ useEffect(() => {
+ if (title) {
+ navigation.setOptions({title: bskyTitle(title, unreadCountLabel)})
+ }
+ }, [title, navigation, unreadCountLabel])
+}
diff --git a/src/lib/hooks/useTabFocusEffect.ts b/src/lib/hooks/useTabFocusEffect.ts
new file mode 100644
index 0000000000..e446084c5a
--- /dev/null
+++ b/src/lib/hooks/useTabFocusEffect.ts
@@ -0,0 +1,27 @@
+import {useEffect, useState} from 'react'
+import {useNavigation} from '@react-navigation/native'
+import {getTabState, TabState} from 'lib/routes/helpers'
+
+export function useTabFocusEffect(
+ tabName: string,
+ cb: (isInside: boolean) => void,
+) {
+ const [isInside, setIsInside] = useState(false)
+
+ // get root navigator state
+ let nav = useNavigation()
+ while (nav.getParent()) {
+ nav = nav.getParent()
+ }
+ const state = nav.getState()
+
+ useEffect(() => {
+ // check if inside
+ let v = getTabState(state, tabName) !== TabState.Outside
+ if (v !== isInside) {
+ // fire
+ setIsInside(v)
+ cb(v)
+ }
+ }, [state, isInside, setIsInside, tabName, cb])
+}
diff --git a/src/lib/hooks/useTimer.ts b/src/lib/hooks/useTimer.ts
new file mode 100644
index 0000000000..b14a9f24fd
--- /dev/null
+++ b/src/lib/hooks/useTimer.ts
@@ -0,0 +1,32 @@
+import * as React from 'react'
+
+/**
+ * Helper hook to run persistent timers on views
+ */
+export function useTimer(time: number, handler: () => void) {
+ const timer = React.useRef(undefined)
+
+ // function to restart the timer
+ const reset = React.useCallback(() => {
+ if (timer.current) {
+ clearTimeout(timer.current)
+ }
+ timer.current = setTimeout(handler, time)
+ }, [time, timer, handler])
+
+ // function to cancel the timer
+ const cancel = React.useCallback(() => {
+ if (timer.current) {
+ clearTimeout(timer.current)
+ timer.current = undefined
+ }
+ }, [timer])
+
+ // start the timer immediately
+ React.useEffect(() => {
+ reset()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ return [reset, cancel]
+}
diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx
index 300c13b0df..dab950350b 100644
--- a/src/lib/icons.tsx
+++ b/src/lib/icons.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
-import Svg, {Path, Rect, Line, Ellipse} from 'react-native-svg'
+import Svg, {Path, Rect, Line, Ellipse, Circle} from 'react-native-svg'
export function GridIcon({
style,
@@ -88,7 +88,7 @@ export function HomeIconSolid({
)
@@ -320,6 +320,35 @@ export function MoonIcon({
)
}
+// Copyright (c) 2020 Refactoring UI Inc.
+// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
+export function SunIcon({
+ style,
+ size,
+ strokeWidth = 1.5,
+}: {
+ style?: StyleProp
+ size?: string | number
+ strokeWidth?: number
+}) {
+ return (
+
+
+
+ )
+}
+
// Copyright (c) 2020 Refactoring UI Inc.
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
export function UserIcon({
@@ -431,7 +460,7 @@ export function RepostIcon({
strokeWidth={strokeWidth}
strokeLinejoin="round"
fill="none"
- d="M 14.437 17.081 L 5.475 17.095 C 4.7 17.095 4.072 16.467 4.072 15.692 L 4.082 5.65 L 1.22 9.854 M 4.082 5.65 L 7.006 9.854 M 9.859 5.65 L 18.625 5.654 C 19.4 5.654 20.028 6.282 20.028 7.057 L 20.031 17.081 L 17.167 12.646 M 20.031 17.081 L 22.866 12.646"
+ d="M 14.437 18.081 L 5.475 18.095 C 4.7 18.095 4.072 17.467 4.072 16.692 L 4.082 6.65 L 1.22 10.854 M 4.082 6.65 L 7.006 10.854 M 9.859 6.65 L 18.625 6.654 C 19.4 6.654 20.028 7.282 20.028 8.057 L 20.031 18.081 L 17.167 13.646 M 20.031 18.081 L 22.866 13.646"
/>
)
@@ -443,7 +472,7 @@ export function HeartIcon({
size = 24,
strokeWidth = 1.5,
}: {
- style?: StyleProp
+ style?: StyleProp
size?: string | number
strokeWidth: number
}) {
@@ -464,7 +493,7 @@ export function HeartIconSolid({
style,
size = 24,
}: {
- style?: StyleProp
+ style?: StyleProp
size?: string | number
}) {
return (
@@ -772,8 +801,8 @@ export function SquarePlusIcon({
height={size || 24}
style={style}>
)
}
+
+export function HandIcon({
+ style,
+ size,
+ strokeWidth = 1.5,
+}: {
+ style?: StyleProp
+ size?: string | number
+ strokeWidth?: number
+}) {
+ return (
+
+
+
+
+
+ )
+}
+
+export function SatelliteDishIconSolid({
+ style,
+ size,
+ strokeWidth = 1.5,
+}: {
+ style?: StyleProp
+ size?: string | number
+ strokeWidth?: number
+}) {
+ return (
+
+
+
+
+
+
+ )
+}
+
+export function SatelliteDishIcon({
+ style,
+ size,
+ strokeWidth = 1.5,
+}: {
+ style?: StyleProp
+ size?: string | number
+ strokeWidth?: number
+}) {
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts
index f68353222c..908826f177 100644
--- a/src/lib/labeling/const.ts
+++ b/src/lib/labeling/const.ts
@@ -1,22 +1,31 @@
import {LabelPreferencesModel} from 'state/models/ui/preferences'
-
-export interface LabelValGroup {
- id: keyof LabelPreferencesModel | 'illegal' | 'unknown'
- title: string
- subtitle?: string
- warning?: string
- values: string[]
-}
+import {LabelValGroup} from './types'
export const ILLEGAL_LABEL_GROUP: LabelValGroup = {
id: 'illegal',
title: 'Illegal Content',
- values: ['csam', 'dmca-violation', 'nudity-nonconsentual'],
+ warning: 'Illegal Content',
+ values: ['csam', 'dmca-violation', 'nudity-nonconsensual'],
+}
+
+export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = {
+ id: 'always-filter',
+ title: 'Content Warning',
+ warning: 'Content Warning',
+ values: ['!filter'],
+}
+
+export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = {
+ id: 'always-warn',
+ title: 'Content Warning',
+ warning: 'Content Warning',
+ values: ['!warn', 'account-security'],
}
export const UNKNOWN_LABEL_GROUP: LabelValGroup = {
id: 'unknown',
title: 'Unknown Label',
+ warning: 'Content Warning',
values: [],
}
@@ -27,9 +36,10 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
nsfw: {
id: 'nsfw',
title: 'Explicit Sexual Images',
- subtitle: 'i.e. Pornography',
+ subtitle: 'i.e. pornography',
warning: 'Sexually Explicit',
- values: ['porn'],
+ values: ['porn', 'nsfl'],
+ isAdultImagery: true,
},
nudity: {
id: 'nudity',
@@ -37,6 +47,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
subtitle: 'Including non-sexual and artistic',
warning: 'Nudity',
values: ['nudity'],
+ isAdultImagery: true,
},
suggestive: {
id: 'suggestive',
@@ -44,24 +55,26 @@ export const CONFIGURABLE_LABEL_GROUPS: Record<
subtitle: 'Does not include nudity',
warning: 'Sexually Suggestive',
values: ['sexual'],
+ isAdultImagery: true,
},
gore: {
id: 'gore',
title: 'Violent / Bloody',
subtitle: 'Gore, self-harm, torture',
warning: 'Violence',
- values: ['gore', 'self-harm', 'torture'],
+ values: ['gore', 'self-harm', 'torture', 'nsfl', 'corpse'],
+ isAdultImagery: true,
},
hate: {
id: 'hate',
title: 'Political Hate-Groups',
warning: 'Hate',
- values: ['icon-kkk', 'icon-nazi'],
+ values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'],
},
spam: {
id: 'spam',
title: 'Spam',
- subtitle: 'Excessive low-quality posts',
+ subtitle: 'Excessive unwanted interactions',
warning: 'Spam',
values: ['spam'],
},
diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts
index b2057ff18c..447b0a99ae 100644
--- a/src/lib/labeling/helpers.ts
+++ b/src/lib/labeling/helpers.ts
@@ -1,9 +1,36 @@
import {
- LabelValGroup,
+ AppBskyActorDefs,
+ AppBskyGraphDefs,
+ AppBskyEmbedRecordWithMedia,
+ AppBskyEmbedRecord,
+ AppBskyEmbedImages,
+ AppBskyEmbedExternal,
+} from '@atproto/api'
+import {
CONFIGURABLE_LABEL_GROUPS,
ILLEGAL_LABEL_GROUP,
+ ALWAYS_FILTER_LABEL_GROUP,
+ ALWAYS_WARN_LABEL_GROUP,
UNKNOWN_LABEL_GROUP,
} from './const'
+import {
+ Label,
+ LabelValGroup,
+ ModerationBehaviorCode,
+ ModerationBehavior,
+ PostModeration,
+ ProfileModeration,
+ PostLabelInfo,
+ ProfileLabelInfo,
+} from './types'
+import {RootStoreModel} from 'state/index'
+
+type Embed =
+ | AppBskyEmbedRecord.View
+ | AppBskyEmbedImages.View
+ | AppBskyEmbedExternal.View
+ | AppBskyEmbedRecordWithMedia.View
+ | {$type: string; [k: string]: unknown}
export function getLabelValueGroup(labelVal: string): LabelValGroup {
let id: keyof typeof CONFIGURABLE_LABEL_GROUPS
@@ -11,9 +38,399 @@ export function getLabelValueGroup(labelVal: string): LabelValGroup {
if (ILLEGAL_LABEL_GROUP.values.includes(labelVal)) {
return ILLEGAL_LABEL_GROUP
}
+ if (ALWAYS_FILTER_LABEL_GROUP.values.includes(labelVal)) {
+ return ALWAYS_FILTER_LABEL_GROUP
+ }
+ if (ALWAYS_WARN_LABEL_GROUP.values.includes(labelVal)) {
+ return ALWAYS_WARN_LABEL_GROUP
+ }
if (CONFIGURABLE_LABEL_GROUPS[id].values.includes(labelVal)) {
return CONFIGURABLE_LABEL_GROUPS[id]
}
}
return UNKNOWN_LABEL_GROUP
}
+
+export function getPostModeration(
+ store: RootStoreModel,
+ postInfo: PostLabelInfo,
+): PostModeration {
+ const accountPref = store.preferences.getLabelPreference(
+ postInfo.accountLabels,
+ )
+ const profilePref = store.preferences.getLabelPreference(
+ postInfo.profileLabels,
+ )
+ const postPref = store.preferences.getLabelPreference(postInfo.postLabels)
+
+ // avatar
+ let avatar = {
+ warn: accountPref.pref === 'hide' || accountPref.pref === 'warn',
+ blur:
+ postInfo.isBlocking ||
+ accountPref.pref === 'hide' ||
+ accountPref.pref === 'warn' ||
+ profilePref.pref === 'hide' ||
+ profilePref.pref === 'warn',
+ }
+
+ // hide no-override cases
+ if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') {
+ return hidePostNoOverride(accountPref.desc.warning)
+ }
+ if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') {
+ return hidePostNoOverride(profilePref.desc.warning)
+ }
+ if (postPref.pref === 'hide' && postPref.desc.id === 'illegal') {
+ return hidePostNoOverride(postPref.desc.warning)
+ }
+
+ // hide cases
+ if (postInfo.isBlocking) {
+ return {
+ avatar,
+ list: hide('Post from an account you blocked.'),
+ thread: hide('Post from an account you blocked.'),
+ view: warn('Post from an account you blocked.'),
+ }
+ }
+ if (postInfo.isBlockedBy) {
+ return {
+ avatar,
+ list: hide('Post from an account that has blocked you.'),
+ thread: hide('Post from an account that has blocked you.'),
+ view: warn('Post from an account that has blocked you.'),
+ }
+ }
+ if (accountPref.pref === 'hide') {
+ return {
+ avatar,
+ list: hide(accountPref.desc.warning),
+ thread: hide(accountPref.desc.warning),
+ view: warn(accountPref.desc.warning),
+ }
+ }
+ if (profilePref.pref === 'hide') {
+ return {
+ avatar,
+ list: hide(profilePref.desc.warning),
+ thread: hide(profilePref.desc.warning),
+ view: warn(profilePref.desc.warning),
+ }
+ }
+ if (postPref.pref === 'hide') {
+ return {
+ avatar,
+ list: hide(postPref.desc.warning),
+ thread: hide(postPref.desc.warning),
+ view: warn(postPref.desc.warning),
+ }
+ }
+
+ // muting
+ if (postInfo.isMuted) {
+ let msg = 'Post from an account you muted.'
+ if (postInfo.mutedByList) {
+ msg = `Muted by ${postInfo.mutedByList.name}`
+ }
+ return {
+ avatar,
+ list: isMute(hide(msg)),
+ thread: isMute(warn(msg)),
+ view: isMute(warn(msg)),
+ }
+ }
+
+ // warning cases
+ if (postPref.pref === 'warn') {
+ if (postPref.desc.isAdultImagery) {
+ return {
+ avatar,
+ list: warnImages(postPref.desc.warning),
+ thread: warnImages(postPref.desc.warning),
+ view: warnImages(postPref.desc.warning),
+ }
+ }
+ return {
+ avatar,
+ list: warnContent(postPref.desc.warning),
+ thread: warnContent(postPref.desc.warning),
+ view: warnContent(postPref.desc.warning),
+ }
+ }
+ if (accountPref.pref === 'warn') {
+ return {
+ avatar,
+ list: warnContent(accountPref.desc.warning),
+ thread: warnContent(accountPref.desc.warning),
+ view: warnContent(accountPref.desc.warning),
+ }
+ }
+
+ return {
+ avatar,
+ list: show(),
+ thread: show(),
+ view: show(),
+ }
+}
+
+export function mergePostModerations(
+ moderations: PostModeration[],
+): PostModeration {
+ const merged: PostModeration = {
+ avatar: {warn: false, blur: false},
+ list: show(),
+ thread: show(),
+ view: show(),
+ }
+ for (const mod of moderations) {
+ if (mod.list.behavior === ModerationBehaviorCode.Hide) {
+ merged.list = mod.list
+ }
+ if (mod.thread.behavior === ModerationBehaviorCode.Hide) {
+ merged.thread = mod.thread
+ }
+ if (mod.view.behavior === ModerationBehaviorCode.Hide) {
+ merged.view = mod.view
+ }
+ }
+ return merged
+}
+
+export function getProfileModeration(
+ store: RootStoreModel,
+ profileInfo: ProfileLabelInfo,
+): ProfileModeration {
+ const accountPref = store.preferences.getLabelPreference(
+ profileInfo.accountLabels,
+ )
+ const profilePref = store.preferences.getLabelPreference(
+ profileInfo.profileLabels,
+ )
+
+ // avatar
+ let avatar = {
+ warn: accountPref.pref === 'hide' || accountPref.pref === 'warn',
+ blur:
+ profileInfo.isBlocking ||
+ accountPref.pref === 'hide' ||
+ accountPref.pref === 'warn' ||
+ profilePref.pref === 'hide' ||
+ profilePref.pref === 'warn',
+ }
+
+ // hide no-override cases
+ if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') {
+ return hideProfileNoOverride(accountPref.desc.warning)
+ }
+ if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') {
+ return hideProfileNoOverride(profilePref.desc.warning)
+ }
+
+ // hide cases
+ if (accountPref.pref === 'hide') {
+ return {
+ avatar,
+ list: hide(accountPref.desc.warning),
+ view: hide(accountPref.desc.warning),
+ }
+ }
+ if (profilePref.pref === 'hide') {
+ return {
+ avatar,
+ list: hide(profilePref.desc.warning),
+ view: hide(profilePref.desc.warning),
+ }
+ }
+
+ // warn cases
+ if (accountPref.pref === 'warn') {
+ return {
+ avatar,
+ list:
+ profileInfo.isBlocking || profileInfo.isBlockedBy
+ ? hide('Blocked account')
+ : warn(accountPref.desc.warning),
+ view: warn(accountPref.desc.warning),
+ }
+ }
+ // we don't warn for this
+ // if (profilePref.pref === 'warn') {
+ // return {
+ // avatar,
+ // list: warn(profilePref.desc.warning),
+ // view: warn(profilePref.desc.warning),
+ // }
+ // }
+
+ return {
+ avatar,
+ list: profileInfo.isBlocking ? hide('Blocked account') : show(),
+ view: show(),
+ }
+}
+
+export function getProfileViewBasicLabelInfo(
+ profile: AppBskyActorDefs.ProfileViewBasic,
+): ProfileLabelInfo {
+ return {
+ accountLabels: filterAccountLabels(profile.labels),
+ profileLabels: filterProfileLabels(profile.labels),
+ isMuted: profile.viewer?.muted || false,
+ isBlocking: !!profile.viewer?.blocking || false,
+ isBlockedBy: !!profile.viewer?.blockedBy || false,
+ }
+}
+
+export function getEmbedLabels(embed?: Embed): Label[] {
+ if (!embed) {
+ return []
+ }
+ if (
+ AppBskyEmbedRecord.isView(embed) &&
+ AppBskyEmbedRecord.isViewRecord(embed.record)
+ ) {
+ return embed.record.labels || []
+ }
+ return []
+}
+
+export function getEmbedMuted(embed?: Embed): boolean {
+ if (!embed) {
+ return false
+ }
+ if (
+ AppBskyEmbedRecord.isView(embed) &&
+ AppBskyEmbedRecord.isViewRecord(embed.record)
+ ) {
+ return !!embed.record.author.viewer?.muted
+ }
+ return false
+}
+
+export function getEmbedMutedByList(
+ embed?: Embed,
+): AppBskyGraphDefs.ListViewBasic | undefined {
+ if (!embed) {
+ return undefined
+ }
+ if (
+ AppBskyEmbedRecord.isView(embed) &&
+ AppBskyEmbedRecord.isViewRecord(embed.record)
+ ) {
+ return embed.record.author.viewer?.mutedByList
+ }
+ return undefined
+}
+
+export function getEmbedBlocking(embed?: Embed): boolean {
+ if (!embed) {
+ return false
+ }
+ if (
+ AppBskyEmbedRecord.isView(embed) &&
+ AppBskyEmbedRecord.isViewRecord(embed.record)
+ ) {
+ return !!embed.record.author.viewer?.blocking
+ }
+ return false
+}
+
+export function getEmbedBlockedBy(embed?: Embed): boolean {
+ if (!embed) {
+ return false
+ }
+ if (
+ AppBskyEmbedRecord.isView(embed) &&
+ AppBskyEmbedRecord.isViewRecord(embed.record)
+ ) {
+ return !!embed.record.author.viewer?.blockedBy
+ }
+ return false
+}
+
+export function filterAccountLabels(labels?: Label[]): Label[] {
+ if (!labels) {
+ return []
+ }
+ return labels.filter(
+ label => !label.uri.endsWith('/app.bsky.actor.profile/self'),
+ )
+}
+
+export function filterProfileLabels(labels?: Label[]): Label[] {
+ if (!labels) {
+ return []
+ }
+ return labels.filter(label =>
+ label.uri.endsWith('/app.bsky.actor.profile/self'),
+ )
+}
+
+// internal methods
+// =
+
+function show() {
+ return {
+ behavior: ModerationBehaviorCode.Show,
+ }
+}
+
+function hidePostNoOverride(reason: string) {
+ return {
+ avatar: {warn: true, blur: true},
+ list: hideNoOverride(reason),
+ thread: hideNoOverride(reason),
+ view: hideNoOverride(reason),
+ }
+}
+
+function hideProfileNoOverride(reason: string) {
+ return {
+ avatar: {warn: true, blur: true},
+ list: hideNoOverride(reason),
+ view: hideNoOverride(reason),
+ }
+}
+
+function hideNoOverride(reason: string) {
+ return {
+ behavior: ModerationBehaviorCode.Hide,
+ reason,
+ noOverride: true,
+ }
+}
+
+function hide(reason: string) {
+ return {
+ behavior: ModerationBehaviorCode.Hide,
+ reason,
+ }
+}
+
+function warn(reason: string) {
+ return {
+ behavior: ModerationBehaviorCode.Warn,
+ reason,
+ }
+}
+
+function warnContent(reason: string) {
+ return {
+ behavior: ModerationBehaviorCode.WarnContent,
+ reason,
+ }
+}
+
+function isMute(behavior: ModerationBehavior): ModerationBehavior {
+ behavior.isMute = true
+ return behavior
+}
+
+function warnImages(reason: string) {
+ return {
+ behavior: ModerationBehaviorCode.WarnImages,
+ reason,
+ }
+}
diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts
new file mode 100644
index 0000000000..1ee058024d
--- /dev/null
+++ b/src/lib/labeling/types.ts
@@ -0,0 +1,69 @@
+import {ComAtprotoLabelDefs, AppBskyGraphDefs} from '@atproto/api'
+import {LabelPreferencesModel} from 'state/models/ui/preferences'
+
+export type Label = ComAtprotoLabelDefs.Label
+
+export interface LabelValGroup {
+ id:
+ | keyof LabelPreferencesModel
+ | 'illegal'
+ | 'always-filter'
+ | 'always-warn'
+ | 'unknown'
+ title: string
+ isAdultImagery?: boolean
+ subtitle?: string
+ warning: string
+ values: string[]
+}
+
+export interface PostLabelInfo {
+ postLabels: Label[]
+ accountLabels: Label[]
+ profileLabels: Label[]
+ isMuted: boolean
+ mutedByList?: AppBskyGraphDefs.ListViewBasic
+ isBlocking: boolean
+ isBlockedBy: boolean
+}
+
+export interface ProfileLabelInfo {
+ accountLabels: Label[]
+ profileLabels: Label[]
+ isMuted: boolean
+ isBlocking: boolean
+ isBlockedBy: boolean
+}
+
+export enum ModerationBehaviorCode {
+ Show,
+ Hide,
+ Warn,
+ WarnContent,
+ WarnImages,
+}
+
+export interface ModerationBehavior {
+ behavior: ModerationBehaviorCode
+ isMute?: boolean
+ noOverride?: boolean
+ reason?: string
+}
+
+export interface AvatarModeration {
+ warn: boolean
+ blur: boolean
+}
+
+export interface PostModeration {
+ avatar: AvatarModeration
+ list: ModerationBehavior
+ thread: ModerationBehavior
+ view: ModerationBehavior
+}
+
+export interface ProfileModeration {
+ avatar: AvatarModeration
+ list: ModerationBehavior
+ view: ModerationBehavior
+}
diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts
index f4a96a22f0..cf43feca85 100644
--- a/src/lib/link-meta/bsky.ts
+++ b/src/lib/link-meta/bsky.ts
@@ -1,3 +1,4 @@
+import * as apilib from 'lib/api/index'
import {LikelyType, LinkMeta} from './link-meta'
// import {match as matchRoute} from 'view/routes'
import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
@@ -128,3 +129,29 @@ export async function getPostAsQuote(
},
}
}
+
+export async function getFeedAsEmbed(
+ store: RootStoreModel,
+ url: string,
+): Promise {
+ url = convertBskyAppUrlIfNeeded(url)
+ const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
+ const feed = makeRecordUri(user, 'app.bsky.feed.generator', rkey)
+ const res = await store.agent.app.bsky.feed.getFeedGenerator({feed})
+ return {
+ isLoading: false,
+ uri: feed,
+ meta: {
+ url: feed,
+ likelyType: LikelyType.AtpData,
+ title: res.data.view.displayName,
+ },
+ embed: {
+ $type: 'app.bsky.embed.record',
+ record: {
+ uri: res.data.view.uri,
+ cid: res.data.view.cid,
+ },
+ },
+ }
+}
diff --git a/src/lib/link-meta/link-meta.ts b/src/lib/link-meta/link-meta.ts
index 6c4ad53849..6863798b4c 100644
--- a/src/lib/link-meta/link-meta.ts
+++ b/src/lib/link-meta/link-meta.ts
@@ -1,8 +1,7 @@
-import he from 'he'
import {isBskyAppUrl} from '../strings/url-helpers'
import {RootStoreModel} from 'state/index'
import {extractBskyMeta} from './bsky'
-import {extractHtmlMeta} from './html'
+import {LINK_META_PROXY} from 'lib/constants'
export enum LikelyType {
HTML,
@@ -54,26 +53,29 @@ export async function getLinkMeta(
try {
const controller = new AbortController()
const to = setTimeout(() => controller.abort(), timeout || 5e3)
- const httpRes = await fetch(url, {
- headers: {accept: 'text/html'},
- signal: controller.signal,
- })
- const httpResBody = await httpRes.text()
+
+ const response = await fetch(
+ `${LINK_META_PROXY(
+ store.session.currentSession?.service || '',
+ )}${encodeURIComponent(url)}`,
+ )
+
+ const body = await response.json()
clearTimeout(to)
- const httpResMeta = extractHtmlMeta({
- html: httpResBody,
- hostname: urlp?.hostname,
- pathname: urlp?.pathname,
- })
- meta.title = httpResMeta.title ? he.decode(httpResMeta.title) : undefined
- meta.description = httpResMeta.description
- ? he.decode(httpResMeta.description)
- : undefined
- meta.image = httpResMeta.image
+
+ const {description, error, image, title} = body
+
+ if (error !== '') {
+ throw new Error(error)
+ }
+
+ meta.description = description
+ meta.image = image
+ meta.title = title
} catch (e) {
// failed
console.error(e)
- meta.error = 'Failed to fetch link'
+ meta.error = e instanceof Error ? e.toString() : 'Failed to fetch link'
}
return meta
diff --git a/src/lib/media/alt-text.ts b/src/lib/media/alt-text.ts
new file mode 100644
index 0000000000..4109f667a0
--- /dev/null
+++ b/src/lib/media/alt-text.ts
@@ -0,0 +1,12 @@
+import {RootStoreModel} from 'state/index'
+import {ImageModel} from 'state/models/media/image'
+
+export async function openAltTextModal(
+ store: RootStoreModel,
+ image: ImageModel,
+) {
+ store.shell.openModal({
+ name: 'alt-text-image',
+ image,
+ })
+}
diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts
index f77b861e20..c35953703a 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -1,56 +1,12 @@
import RNFetchBlob from 'rn-fetch-blob'
import ImageResizer from '@bam.tech/react-native-image-resizer'
-import {Image as RNImage, Share} from 'react-native'
+import {Image as RNImage, Share as RNShare} from 'react-native'
import {Image} from 'react-native-image-crop-picker'
-import RNFS from 'react-native-fs'
+import * as RNFS from 'react-native-fs'
import uuid from 'react-native-uuid'
-import * as Toast from 'view/com/util/Toast'
+import * as Sharing from 'expo-sharing'
import {Dimensions} from './types'
-import {POST_IMG_MAX} from 'lib/constants'
-import {isAndroid} from 'platform/detection'
-
-export async function compressAndResizeImageForPost(
- image: Image,
-): Promise {
- const uri = `file://${image.path}`
- let resized: Omit
-
- for (let i = 0; i < 9; i++) {
- const quality = 100 - i * 10
-
- try {
- resized = await ImageResizer.createResizedImage(
- uri,
- POST_IMG_MAX.width,
- POST_IMG_MAX.height,
- 'JPEG',
- quality,
- undefined,
- undefined,
- undefined,
- {mode: 'cover'},
- )
- } catch (err) {
- throw new Error(`Failed to resize: ${err}`)
- }
-
- if (resized.size < POST_IMG_MAX.size) {
- const path = await moveToPermanentPath(resized.path)
-
- return {
- path,
- mime: 'image/jpeg',
- size: resized.size,
- height: resized.height,
- width: resized.width,
- }
- }
- }
-
- throw new Error(
- `This image is too big! We couldn't compress it down to ${POST_IMG_MAX.size} bytes`,
- )
-}
+import {isAndroid, isIOS} from 'platform/detection'
export async function compressIfNeeded(
img: Image,
@@ -120,19 +76,33 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
}
export async function saveImageModal({uri}: {uri: string}) {
+ if (!(await Sharing.isAvailableAsync())) {
+ // TODO might need to give an error to the user in this case -prf
+ return
+ }
const downloadResponse = await RNFetchBlob.config({
fileCache: true,
}).fetch('GET', uri)
- const imagePath = downloadResponse.path()
- const base64Data = await downloadResponse.readFile('base64')
- const result = await Share.share({
- url: 'data:image/png;base64,' + base64Data,
- })
- if (result.action === Share.sharedAction) {
- Toast.show('Image saved to gallery')
- } else if (result.action === Share.dismissedAction) {
- // dismissed
+ // NOTE
+ // assuming PNG
+ // we're currently relying on the fact our CDN only serves pngs
+ // -prf
+
+ let imagePath = downloadResponse.path()
+ imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
+
+ // NOTE
+ // for some reason expo-sharing refuses to work on iOS
+ // ...and visa versa
+ // -prf
+ if (isIOS) {
+ await RNShare.share({url: imagePath})
+ } else {
+ await Sharing.shareAsync(imagePath, {
+ mimeType: 'image/png',
+ UTI: 'image/png',
+ })
}
RNFS.unlink(imagePath)
}
@@ -188,7 +158,7 @@ async function doResize(localUri: string, opts: DoResizeOpts): Promise {
)
}
-async function moveToPermanentPath(path: string): Promise {
+async function moveToPermanentPath(path: string, ext = ''): Promise {
/*
Since this package stores images in a temp directory, we need to move the file to a permanent location.
Relevant: IOS bug when trying to open a second time:
@@ -196,13 +166,28 @@ async function moveToPermanentPath(path: string): Promise {
*/
const filename = uuid.v4()
- const destinationPath = `${RNFS.TemporaryDirectoryPath}/${filename}`
+ const destinationPath = joinPath(
+ RNFS.TemporaryDirectoryPath,
+ `${filename}${ext}`,
+ )
await RNFS.moveFile(path, destinationPath)
return normalizePath(destinationPath)
}
-function normalizePath(str: string): string {
- if (isAndroid) {
+function joinPath(a: string, b: string) {
+ if (a.endsWith('/')) {
+ if (b.startsWith('/')) {
+ return a.slice(0, -1) + b
+ }
+ return a + b
+ } else if (b.startsWith('/')) {
+ return a + b
+ }
+ return a + '/' + b
+}
+
+function normalizePath(str: string, allPlatforms = false): string {
+ if (isAndroid || allPlatforms) {
if (!str.startsWith('file://')) {
return `file://${str}`
}
diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts
index 85f6b6138e..464802c32f 100644
--- a/src/lib/media/manip.web.ts
+++ b/src/lib/media/manip.web.ts
@@ -1,25 +1,6 @@
import {Dimensions} from './types'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {getDataUriSize, blobToDataUri} from './util'
-import {POST_IMG_MAX} from 'lib/constants'
-
-export async function compressAndResizeImageForPost({
- path,
- width,
- height,
-}: {
- path: string
- width: number
- height: number
-}): Promise {
- // Compression is handled in `doResize` via `quality`
- return await doResize(path, {
- width,
- height,
- maxSize: POST_IMG_MAX.size,
- mode: 'stretch',
- })
-}
export async function compressIfNeeded(
img: RNImage,
diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx
index e53dc42bea..9805c34642 100644
--- a/src/lib/media/picker.e2e.tsx
+++ b/src/lib/media/picker.e2e.tsx
@@ -2,7 +2,7 @@ import {RootStoreModel} from 'state/index'
import {Image as RNImage} from 'react-native-image-crop-picker'
import RNFS from 'react-native-fs'
import {CropperOptions} from './types'
-import {compressAndResizeImageForPost} from './manip'
+import {compressIfNeeded} from './manip'
let _imageCounter = 0
async function getFile() {
@@ -13,7 +13,7 @@ async function getFile() {
.join('/'),
)
const file = files[_imageCounter++ % files.length]
- return await compressAndResizeImageForPost({
+ return await compressIfNeeded({
path: file.path,
mime: 'image/jpeg',
size: file.size,
diff --git a/src/lib/media/picker.shared.ts b/src/lib/media/picker.shared.ts
new file mode 100644
index 0000000000..00b09c6b87
--- /dev/null
+++ b/src/lib/media/picker.shared.ts
@@ -0,0 +1,23 @@
+import {
+ ImagePickerOptions,
+ launchImageLibraryAsync,
+ MediaTypeOptions,
+} from 'expo-image-picker'
+import {getDataUriSize} from './util'
+
+export async function openPicker(opts?: ImagePickerOptions) {
+ const response = await launchImageLibraryAsync({
+ exif: false,
+ mediaTypes: MediaTypeOptions.Images,
+ quality: 1,
+ ...opts,
+ })
+
+ return (response.assets ?? []).map(image => ({
+ mime: 'image/jpeg',
+ height: image.height,
+ width: image.width,
+ path: image.uri,
+ size: getDataUriSize(image.uri),
+ }))
+}
diff --git a/src/lib/media/picker.tsx b/src/lib/media/picker.tsx
index af4a3e4d34..d0ee1ae223 100644
--- a/src/lib/media/picker.tsx
+++ b/src/lib/media/picker.tsx
@@ -1,12 +1,11 @@
import {
- openPicker as openPickerFn,
openCamera as openCameraFn,
openCropper as openCropperFn,
- ImageOrVideo,
+ Image as RNImage,
} from 'react-native-image-crop-picker'
import {RootStoreModel} from 'state/index'
-import {PickerOpts, CameraOpts, CropperOptions} from './types'
-import {Image as RNImage} from 'react-native-image-crop-picker'
+import {CameraOpts, CropperOptions} from './types'
+export {openPicker} from './picker.shared'
/**
* NOTE
@@ -17,31 +16,6 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
* -prf
*/
-export async function openPicker(
- _store: RootStoreModel,
- opts?: PickerOpts,
-): Promise {
- const items = await openPickerFn({
- mediaType: 'photo', // TODO: eventually add other media types
- multiple: opts?.multiple,
- maxFiles: opts?.maxFiles,
- forceJpg: true, // ios only
- compressImageQuality: 0.8,
- })
-
- const toMedia = (item: ImageOrVideo) => ({
- path: item.path,
- mime: item.mime,
- size: item.size,
- width: item.width,
- height: item.height,
- })
- if (Array.isArray(items)) {
- return items.map(toMedia)
- }
- return [toMedia(items)]
-}
-
export async function openCamera(
_store: RootStoreModel,
opts: CameraOpts,
@@ -55,6 +29,7 @@ export async function openCamera(
forceJpg: true, // ios only
compressImageQuality: 0.8,
})
+
return {
path: item.path,
mime: item.mime,
@@ -67,11 +42,10 @@ export async function openCamera(
export async function openCropper(
_store: RootStoreModel,
opts: CropperOptions,
-): Promise {
+) {
const item = await openCropperFn({
...opts,
forceJpg: true, // ios only
- compressImageQuality: 0.8,
})
return {
diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx
index 583f78a305..d12685b0c5 100644
--- a/src/lib/media/picker.web.tsx
+++ b/src/lib/media/picker.web.tsx
@@ -1,34 +1,9 @@
///
-import {PickerOpts, CameraOpts, CropperOptions} from './types'
+import {CameraOpts, CropperOptions} from './types'
import {RootStoreModel} from 'state/index'
-import {getImageDim} from 'lib/media/manip'
-import {extractDataUriMime} from './util'
import {Image as RNImage} from 'react-native-image-crop-picker'
-
-interface PickedFile {
- uri: string
- path: string
- size: number
-}
-
-export async function openPicker(
- _store: RootStoreModel,
- opts?: PickerOpts,
-): Promise {
- const res = await selectFile(opts)
- const dim = await getImageDim(res.uri)
- const mime = extractDataUriMime(res.uri)
- return [
- {
- path: res.uri,
- mime,
- size: res.size,
- width: dim.width,
- height: dim.height,
- },
- ]
-}
+export {openPicker} from './picker.shared'
export async function openCamera(
_store: RootStoreModel,
@@ -57,44 +32,3 @@ export async function openCropper(
})
})
}
-
-/**
- * Opens the select file dialog in the browser.
- * NOTE:
- * If in the future someone updates this method to use:
- * https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker
- * Check that the `showOpenFilePicker` API does not require any permissions
- * granted to use. As of this writing, it does not, but that could change
- * in the future. If the user does need to go through a permissions granting
- * flow, then checkout the usePhotoLibraryPermission() hook in
- * src/lib/hooks/usePermissions.ts
- * so that it gets appropriately updated.
- */
-function selectFile(opts?: PickerOpts): Promise {
- return new Promise((resolve, reject) => {
- var input = document.createElement('input')
- input.type = 'file'
- input.accept = opts?.mediaType === 'photo' ? 'image/*' : '*/*'
- input.onchange = e => {
- const target = e.target as HTMLInputElement
- const file = target?.files?.[0]
- if (!file) {
- return reject(new Error('Canceled'))
- }
-
- var reader = new FileReader()
- reader.readAsDataURL(file)
- reader.onload = readerEvent => {
- if (!readerEvent.target) {
- return reject(new Error('Canceled'))
- }
- resolve({
- uri: readerEvent.target.result as string,
- path: file.name,
- size: file.size,
- })
- }
- }
- input.click()
- })
-}
diff --git a/src/lib/media/util.ts b/src/lib/media/util.ts
index 75915de6b9..73f9748745 100644
--- a/src/lib/media/util.ts
+++ b/src/lib/media/util.ts
@@ -1,5 +1,3 @@
-import {Dimensions} from './types'
-
export function extractDataUriMime(uri: string): string {
return uri.substring(uri.indexOf(':') + 1, uri.indexOf(';'))
}
@@ -10,21 +8,6 @@ export function getDataUriSize(uri: string): number {
return Math.round((uri.length * 3) / 4)
}
-export function scaleDownDimensions(
- dim: Dimensions,
- max: Dimensions,
-): Dimensions {
- if (dim.width < max.width && dim.height < max.height) {
- return dim
- }
- const wScale = dim.width > max.width ? max.width / dim.width : 1
- const hScale = dim.height > max.height ? max.height / dim.height : 1
- if (wScale < hScale) {
- return {width: dim.width * wScale, height: dim.height * wScale}
- }
- return {width: dim.width * hScale, height: dim.height * hScale}
-}
-
export function isUriImage(uri: string) {
return /\.(jpg|jpeg|png).*$/.test(uri)
}
diff --git a/src/lib/merge-refs.ts b/src/lib/merge-refs.ts
new file mode 100644
index 0000000000..4617b5260d
--- /dev/null
+++ b/src/lib/merge-refs.ts
@@ -0,0 +1,27 @@
+/**
+ * This TypeScript function merges multiple React refs into a single ref callback.
+ * When developing low level UI components, it is common to have to use a local ref
+ * but also support an external one using React.forwardRef.
+ * Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility.
+ * Today a ref can be a function or an object, tomorrow it could be another thing, who knows.
+ * This utility handles compatibility for you.
+ * This function is inspired by https://github.com/gregberge/react-merge-refs
+ * @param refs - An array of React refs, which can be either `React.MutableRefObject` or
+ * `React.LegacyRef`. These refs are used to store references to DOM elements or React components.
+ * The `mergeRefs` function takes in an array of these refs and returns a callback function that
+ * @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and
+ * returns a ref callback function that can be used to merge multiple refs into a single ref.
+ */
+export function mergeRefs(
+ refs: Array | React.LegacyRef>,
+): React.RefCallback {
+ return value => {
+ refs.forEach(ref => {
+ if (typeof ref === 'function') {
+ ref(value)
+ } else if (ref != null) {
+ ;(ref as React.MutableRefObject).current = value
+ }
+ })
+ }
+}
diff --git a/src/lib/notifee.ts b/src/lib/notifee.ts
index 866319031f..42feb01c67 100644
--- a/src/lib/notifee.ts
+++ b/src/lib/notifee.ts
@@ -41,26 +41,26 @@ export function displayNotification(
}
export function displayNotificationFromModel(
- notif: NotificationsFeedItemModel,
+ notification: NotificationsFeedItemModel,
) {
let author = sanitizeDisplayName(
- notif.author.displayName || notif.author.handle,
+ notification.author.displayName || notification.author.handle,
)
let title: string
let body: string = ''
- if (notif.isLike) {
+ if (notification.isLike) {
title = `${author} liked your post`
- body = notif.additionalPost?.thread?.postRecord?.text || ''
- } else if (notif.isRepost) {
+ body = notification.additionalPost?.thread?.postRecord?.text || ''
+ } else if (notification.isRepost) {
title = `${author} reposted your post`
- body = notif.additionalPost?.thread?.postRecord?.text || ''
- } else if (notif.isMention) {
+ body = notification.additionalPost?.thread?.postRecord?.text || ''
+ } else if (notification.isMention) {
title = `${author} mentioned you`
- body = notif.additionalPost?.thread?.postRecord?.text || ''
- } else if (notif.isReply) {
+ body = notification.additionalPost?.thread?.postRecord?.text || ''
+ } else if (notification.isReply) {
title = `${author} replied to your post`
- body = notif.additionalPost?.thread?.postRecord?.text || ''
- } else if (notif.isFollow) {
+ body = notification.additionalPost?.thread?.postRecord?.text || ''
+ } else if (notification.isFollow) {
title = 'New follower!'
body = `${author} has followed you`
} else {
@@ -68,10 +68,12 @@ export function displayNotificationFromModel(
}
let image
if (
- AppBskyEmbedImages.isView(notif.additionalPost?.thread?.post.embed) &&
- notif.additionalPost?.thread?.post.embed.images[0]?.thumb
+ AppBskyEmbedImages.isView(
+ notification.additionalPost?.thread?.post.embed,
+ ) &&
+ notification.additionalPost?.thread?.post.embed.images[0]?.thumb
) {
- image = notif.additionalPost.thread.post.embed.images[0].thumb
+ image = notification.additionalPost.thread.post.embed.images[0].thumb
}
return displayNotification(title, body, image)
}
diff --git a/src/lib/routes/back-handler.ts b/src/lib/routes/back-handler.ts
new file mode 100644
index 0000000000..c4067c53e9
--- /dev/null
+++ b/src/lib/routes/back-handler.ts
@@ -0,0 +1,8 @@
+import {BackHandler} from 'react-native'
+import {RootStoreModel} from 'state/index'
+
+export function init(store: RootStoreModel) {
+ BackHandler.addEventListener('hardwareBackPress', () => {
+ return store.shell.closeAnyActiveElement()
+ })
+}
diff --git a/src/lib/routes/helpers.ts b/src/lib/routes/helpers.ts
index cfa6ae53be..cdac9039ac 100644
--- a/src/lib/routes/helpers.ts
+++ b/src/lib/routes/helpers.ts
@@ -11,7 +11,7 @@ export function getCurrentRoute(state: State) {
export function isStateAtTabRoot(state: State | undefined) {
if (!state) {
// NOTE
- // if state is not defined it's because init is occuring
+ // if state is not defined it's because init is occurring
// and therefore we can safely assume we're at root
// -prf
return true
@@ -20,6 +20,7 @@ export function isStateAtTabRoot(state: State | undefined) {
return (
isTab(currentRoute.name, 'Home') ||
isTab(currentRoute.name, 'Search') ||
+ isTab(currentRoute.name, 'Feeds') ||
isTab(currentRoute.name, 'Notifications') ||
isTab(currentRoute.name, 'MyProfile')
)
@@ -55,10 +56,15 @@ export function getTabState(state: State | undefined, tab: string): TabState {
return TabState.Outside
}
+type ExistingState = {
+ name: string
+ params?: RouteParams
+}
export function buildStateObject(
stack: string,
route: string,
params: RouteParams,
+ state: ExistingState[] = [],
) {
if (stack === 'Flat') {
return {
@@ -70,7 +76,7 @@ export function buildStateObject(
{
name: stack,
state: {
- routes: [{name: route, params}],
+ routes: [...state, {name: route, params}],
},
},
],
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index f8698f1cc1..4eb5e29d26 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -5,13 +5,21 @@ export type {NativeStackScreenProps} from '@react-navigation/native-stack'
export type CommonNavigatorParams = {
NotFound: undefined
+ Moderation: undefined
+ ModerationMuteLists: undefined
+ ModerationMutedAccounts: undefined
+ ModerationBlockedAccounts: undefined
+ DiscoverFeeds: undefined
Settings: undefined
Profile: {name: string; hideBackButton?: boolean}
ProfileFollowers: {name: string}
ProfileFollows: {name: string}
+ ProfileList: {name: string; rkey: string}
PostThread: {name: string; rkey: string}
PostLikedBy: {name: string; rkey: string}
PostRepostedBy: {name: string; rkey: string}
+ CustomFeed: {name: string; rkey: string}
+ CustomFeedLikedBy: {name: string; rkey: string}
Debug: undefined
Log: undefined
Support: undefined
@@ -19,11 +27,14 @@ export type CommonNavigatorParams = {
TermsOfService: undefined
CommunityGuidelines: undefined
CopyrightPolicy: undefined
+ AppPasswords: undefined
+ SavedFeeds: undefined
}
export type BottomTabNavigatorParams = CommonNavigatorParams & {
HomeTab: undefined
SearchTab: undefined
+ FeedsTab: undefined
NotificationsTab: undefined
MyProfileTab: undefined
}
@@ -36,6 +47,10 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
Search: {q?: string}
}
+export type FeedsTabNavigatorParams = CommonNavigatorParams & {
+ Feeds: undefined
+}
+
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined
}
@@ -47,6 +62,7 @@ export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined
Search: {q?: string}
+ Feeds: undefined
Notifications: undefined
}
@@ -55,6 +71,8 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Home: undefined
SearchTab: undefined
Search: {q?: string}
+ FeedsTab: undefined
+ Feeds: undefined
NotificationsTab: undefined
Notifications: undefined
MyProfileTab: undefined
diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts
new file mode 100644
index 0000000000..c5d1d3eb61
--- /dev/null
+++ b/src/lib/sentry.ts
@@ -0,0 +1,46 @@
+import {isNative, isWeb} from 'platform/detection'
+import {FC} from 'react'
+import * as Sentry from 'sentry-expo'
+
+// Sentry Initialization
+
+export const getRoutingInstrumentation = () => {
+ return new Sentry.Native.ReactNavigationInstrumentation() // initialize this in `onReady` prop of NavigationContainer
+}
+
+Sentry.init({
+ dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432',
+ enableInExpoDevelopment: false, // if true, Sentry will try to send events/errors in development mode.
+ debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production
+ environment: __DEV__ ? 'development' : 'production', // Set the environment
+ enableAutoPerformanceTracking: true, // Enable auto performance tracking
+ tracesSampleRate: 0.5, // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. // TODO: this might be too much in production
+ integrations: isNative
+ ? [
+ new Sentry.Native.ReactNativeTracing({
+ shouldCreateSpanForRequest: url => {
+ // Do not create spans for outgoing requests to a `/logs` endpoint as it is too noisy due to expo
+ return !url.match(/\/logs$/)
+ },
+ routingInstrumentation: getRoutingInstrumentation(),
+ }),
+ ]
+ : [], // no integrations for web, yet
+})
+
+// if web, use Browser client, otherwise use Native client
+export function getSentryClient() {
+ if (isWeb) {
+ return Sentry.Browser
+ }
+ return Sentry.Native
+}
+
+// wrap root App component with Sentry for automatic touch event tracking and performance monitoring
+export function withSentry(Component: FC) {
+ if (isWeb) {
+ return Component // .wrap is not required or available for web
+ }
+ const sentryClient = getSentryClient()
+ return sentryClient.wrap(Component)
+}
diff --git a/src/lib/sharing.ts b/src/lib/sharing.ts
new file mode 100644
index 0000000000..b294d74649
--- /dev/null
+++ b/src/lib/sharing.ts
@@ -0,0 +1,24 @@
+import {isIOS, isAndroid} from 'platform/detection'
+// import * as Sharing from 'expo-sharing'
+import Clipboard from '@react-native-clipboard/clipboard'
+import * as Toast from '../view/com/util/Toast'
+import {Share} from 'react-native'
+
+/**
+ * This function shares a URL using the native Share API if available, or copies it to the clipboard
+ * and displays a toast message if not (mostly on web)
+ * @param {string} url - A string representing the URL that needs to be shared or copied to the
+ * clipboard.
+ */
+export async function shareUrl(url: string) {
+ if (isAndroid) {
+ Share.share({message: url})
+ } else if (isIOS) {
+ Share.share({url})
+ } else {
+ // React Native Share is not supported by web. Web Share API
+ // has increasing but not full support, so default to clipboard
+ Clipboard.setString(url)
+ Toast.show('Copied to clipboard')
+ }
+}
diff --git a/src/lib/strings/display-names.ts b/src/lib/strings/display-names.ts
index 5b58dec3d0..b98153732d 100644
--- a/src/lib/strings/display-names.ts
+++ b/src/lib/strings/display-names.ts
@@ -6,7 +6,22 @@ const CHECK_MARKS_RE = /[\u2705\u2713\u2714\u2611]/gu
export function sanitizeDisplayName(str: string): string {
if (typeof str === 'string') {
- return str.replace(CHECK_MARKS_RE, '')
+ return str.replace(CHECK_MARKS_RE, '').trim()
}
return ''
}
+
+export function combinedDisplayName({
+ handle,
+ displayName,
+}: {
+ handle?: string
+ displayName?: string
+}): string {
+ if (!handle) {
+ return ''
+ }
+ return displayName
+ ? `${sanitizeDisplayName(displayName)} (@${handle})`
+ : `@${handle}`
+}
diff --git a/src/lib/strings/errors.ts b/src/lib/strings/errors.ts
index 0efcad335c..0c11a6706c 100644
--- a/src/lib/strings/errors.ts
+++ b/src/lib/strings/errors.ts
@@ -19,5 +19,9 @@ export function cleanError(str: any): string {
export function isNetworkError(e: unknown) {
const str = String(e)
- return str.includes('Abort') || str.includes('Network request failed')
+ return (
+ str.includes('Abort') ||
+ str.includes('Network request failed') ||
+ str.includes('Failed to fetch')
+ )
}
diff --git a/src/lib/strings/headings.ts b/src/lib/strings/headings.ts
new file mode 100644
index 0000000000..a88a696458
--- /dev/null
+++ b/src/lib/strings/headings.ts
@@ -0,0 +1,4 @@
+export function bskyTitle(page: string, unreadCountLabel?: string) {
+ const unreadPrefix = unreadCountLabel ? `(${unreadCountLabel}) ` : ''
+ return `${unreadPrefix}${page} - Bluesky`
+}
diff --git a/src/lib/strings/rich-text-detection.ts b/src/lib/strings/rich-text-detection.ts
index 51d09ec5d2..931617cd19 100644
--- a/src/lib/strings/rich-text-detection.ts
+++ b/src/lib/strings/rich-text-detection.ts
@@ -27,7 +27,7 @@ export function detectLinkables(text: string): DetectedLinkable[] {
matchValue = matchValue.slice(1)
}
- // strip ending puncuation
+ // strip ending punctuation
if (/[.,;!?]$/.test(matchValue)) {
matchValue = matchValue.slice(0, -1)
}
diff --git a/src/lib/strings/time.ts b/src/lib/strings/time.ts
index 8357d3c314..588b844598 100644
--- a/src/lib/strings/time.ts
+++ b/src/lib/strings/time.ts
@@ -27,3 +27,25 @@ export function ago(date: number | string | Date): string {
return new Date(ts).toLocaleDateString()
}
}
+
+export function niceDate(date: number | string | Date) {
+ const d = new Date(date)
+ return `${d.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })} at ${d.toLocaleTimeString(undefined, {
+ hour: 'numeric',
+ minute: '2-digit',
+ })}`
+}
+
+export function getAge(birthDate: Date): number {
+ var today = new Date()
+ var age = today.getFullYear() - birthDate.getFullYear()
+ var m = today.getMonth() - birthDate.getMonth()
+ if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
+ age--
+ }
+ return age
+}
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index 17a49fb265..d6d43b89d6 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -66,6 +66,10 @@ export function isBskyAppUrl(url: string): boolean {
return url.startsWith('https://bsky.app/')
}
+export function isExternalUrl(url: string): boolean {
+ return !isBskyAppUrl(url) && url.startsWith('http')
+}
+
export function isBskyPostUrl(url: string): boolean {
if (isBskyAppUrl(url)) {
try {
@@ -78,6 +82,18 @@ export function isBskyPostUrl(url: string): boolean {
return false
}
+export function isBskyCustomFeedUrl(url: string): boolean {
+ if (isBskyAppUrl(url)) {
+ try {
+ const urlp = new URL(url)
+ return /profile\/(?[^/]+)\/feed\/(?[^/]+)/i.test(
+ urlp.pathname,
+ )
+ } catch {}
+ }
+ return false
+}
+
export function convertBskyAppUrlIfNeeded(url: string): string {
if (isBskyAppUrl(url)) {
try {
@@ -90,6 +106,15 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
return url
}
+export function listUriToHref(url: string): string {
+ try {
+ const {hostname, rkey} = new AtUri(url)
+ return `/profile/${hostname}/lists/${rkey}`
+ } catch {
+ return ''
+ }
+}
+
export function getYoutubeVideoId(link: string): string | undefined {
let url
try {
diff --git a/src/lib/styles.ts b/src/lib/styles.ts
index 37d1696794..fb631c0bff 100644
--- a/src/lib/styles.ts
+++ b/src/lib/styles.ts
@@ -1,4 +1,4 @@
-import {StyleProp, StyleSheet, TextStyle} from 'react-native'
+import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native'
import {Theme, TypographyVariant} from './ThemeContext'
import {isMobileWeb} from 'platform/detection'
@@ -52,6 +52,7 @@ export const colors = {
green5: '#082b03',
unreadNotifBg: '#ebf6ff',
+ brandBlue: '#0066FF',
}
export const gradients = {
@@ -118,15 +119,19 @@ export const s = StyleSheet.create({
mr2: {marginRight: 2},
mr5: {marginRight: 5},
mr10: {marginRight: 10},
+ mr20: {marginRight: 20},
ml2: {marginLeft: 2},
ml5: {marginLeft: 5},
ml10: {marginLeft: 10},
+ ml20: {marginLeft: 20},
mt2: {marginTop: 2},
mt5: {marginTop: 5},
mt10: {marginTop: 10},
+ mt20: {marginTop: 20},
mb2: {marginBottom: 2},
mb5: {marginBottom: 5},
mb10: {marginBottom: 10},
+ mb20: {marginBottom: 20},
// paddings
p2: {padding: 2},
@@ -149,6 +154,7 @@ export const s = StyleSheet.create({
pb5: {paddingBottom: 5},
pb10: {paddingBottom: 10},
pb20: {paddingBottom: 20},
+ px5: {paddingHorizontal: 5},
// flex
flexRow: {flexDirection: 'row'},
@@ -164,6 +170,10 @@ export const s = StyleSheet.create({
w100pct: {width: '100%'},
h100pct: {height: '100%'},
hContentRegion: isMobileWeb ? {flex: 1} : {height: '100%'},
+ window: {
+ width: Dimensions.get('window').width,
+ height: Dimensions.get('window').height,
+ },
// text align
textLeft: {textAlign: 'left'},
@@ -209,6 +219,8 @@ export const s = StyleSheet.create({
green3: {color: colors.green3},
green4: {color: colors.green4},
green5: {color: colors.green5},
+
+ brandBlue: {color: colors.brandBlue},
})
export function lh(
diff --git a/src/lib/themes.ts b/src/lib/themes.ts
index 76d4fbf2f5..95aee0842f 100644
--- a/src/lib/themes.ts
+++ b/src/lib/themes.ts
@@ -291,8 +291,8 @@ export const darkTheme: Theme = {
palette: {
...defaultTheme.palette,
default: {
- background: colors.gray8,
- backgroundLight: colors.gray6,
+ background: colors.black,
+ backgroundLight: colors.gray7,
text: colors.white,
textLight: colors.gray3,
textInverted: colors.black,
@@ -307,7 +307,7 @@ export const darkTheme: Theme = {
replyLineDot: colors.gray6,
unreadNotifBg: colors.blue7,
unreadNotifBorder: colors.blue6,
- postCtrl: '#61657A',
+ postCtrl: '#707489',
brandText: '#0085ff',
emptyStateIcon: colors.gray4,
},
diff --git a/src/lib/type-assertions.ts b/src/lib/type-assertions.ts
new file mode 100644
index 0000000000..6b5db51247
--- /dev/null
+++ b/src/lib/type-assertions.ts
@@ -0,0 +1,3 @@
+export const getKeys = Object.keys as (
+ obj: T,
+) => Array
diff --git a/src/locale/en/community-guidelines.tsx b/src/locale/en/community-guidelines.tsx
deleted file mode 100644
index 5a8069998a..0000000000
--- a/src/locale/en/community-guidelines.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import React from 'react'
-import {H3, H4, P, UL, LI, A, EM, OL} from 'view/com/util/Html'
-
-export default function () {
- return (
- <>
- Last Updated: 2023/04/06
-
- The Bluesky app is built on a decentralized social networking protocol,
- the AT Protocol (atproto). Atproto is an open protocol that supports
- many different kinds of services. Our mission for the Bluesky app is to
- foster a vibrant and evolving community that respects individual
- preferences and adapts to the changing needs of our users. With this in
- mind, we have established the following goals for our community
- guidelines:
-
-
-
- Empower user choice: We strive to provide users with the ability to
- select self-governing services on the AT protocol that align with
- their personal preferences and values. This includes making it easy
- for others to run their own services, and for users to migrate between
- services.
-
-
- Cultivate a welcoming environment: Our aim is to create a safe and
- friendly space on bsky.social, the server we run, where new users feel
- welcome and supported, and where we ourselves enjoy participating. To
- help achieve this vision, we have implemented moderation systems
- guided by the following policies.
-
-
- Maintain up-to-date guidelines: As our user base grows and changes,
- our community guidelines must evolve as well. We will regularly review
- and update these guidelines in response to feedback from our users,
- emerging trends, and changing circumstances, and will strive to
- maintain transparency about any changes to our policies.
-
-
-
- In the following sections, we will dive into the specific policies that
- make up our server and community guidelines.
-
- Server Guidelines
-
- Our server guidelines outline the content policies we have established
- for the material hosted on our infrastructure at bsky.social. These
- policies have been selected to minimize potential risks and costs
- associated with hosting certain types of content, since we have limited
- developer resources and want to focus them on improving the AT Protocol.
-
-
- It is important to note that other servers within the atproto network
- may have different server-level rules. If you find that our policies do
- not align with your preferences, we encourage you to explore alternative
- servers or create your own. If you initially choose to use bsky.social
- but later decide that our policies do not suit your needs, you will soon
- be able to seamlessly migrate your account between servers.
-
-
- No illegal content or transactions
-
-
-
- Don’t share, promote, or engage in any illegal activities or
- transactions on our platform. This includes, but is not limited to,
- sharing copyrighted material without permission, distributing illicit
- substances, or participating in any form of illegal trade.
-
-
-
- Don’t break our infrastructure
-
-
-
- If you find a vulnerability, please report it to us at
- support@bsky.app, and don’t use it to exploit or take down our
- infrastructure.
-
-
- Community Guidelines
-
- Our community guidelines are designed to promote a safe and enjoyable
- experience for all users on our server. These policies serve as an
- additional layer on top of our server-level guidelines and are intended
- to foster a positive and respectful environment. For some community
- guidelines, we plan to offer content filters that you can adjust
- according to your preferences, allowing you to view content that has
- been initially filtered out. Please be aware that these rules will
- evolve over time as we continually work to cultivate a healthy and
- thriving community.
-
-
- Be polite and respectful
-
-
-
- Don’t harass, use slurs, threaten violence, or attack people
-
-
-
- Don’t spam
-
-
-
- Don’t repeatedly post the same message, or excessively promote
- anything
-
-
-
- Don’t abuse the reporting system
-
-
-
- Don’t use the reporting tool to spam, harass users, or submit
- unfounded or trivial complaints. The reporting system is in place to
- address genuine concerns and maintain the safety and integrity of our
- community, so please use it responsibly.
-
-
- Enforcement
-
- Our goal is to provide a flexible environment that balances the freedom
- and safety of our users. Violations of server or community guidelines
- may result in a flag, a warning, an account suspension until you migrate
- away from our service, or a permanent account suspension and ban from
- our services.{' '}
-
- References:
-
- Twitter:{' '}
-
- https://help.twitter.com/en/rules-and-policies/twitter-rules
-
-
-
- Reddit:{' '}
-
- https://www.redditinc.com/policies/content-policy
-
-
-
- Discord:{' '}
-
- https://discord.com/guidelines
-
-
-
- Discord TOS:{' '}
- https://discord.com/terms
-
- >
- )
-}
diff --git a/src/locale/en/copyright-policy.tsx b/src/locale/en/copyright-policy.tsx
deleted file mode 100644
index 3e995b5d0f..0000000000
--- a/src/locale/en/copyright-policy.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from 'react'
-import {H3, H4, P, UL, LI, A} from 'view/com/util/Html'
-
-export default function () {
- return (
- <>
- Last Updated: 2023/04/06
- Notification of Copyright Infringement
-
- Bluesky, PBLLC d.b.a. Bluesky (“Bluesky”) respects the
- intellectual property rights of others and expects its users to do the
- same.
-
-
- It is Bluesky’s policy, in appropriate circumstances and at its
- discretion, to disable the accounts of users who repeatedly infringe the
- copyrights of others.
-
-
- In accordance with the Digital Millennium Copyright Act of 1998, the
- text of which may be found on the U.S. Copyright Office website at{' '}
-
- http://www.copyright.gov/legislation/dmca.pdf
-
- , Bluesky will respond expeditiously to claims of copyright infringement
- committed using the Bluesky website, app, or other Bluesky owned or
- controlled online networ k services accessible through a mobile
- device or other type of device (the “Sites”) that are
- reported to Bluesky’s Designated Copyright Agent, identified in
- the sample notice below.
-
-
- If you are a copyright owner, or are authorized to act on behalf of one,
- or authorized to act under any exclusive right under copyright, please
- report alleged copyright infringements taking place on or through the
- Sites by completing the following DMCA Notice of Alleged Infringement
- and delivering it to Bluesky’s Designated Copyright Agent. Upon
- receipt of the Notice as described below, Bluesky will take whatever
- action, in its sole discretion, it deems appropriate, including removal
- of the challenged material from the Sites.
-
- DMCA Notice of Alleged Infringement (“Notice”)
-
- 1. Identify the copyrighted work that you claim has been infringed, or
- – if multiple copyrighted works are covered by this Notice –
- you may provide a representative list of the copyrighted works that you
- claim have been infringed.
-
-
- 2. Identify the material that you claim is infringing (or to be the
- subject of infringing activity) and that is to be removed or access to
- which is to be disabled, and information reasonably sufficient to permit
- us to locate the material, including at a minimum, if applicable, the
- URL of the link shown on the Site(s) where such material may be found.
-
-
- 3. Provide your mailing address, telephone number, and email address.
-
-
- 4. Include both of the following statements in the body of the Notice:
-
-
-
- “I hereby state that I have a good faith belief that the
- disputed use of the copyrighted material is not authorized by the
- copyright owner, its agent, or the law (e.g., as a fair use).”
-
-
- “I hereby state that the information in this Notice is accurate
- and, under penalty of perjury, that I am the owner, or authorized to
- act on behalf of the owner, of the copyright or of an exclusive right
- under the copyright that is allegedly infringed.”
-
-
-
- 5. Provide your full legal name and your electronic or physical
- signature.
-
-
- Deliver this Notice, with all items completed, to Bluesky’s
- Designated Copyright Agent:
-
- Copyright Agent
- c/o Bluesky, PBLLC
- support@bsky.app
- >
- )
-}
diff --git a/src/locale/en/privacy-policy.tsx b/src/locale/en/privacy-policy.tsx
deleted file mode 100644
index c0bae56f67..0000000000
--- a/src/locale/en/privacy-policy.tsx
+++ /dev/null
@@ -1,606 +0,0 @@
-import React from 'react'
-import {H2, H4, P, UL, LI, A} from 'view/com/util/Html'
-
-export default function () {
- return (
- <>
- Last Updated: 2023/02/02
-
- This Privacy Policy is designed to help you understand how Bluesky,
- PBLLC d.b.a. Bluesky (“Bluesky,” “we,” “us,” or “our”)
- collects, uses, processes, and shares your personal information, and to
- help you understand and exercise your privacy rights.{' '}
-
- 1. SCOPE AND UPDATES TO THIS PRIVACY POLICY
- 2. PERSONAL INFORMATION WE COLLECT
- 3. HOW WE USE YOUR PERSONAL INFORMATION
- 4. HOW WE DISCLOSE YOUR PERSONAL INFORMATION
- 5. YOUR PRIVACY CHOICES AND RIGHTS
- 6. SECURITY OF YOUR INFORMATION
- 7. INTERNATIONAL DATA TRANSFERS
- 8. RETENTION OF PERSONAL INFORMATION
- 9. SUPPLEMENTAL NOTICE FOR NEVADA RESIDENTS
- 10. CHILDREN’S INFORMATION
- 11. OTHER PROVISIONS
- 12. CONTACT US
-
- 1. SCOPE AND UPDATES TO THIS PRIVACY POLICY
-
- This Privacy Policy applies to personal information processed by us,
- including on our websites, mobile applications, and other online or
- offline offerings. To make this Privacy Policy easier to read, our
- websites, mobile applications, and other offerings are collectively
- called the “Services.”
-
-
- Changes to our Privacy Policy. We may revise this Privacy Policy from
- time to time in our sole discretion. If there are any material changes
- to this Privacy Policy, we will notify you as required by applicable
- law. You understand and agree that you will be deemed to have accepted
- the updated Privacy Policy if you continue to use our Services after the
- new Privacy Policy takes effect.
-
- 2. PERSONAL INFORMATION WE COLLECT
-
- The categories of personal information we collect depend on how you
- interact with us, our Services, and the requirements of applicable law.
- We collect information that you provide to us, information we obtain
- automatically when you use our Services, and information from other
- sources such as third-party services and organizations, as described
- below.
-
- 1. Personal Information You Provide to Us Directly
- We may collect personal information that you provide to us.
-
-
- Account Creation. We may collect personal information when you create
- an account with us, such as a username and password.
-
-
-
-
- Your Communications with Us. We may collect personal information, such
- as email address, phone number, or full name when you request
- information about our Services, request customer or technical support,
- or otherwise communicate with us.
-
-
-
-
- Interactive Features. We and others who use our Services may collect
- personal information that you submit or make available through our
- interactive features (e.g., messaging and chat features, commenting
- functionalities, forums, blogs, and social media pages). Any
- information you provide using the public sharing features of the
- Services will be considered “public,” unless otherwise required by
- applicable law, and is not subject to the privacy protections
- referenced herein. Please exercise caution before revealing any
- information that may identify you in the real world to other users.
-
-
- 2. Personal Information Collected Automatically
-
- We may collect personal information automatically when you use our
- Services.
-
-
-
- Automatic Collection of Personal Information. We may collect certain
- information automatically when you use our Services, such as your
- Internet protocol (IP) address, user settings, cookie identifiers,
- mobile carrier, mobile advertising and other unique identifiers,
- browser or device information, and Internet service provide. We may
- also automatically collect information regarding your use of our
- Services, such as pages that you visit before, during and after using
- our Services, information about the links you click, the types of
- content you interact with, the frequency and duration of your
- activities, and other information about how you use our Services.
-
-
-
- Crash Reports. If you provide crash reports, we may collect personal
- information related to such crash reports, including detailed
- diagnostic information about your device and the activities that led
- to the crash.
-
-
-
-
-
- Cookie Policy (and Other Technologies). We, as well as third
- parties that provide content, or other functionality on our Services,
- may use cookies, pixel tags, and other technologies (“Technologies”)
- to automatically collect information through your use of our Services.
-
-
-
- Cookies. Cookies are small text files placed in device browsers that
- store preferences and facilitate and enhance your experience.
-
-
-
-
- Pixel Tags/Web Beacons. A pixel tag (also known as a web beacon) is
- a piece of code embedded in our Services that collects information
- about engagement on our Services. The use of a pixel tag allows us
- to record, for example, that a user has visited a particular web
- page or clicked on a particular advertisement. We may also include
- web beacons in e-mails to understand whether messages have been
- opened, acted on, or forwarded.
-
-
-
-
- Our uses of these Technologies fall into the following general
- categories:
-
-
-
- Operationally Necessary. This includes Technologies that allow you
- access to our Services, applications, and tools that are required to
- identify irregular website behavior, prevent fraudulent activity,
- improve security, or allow you to make use of our functionality;
-
-
-
-
- Performance-Related. We may use Technologies to assess the performance
- of our Services, including as part of our analytic practices to help
- us understand how individuals use our Services (see Analytics below);
-
-
-
-
- Functionality-Related. We may use Technologies that allow us to offer
- you enhanced functionality when accessing or using our Services. This
- may include identifying you when you sign into our Services or keeping
- track of your specified preferences, interests, or past items viewed;
-
-
-
- See “Your Privacy Choices and Rights” below to understand your choices
- regarding these Technologies.
-
-
-
- Analytics. We may use Technologies and other third-party tools to
- process analytics information on our Services. These Technologies
- allow us to better understand how our digital Services are used and to
- continually improve and personalize our Services. Some of our
- analytics partners include:
-
-
-
- Segment.io. We use Segment's event tracking services to
- aggregate, archive, and distribute website and application usage
- information. For more information about how Segment uses your
- personal information, please visit{' '}
-
- https://www.twilio.com/legal/privacy
-
- .
-
-
- Mixpanel. We use Mixpanel's analytics and event tracking
- services to record and analyze website and application usage. For
- more information about how Mixpanel uses your personal information,
- please visit{' '}
-
- https://mixpanel.com/legal/privacy-policy
-
- .
-
-
- DataDog. We use DataDog's monitoring, tracing, and logging
- services to record application and server metrics, logs, and related
- debugging information. For more information about how DataDog uses
- your personal information, please visit{' '}
-
- https://www.datadoghq.com/legal/privacy/
-
- .
-
-
-
-
-
- Social Media Platforms. Our Services may contain social media
- buttons, such as Twitter, GitHub, Matrix, which might include widgets
- such as the “share this” button or other interactive mini programs).
- These features may collect personal information such as your IP
- address and which page you are visiting on our Services, and may set a
- cookie to enable the feature to function properly. Your interactions
- with these platforms are governed by the privacy policy of the company
- providing it.
-
-
- 3. Personal Information Collected from Other Sources
-
- Third-Party Services and Sources. We may obtain personal information
- about you from other sources, including through third-party services and
- organizations. For example, if you access our Services through a
- third-party application, such as an app store, a third-party login
- service, or a social networking site, we may collect personal
- information about you from that third-party application that you have
- made available via your privacy settings.
-
-
- Referrals and Sharing Features. Our Services may offer various
- tools and functionalities that allow you to provide personal information
- about your friends through our referral service. Third parties may also
- use the Services to upload personal information about you, including
- when they tag you. Our referral services may also allow you to forward
- or share certain content with a friend or colleague, such as an email
- inviting your friend to use our Services. Please only share with us
- contact information of people with whom you have a relationship (e.g.,
- relative, friend, neighbor, or co-worker).
-
- 3. HOW WE USE YOUR PERSONAL INFORMATION
-
- We use your personal information for a variety of business purposes,
- including to provide our Services, for administrative purposes, and to
- market our products and Services, as described below.
-
- 1. Provide Our Services
-
- We use your information to fulfill our contract with you and provide you
- with our Services, such as:
-
-
- Managing your information and accounts;
-
- Providing access to certain areas, functionalities, and features of
- our Services;
-
- Answering requests for customer or technical support;
-
- Communicating with you about your account, activities on our Services,
- and policy changes;
-
-
- Processing your financial information and other payment methods for
- products or Services purchased;
-
- Allowing you to register for events.
-
- 2. Administrative Purposes
-
- We use your information for various administrative purposes, such as:
-
-
-
- Pursuing our legitimate interests such as direct marketing, research
- and development (including marketing research), network and
- information security, and fraud prevention;
-
-
- Detecting security incidents, protecting against malicious, deceptive,
- fraudulent or illegal activity, and prosecuting those responsible for
- that activity;
-
- Measuring interest and engagement in our Services;
-
- Short-term, transient use, such as contextual customization of ads;
-
- Improving, upgrading, or enhancing our Services;
- Developing new products and services;
- Ensuring internal quality control and safety;
-
-
-
- Authenticating and verifying individual identities, including requests
- to exercise your rights under this Privacy Policy;
-
- Debugging to identify and repair errors with our Services;
-
- Auditing relating to interactions, transactions, and other compliance
- activities;
-
-
- Sharing personal information with third parties as needed to provide
- the Services;
-
- Enforcing our agreements and policies; and
-
- Carrying out activities that are required to comply with our legal
- obligations.
-
-
- 3. With Your Consent
-
- We may use personal information for other purposes that are clearly
- disclosed to you at the time you provide personal information or with
- your consent.
-
- 4. Other Purposes
-
- We also use your personal information for other purposes as requested by
- you or as permitted by applicable law.
-
-
-
- De-identified and Aggregated Information. We may use personal
- information to create de-identified and/or aggregated information,
- such as demographic information, information about the device from
- which you access our Services, or other analyses we create.{' '}
-
-
- 4. HOW WE DISCLOSE YOUR PERSONAL INFORMATION
-
- We disclose your personal information to third parties for a variety of
- business purposes, including to provide our Services, to protect us or
- others, or in the event of a major business transaction such as a
- merger, sale, or asset transfer, as described below.
-
- 1. Disclosures to Provide our Services
-
- The categories of third parties with whom we may share your personal
- information are described below.
-
-
-
- Service Providers. We may share your personal information with our
- third-party service providers and vendors that assist us with the
- provision of our Services. This includes service providers and vendors
- that provide us with IT support, hosting, payment processing, customer
- service, and related services.
-
-
-
-
- Business Partners. We may share your personal information with
- business partners to provide you with a product or service you have
- requested. We may also share your personal information with business
- partners with whom we jointly offer products or services.
-
-
-
-
- Other Users or Third Parties You Share or Interact With. As described
- above in “Personal Information We Collect,” our Services may allow you
- to share personal information or interact with other users and third
- parties (including individuals and third parties who do not use our
- Services and the general public).
-
-
-
-
- APIs/SDKs. We may use third-party application program interfaces
- (“APIs”) and software development kits (“SDKs”) as part of the
- functionality of our Services. For more information about our use of
- APIs and SDKs, please contact us as set forth in “ Contact
- Us” below.
-
-
- 2. Disclosures to Protect Us or Others
-
- We may access, preserve, and disclose any information we store
- associated with you to external parties if we, in good faith, believe
- doing so is required or appropriate to: comply with law enforcement or
- national security requests and legal process, such as a court order or
- subpoena; protect your, our, or others’ rights, property, or
- safety; enforce our policies or contracts; collect amounts owed to us;
- or assist with an investigation or prosecution of suspected or actual
- illegal activity.
-
-
- 3. Disclosure in the Event of Merger, Sale, or Other Asset Transfers
-
-
- If we are involved in a merger, acquisition, financing due diligence,
- reorganization, bankruptcy, receivership, purchase or sale of assets, or
- transition of service to another provider, your information may be sold
- or transferred as part of such a transaction, as permitted by law and/or
- contract.
-
- 5. YOUR PRIVACY CHOICES AND RIGHTS
-
- Your Privacy Choices. The privacy choices you may have about your
- personal information are determined by applicable law and are described
- below.
-
-
-
- Email Communications. If you receive an unwanted email from us, you
- can use the unsubscribe link found at the bottom of the email to opt
- out of receiving future emails. Note that you will continue to receive
- transaction-related emails regarding products or Services you have
- requested. We may also send you certain non-promotional communications
- regarding us and our Services, and you will not be able to opt out of
- those communications (e.g., communications regarding our Services or
- updates to our Terms or this Privacy Policy).
-
-
-
-
- Mobile Devices. We may send you push notifications through our mobile
- application. You may opt out from receiving these push notifications
- by changing the settings on your mobile device. With your consent, we
- may also collect precise location-based information via our mobile
- application. You may opt out of this collection by changing the
- settings on your mobile device.
-
-
-
-
- “Do Not Track.” Do Not Track (“DNT”) is a privacy preference that
- users can set in certain web browsers. Please note that we do not
- respond to or honor DNT signals or similar mechanisms transmitted by
- web browsers.
-
-
-
- Your Privacy Rights. In accordance with applicable law, you may have the
- right to:
-
-
-
- Access to and Portability of Your Personal Information, including: (i)
- confirming whether we are processing your personal information; (ii)
- obtaining access to or a copy of your personal information; and (iii)
- receiving an electronic copy of personal information that you have
- provided to us, or asking us to send that information to another
- company in a structured, commonly used, and machine readable format
- (also known as the “right of data portability”);
-
-
-
-
- Request Correction of your personal information where it is inaccurate
- or incomplete. In some cases, we may provide self-service tools that
- enable you to update your personal information;
-
-
-
- Request Deletion of your personal information;
-
-
-
- Request Restriction of or Object to our processing of your
- personal information where the processing of your personal information
- is based on our legitimate interest or for direct marketing purposes;
- and
-
-
-
-
- Withdraw your Consent to our processing of your personal
- information. Please note that your withdrawal will only take effect
- for future processing, and will not affect the lawfulness of
- processing before the withdrawal.
-
-
-
- If you would like to exercise any of these rights, please contact us as
- set forth in “Contact Us” below. We will process such requests in
- accordance with applicable laws.
-
- 6. SECURITY OF YOUR INFORMATION
-
- We take steps to ensure that your information is treated securely and in
- accordance with this Privacy Policy. Unfortunately, no system is
- 100% secure, and we cannot ensure or warrant the security of any
- information you provide to us. To the fullest extent permitted by
- applicable law, we do not accept liability for unauthorized access, use,
- disclosure, or loss of personal information.
-
-
- By using our Services or providing personal information to us, you agree
- that we may communicate with you electronically regarding security,
- privacy, and administrative issues relating to your use of our Services.
- If we learn of a security system’s breach, we may attempt to
- notify you electronically by posting a notice on our Services, by mail,
- or by sending an email to you.
-
- 7. INTERNATIONAL DATA TRANSFERS
-
- All information processed by us may be transferred, processed, and
- stored anywhere in the world, including, but not limited to, the United
- States or other countries, which may have data protection laws that are
- different from the laws where you live. We endeavor to safeguard your
- information consistent with the requirements of applicable laws.
-
-
- If we transfer personal information which originates in the European
- Economic Area, Switzerland, and/or the United Kingdom to a country that
- has not been found to provide an adequate level of protection under
- applicable data protection laws, one of the safeguards we may use to
- support such transfer is the{' '}
-
- EU Standard Contractual Clauses
-
- .
-
-
- For more information about the safeguards we use for international
- transfers of your personal information, please contact us as set forth
- below.
-
- 8. RETENTION OF PERSONAL INFORMATION
-
- We store the personal information we collect as described in this
- Privacy Policy for as long as you use our Services, or as necessary to
- fulfill the purpose(s) for which it was collected, provide our Services,
- resolve disputes, establish legal defenses, conduct audits, pursue
- legitimate business purposes, enforce our agreements, and comply with
- applicable laws.
-
- 9. SUPPLEMENTAL NOTICE FOR NEVADA RESIDENTS
-
- If you are a resident of Nevada, you have the right to opt-out of the
- sale of certain personal information to third parties who intend to
- license or sell that personal information. You can exercise this right
- by contacting us at{' '}
- support@bsky.app with the subject
- line “Nevada Do Not Sell Request” and providing us with your name and
- the email address associated with your account. Please note that we do
- not currently sell your personal information as sales are defined in
- Nevada Revised Statutes Chapter 603A. If you have any questions, please
- contact us as set forth in Contact Us below.
-
- 10. CHILDREN’S INFORMATION
-
- The Services are not directed to children under 13 (or other age as
- required by local law), and we do not knowingly collect personal
- information from children.
-
-
- If you are a parent or guardian and believe your child has uploaded
- personal information to our site without your consent, you may contact
- us as described in “Contact Us” below. If we become aware that a child
- has provided us with personal information in violation of applicable
- law, we will delete any personal information we have collected, unless
- we have a legal obligation to keep it, and terminate the child’s
- account.
-
- 11. OTHER PROVISIONS
-
- Third-Party Websites/Applications. The Services may contain links to
- other websites/applications and other websites/applications may
- reference or link to our Services. These third-party services are not
- controlled by us. We encourage our users to read the privacy policies of
- each website and application with which they interact. We do not
- endorse, screen, or approve, and are not responsible for, the privacy
- practices or content of such other websites or applications. Providing
- personal information to third-party websites or applications is at your
- own risk.
-
-
- Supervisory Authority. If your personal information is subject to
- the applicable data protection laws of the European Economic Area,
- Switzerland, or the United Kingdom, you have the right to lodge a
- complaint with the competent supervisory authority or attorney general
- if you believe our processing of your personal information violates
- applicable law.
-
-
- 12. CONTACT US
-
- Bluesky is the controller of the personal information we process under
- this Privacy Policy.
-
-
- If you have any questions about our privacy practices or this Privacy
- Policy, or to exercise your rights as detailed in this Privacy Policy,
- please contact us at:{' '}
- support@bsky.app
-
- >
- )
-}
diff --git a/src/locale/en/terms-of-service.tsx b/src/locale/en/terms-of-service.tsx
deleted file mode 100644
index 8219cae217..0000000000
--- a/src/locale/en/terms-of-service.tsx
+++ /dev/null
@@ -1,723 +0,0 @@
-import React from 'react'
-import {H4, P, OL, LI, A, STRONG, EM, UL} from 'view/com/util/Html'
-
-export default function () {
- return (
- <>
- Last Updated: 2023/04/06
-
- Welcome to the Bluesky, PBLLC d.b.a. Bluesky (“Bluesky”,
- “we”, or “us”) website located at{' '}
- bsky.social (“Site”), the
- Authenticated Transfer social protocol (“Protocol”) and our
- mobile application (“App”). Please read these Terms of
- Service (the “Terms”) and our{' '}
- Privacy Policy {' '}
- (“Privacy Policy”) carefully because they govern your use of
- our Site, Protocol, App, and our content accessible therein. In
- addition, please read the{' '}
-
- Bluesky Community Guidelines
- {' '}
- (the “Bluesky Community Guidelines”), which are incorporated
- by reference and included in the Terms. To make these Terms easier to
- read, our Site, Protocol, and App, and our content and services provided
- therein, are collectively called the “Services.” The
- Services are not official products and have not been commercially or
- publicly released by Bluesky.
-
-
- IMPORTANT NOTICE REGARDING ARBITRATION: WHEN YOU AGREE TO THESE TERMS
- YOU ARE AGREEING (WITH LIMITED EXCEPTION) TO RESOLVE ANY DISPUTE BETWEEN
- YOU AND BLUESKY THROUGH BINDING, INDIVIDUAL ARBITRATION RATHER THAN IN
- COURT. PLEASE REVIEW CAREFULLY SECTION 18 “DISPUTE
- RESOLUTION” BELOW FOR DETAILS REGARDING ARBITRATION. HOWEVER, IF
- YOU ARE A RESIDENT OF A JURISDICTION WHERE APPLICABLE LAW PROHIBITS
- ARBITRATION OF DISPUTES, THE AGREEMENT TO ARBITRATE IN SECTION 18
- WILL NOT APPLY TO YOU BUT THE PROVISIONS OF SECTION 17
- “GOVERNING LAW AND FORUM CHOICE” WILL APPLY INSTEAD.
-
-
-
- Agreement to Terms. By using our Services, you agree
- to be bound by these Terms. If you don’t agree to be bound by
- these Terms, do not use the Services. If you are accessing and using
- the Services on behalf of a company (such as your employer) or other
- legal entity, you represent and warrant that you have the authority to
- bind that company or other legal entity to these Terms. In that case,
- “you” and “your” will refer to that company or
- other legal entity.
-
-
- Acknowledgment of Beta Services. You acknowledge and
- agree that: (a) the Services are not official products and have not
- been commercially or publicly released by Bluesky; (b) the Services
- may not operate properly, be in final form or be fully functional; (c)
- the Services may contain errors, design flaws or other problems; (d)
- it may not be possible to make the Services fully functional; (e) the
- information obtained using the Services may not be accurate; (f) use
- of the Services may result in unexpected results, loss of data or
- communications, project delays or other unpredictable damage or loss;
- (g) Bluesky is under no obligation to release a commercial or public
- version of the Services; and (h) Bluesky has the right to unilaterally
- to abandon development of the Services, at any time and without any
- obligation or liability to you.
-
-
- Privacy Policy. Please refer to our{' '}
- Privacy Policy for
- information on how we collect, use and share your information. You
- acknowledge and agree that your use of the Services is subject to our
- Privacy Policy.
-
-
- Changes to these Terms or the Services. We may update
- these Terms from time to time at our sole discretion. If we do,
- we’ll let you know by posting the updated Terms on the Site,
- Protocol or the App and may also send other communications. It’s
- important that you review these Terms whenever we update them or you
- use the Services. If you continue to use the Services after we have
- posted updated Terms it means that you accept and agree to the
- changes. If you don’t agree to be bound by the changes, you may
- not use the Services anymore. Because our Services are evolving over
- time we may change or discontinue all or any part of the Services, at
- any time and without notice, at our sole discretion. We may add,
- remove, suspend or alter access to any content available through or on
- the Services at any time and make no guarantee as to the availability
- or minimum amount of specific content.{' '}
-
-
- Who May Use the Services?
-
-
- Eligibility . To use the Services, you must be at least 13
- years of age and not otherwise barred from using the Services
- under applicable law. If you are over 13 years of age but under
- the age of majority in your respective jurisdiction, you hereby
- represent and warrant that your parent or legal guardian has read
- these Terms and accepts them on your behalf. Parents and legal
- guardians are responsible for the acts of their minor children
- when using the Services, whether or not the parent or guardian has
- authorized such acts.
-
-
- Registration and Your Information . If you want to use
- certain features of the Services you’ll have to create an
- account (“Account”) via the Services.
-
-
- Accuracy of Account Information . It’s important
- that you provide us with accurate, complete and current
- information for your Account and you agree to keep this
- information up-to-date. If you don’t, we might have to
- suspend or terminate your Account. You agree that you won’t
- disclose your Account password to anyone and you’ll notify
- us immediately of any unauthorized use of your Account.
- You’re responsible for all activities that occur under your
- Account, including, without limitation, the posting of User
- Content (as defined below), and any communications or other
- contact you have with other users of the Services, whether or not
- you know about them. We may take actions we deem reasonably
- necessary to prevent fraud and abuse, including placing
- restrictions on user accounts or on the amount of content that can
- be accessed from the Services at any one time.
-
-
-
-
- Feedback. We welcome feedback, comments and
- suggestions for improvements to the Services (“Feedback”).
- You can submit Feedback by posting in the App or emailing us at
- support@bsky.app. You grant us a non-exclusive, transferable,
- worldwide, perpetual, irrevocable, fully-paid, royalty-free license,
- with the right to sublicense, under any and all intellectual property
- rights that you own or control, to use, copy, modify, create
- derivative works based upon and otherwise exploit the Feedback for any
- purpose.
-
-
- Content Ownership, Responsibility and Removal.
-
-
- Definitions . For purposes of these Terms: (i){' '}
- “Content” means text, graphics,
- images, music, software, audio, video, works of authorship of any
- kind, and information or other materials that are posted,
- generated, provided or otherwise made available through the
- Services; and (ii) “User Content” {' '}
- means any Content that Account holders (including you) provide or
- make available through the Services. User Content also includes,
- without limitation, any communications or content that you share
- with another user of the Services such as comments on other
- users’ User Content or information you provide or make
- available through the Services. Content includes, without
- limitation, User Content.
-
-
- Our Content Ownership . Except for any licensed rights
- granted under these Terms, Bluesky does not claim any ownership
- rights in any User Content and nothing in these Terms will be
- deemed to restrict any rights that you may have to use and exploit
- your User Content. Subject to the foregoing, Bluesky and its
- licensors exclusively own all right, title and interest in and to
- the Services and Content, including all associated intellectual
- property rights and all features, trademarks, trade names, service
- marks, trade dress, and the look and feel of the Services. You
- acknowledge that the Services and Content are protected by
- copyright, trademark, and other laws of the United States and
- foreign countries. You agree not to remove, alter or obscure any
- copyright, trademark, service mark or other proprietary rights
- notices incorporated in or accompanying the Services or Content.
-
-
- Rights in Content Granted by Bluesky . Subject to your
- compliance with these Terms, Bluesky grants to you a limited,
- non-exclusive, non-transferable license, with no right to
- sublicense, to download, view, copy, display and print the Content
- solely in connection with your permitted use of the Services and
- solely for your personal and non-commercial purposes. Other than
- the right to use the Services as explicitly described in these
- Terms for your personal, limited use, no other rights are granted
- to you under these Terms.{' '}
-
-
- Rights in User Content Granted by You to Us . By making
- any User Content available through the Services, you hereby grant
- to Bluesky and its subsidiaries, affiliates, licensee, successors,
- and assigns (the “Bluesky Parties”) an irrevocable,
- non-exclusive, perpetual, transferable, worldwide, royalty-free
- license, with the right to sublicense (through multiple tiers of
- sub-licensing), to use, copy, modify, adapt, crop, edit, creative
- derivative works, distribute, publicly display, publicly perform
- and otherwise exploit in any media now known or hereafter devised,
- your User Content, in whole or in part, in connection with (i)
- providing the Services and Content to you and to others; (ii)
- promote and market Bluesky and our Services, including without
- limitation through Bluesky’s owned, operated, and/or branded
- social media channels. For example, Bluesky may create
- compilations of Content made available by Account holders
- (including your User Content), and/or use User Content or such
- compilations to promote the App through Bluesky’s operated,
- and/or branded social media channels, without further payment or
- consideration by Bluesky. However, without your prior consent,
- Bluesky will not use your User Content in any Content that is
- sponsored by a third party.
-
-
- Name, Likeness, Other Personal Rights . By submitting User
- Content in which you may appear, including without limitation your
- photograph, you hereby grant to (i) Bluesky and the Bluesky
- Parties and (ii) other Account holders, an irrevocable,
- non-exclusive, perpetual, transferable, worldwide, royalty-free,
- unlimited license to use your name, image, likeness, or other
- information or materials supplied by you, including any third
- party materials as they appear in such User Content, consistent
- with the rights granted by you in this Section 7.
-
-
- Your Responsibility for User Content . You are solely
- responsible for all of your User Content. You represent and
- warrant that your User Content is original, that you own your User
- Content or you have all rights that are necessary to grant us and
- the Bluesky Parties the license rights in your User Content under
- these Terms. This includes the rights to the name, likeness or
- other publicity rights of any other party appearing in your User
- Content. You also represent and warrant that neither your User
- Content, nor your use and provision of your User Content to be
- made available through the Services, nor any exercise of any
- rights granted by you in such User Content will (i) conflict with
- any rights or commitments granted by you to any other party; (ii)
- infringe, misappropriate or violate a third party’s
- intellectual property rights, or rights of publicity or privacy,
- or (iii) result in the violation of any applicable law or
- regulation. You represent and warrant that all of your User
- Content and your activities in connection with the Services will,
- at all times, comply with (i) these Terms; (ii) all applicable
- laws, rules, and regulations; and (iii) any other guidelines or
- requirements that we may make available to you from time to time.
-
-
- Removal of User Content . We have the right, in our sole
- discretion, to remove any User Content. You can remove your User
- Content by specifically deleting it. However, in certain
- instances, some of your User Content (such as posts or comments
- you make) may not be completely removed and copies of your User
- Content may continue to exist on the Services. We are not
- responsible or liable for the removal or deletion of (or the
- failure to remove or delete) any of your User Content. Except as
- expressly stated herein, you acknowledge and agree that we have no
- obligation to provide, monitor, edit, upload, or remove any of
- your User Content, although we have the right to do so.
-
-
-
-
- Respecting Others’ User Content . Subject to our
- rights under Section 7, you acknowledge that all User Content and any
- content related thereto are the property of the respective user that
- makes the User Content available through the Services or, if made
- explicit on the Services, Bluesky. You acknowledge and agree that
- Bluesky is not responsible or liable for your User Content or any
- direct message through the Services between any user of the Services
- and another user of the Services (“Communications”),
- including you.
-
-
- Rights and Terms for Apps.
-
-
- Rights in App Granted by Bluesky . Subject to your
- compliance with these Terms, Bluesky grants to you a limited,
- non-exclusive, non-transferable license, with no right to
- sublicense, to download and install the App on a mobile device
- that you own or control and to run the App solely for your own
- personal non-commercial purposes. Except as expressly permitted in
- these Terms, you may not: (i) copy, modify or create
- derivative works based on the App; (ii) sublicense, lease,
- lend or rent the App to any third party; (iii) decompile or
- disassemble the App; or (iv) make the functionality of the
- App available to multiple users through any means. Bluesky
- reserves all rights in and to the App not expressly granted to you
- under these Terms. Certain portions of the App may be subject to
- an open source license agreement, as expressly designated within
- the App or on the Site. Such license will govern the use of such
- portions of the App to the extent that such license agreement
- conflicts with or is inconsistent with this Section 9(a) (for
- example, if the license grants broader use rights).
-
-
- Accessing App from App Store . The following terms apply
- to any App accessed through or downloaded from any app store or
- distribution platform (like the Apple App Store) where the App may
- now or in the future be made available (each, an “App
- Provider”). You acknowledge and agree that:
-
-
- These Terms are concluded between you and Bluesky, and not
- with the App Provider, and Bluesky (not the App Provider), is
- solely responsible for the App.
-
-
- The App Provider has no obligation to furnish any maintenance
- and support services with respect to the App.
-
-
- In the event of any failure of the App to conform to any
- applicable warranty, you may notify the App Provider and, to
- the maximum extent permitted by applicable law, the App
- Provider will have no other warranty obligation whatsoever
- with respect to the App. Any other claims, losses,
- liabilities, damages, costs or expenses attributable to any
- failure to conform to any warranty will be the sole
- responsibility of Bluesky.
-
-
- The App Provider is not responsible for addressing any claims
- you have or any claims of any third party relating to the App
- or your possession and use of the App, including, but not
- limited to: (i) product liability claims; (ii) any claim
- that the App fails to conform to any applicable legal or
- regulatory requirement; and (iii) claims arising under
- consumer protection or similar legislation.
-
-
- In the event of any third party claim that the App or your
- possession and use of that App infringes that third
- party’s intellectual property rights, Bluesky will be
- solely responsible for the investigation, defense, settlement
- and discharge of any such intellectual property infringement
- claim to the extent required by these Terms.
-
-
- The App Provider, and its subsidiaries, are third-party
- beneficiaries of these Terms as related to your license to the
- App, and upon your acceptance of these Terms, the App Provider
- will have the right (and will be deemed to have accepted the
- right) to enforce these Terms against you as a third-party
- beneficiary thereof.
-
-
- You represent and warrant that (i) you are not located in a
- country that is subject to a U.S. Government embargo, or that
- has been designated by the U.S. Government as a
- terrorist-supporting country; and (ii) you are not listed on
- any U.S. Government list of prohibited or restricted parties.
-
-
- You must also comply with all applicable third-party terms of
- service when using the App.
-
-
-
-
-
-
-
- General Prohibitions and Bluesky’s Enforcement Rights
-
- . You agree not to do any of the following:
-
-
- Post, upload, publish, submit or transmit any content that:
- (i) infringes, misappropriates or violates a third
- party’s patent, copyright, trademark, trade secret, moral
- rights or other intellectual property rights, or rights of
- publicity or privacy; or (ii) violates any applicable law or
- regulation or would give rise to civil liability;
-
-
- Use, display, mirror or frame the Services, Bluesky’s name,
- any Bluesky trademark, logo or other proprietary information,
- without Bluesky’s express written consent;
-
-
- Access, tamper with, or use non-public areas of the Services;
-
-
- Attempt to probe, scan or test the vulnerability of any Bluesky
- system or network or breach any security or authentication
- measures without reporting such vulnerability or breach to
- support@bsky.app, except as part of community testing initiatives
- authorized by Bluesky;
-
-
- Avoid, bypass, remove, deactivate, impair, descramble or otherwise
- circumvent any technological measure implemented by Bluesky or any
- of Bluesky’s providers or any other third party (including
- another user) to protect the Services, including any service
- protection or usage limits;
-
-
- Attempt to access or search the Services or download content from
- the Services using any engine, software, tool, agent, device or
- mechanism (including spiders, robots, crawlers, data mining tools
- or the like) that imposes unreasonable burdens on the Services or
- that we otherwise deem abusive or harmful;
-
- Send any junk mail or spam;
-
- Use any meta tags or other hidden text or metadata utilizing a
- Bluesky trademark, logo URL or product name without
- Bluesky’s express written consent;
-
-
- Use the Services, or any portion thereof, in any manner not
- permitted by these Terms;
-
-
- Interfere with, or attempt to interfere with, the access of any
- user, host or network, including, without limitation, sending a
- virus, overloading, flooding, spamming, or mail-bombing the
- Services;
-
-
- Collect or store any personally identifiable information from the
- Services from other users of the Services without their
- express permission;
-
-
- Impersonate or misrepresent your affiliation with any person or
- entity, claim a false affiliation, or misrepresent the source or
- identity of content used through the Services;
-
-
- You agree not to provide any information that is intended to
- misinform, misdirect, mislead, or otherwise deceive any users of
- the Services or any other third party;
-
- Violate any applicable law or regulation;
-
- Commercialize any User Content not in accordance with these Terms;
- or
-
-
- Directly or indirectly induce others to do any of the above.
-
-
-
- Bluesky is not obligated to monitor access to or use of the Services
- or to review or edit any content. However, we have the right to do
- so for the purpose of operating the Services, to ensure compliance
- with these Terms and to comply with applicable law or other legal
- requirements. We reserve the right, but are not obligated, to remove
- or disable access to any content, including User Content, at any
- time and without notice, including, but not limited to, if we, at
- our sole discretion, consider it objectionable or in violation of
- these Terms. We have the right to investigate violations of these
- Terms or conduct that affects the Services. We may also consult and
- cooperate with law enforcement authorities to prosecute users who
- violate the law.
-
-
-
- DMCA/Copyright Policy. Bluesky respects copyright law
- and expects its users to do the same. It is Bluesky’s policy to
- terminate in appropriate circumstances account holders who repeatedly
- infringe or are believed to be repeatedly infringing the rights of
- copyright holders. Please see Bluesky’s Copyright Policy at{' '}
- bsky.app/support/copyright for
- further information.
-
-
- Links to Third Party Websites or Resources. The
- Services (including the Site, Protocol and App) may allow you to
- access third-party websites or other resources. To the extent provided
- by us, we provide access only as a convenience and are not responsible
- for the content, products or services on or available from those
- resources or links displayed on such websites. Users of the Services
- may provide access through User Content to third-party websites or
- other resources. To the extent provided by you, you hereby acknowledge
- and agree that, as between you and Bluesky, you bear full
- responsibility and liability in connection with such access by users
- of the Services. You acknowledge sole responsibility for and assume
- all risk arising from, your use of any third-party resources. You
- further acknowledge that we have no responsibility to remove, add,
- modify, or monitor User Content, including any access to third-party
- websites or other resources contained in User Content.
-
-
- Termination. We may terminate or suspend, in whole or
- in part, your access to and use of the Services, including suspending
- access to or terminating your account, at our sole discretion, at any
- time and without notice to you. You may request deletion of your
- account, which we will consider on a case-by-case basis, by emailing
- support@bsky.app. Upon any termination, discontinuation or
- cancellation of the Services or your account, the following Sections
- will survive: 2, 7(a), 7(b), 7(d), 7(e), 7(f), 8, 10, 13, 14, 15, 16,
- 17, 18 and 19.
-
-
-
- Warranty Disclaimers. THE SERVICES ARE PROVIDED
- “AS IS,” WITHOUT WARRANTY OF ANY KIND. WITHOUT LIMITING
- THE FOREGOING, WE EXPLICITLY DISCLAIM ANY IMPLIED WARRANTIES OF
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT
- AND NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF
- DEALING OR USAGE OF TRADE. WE MAKE NO WARRANTY THAT THE SERVICES
- WILL MEET YOUR REQUIREMENTS OR BE AVAILABLE ON AN UNINTERRUPTED,
- SECURE, OR ERROR-FREE BASIS. WE MAKE NO WARRANTY REGARDING THE
- QUALITY, ACCURACY, TIMELINESS, TRUTHFULNESS, COMPLETENESS OR
- RELIABILITY OF ANY INFORMATION OR CONTENT ON THE SERVICES. WE
- ARE NOT RESPONSIBLE OR LIABLE FOR USER CONTENT, OR ANY
- COMMUNICATIONS, AND WE MAKE NO WARRANTY OR REPRESENTATION OF ANY
- KIND IN REGARD TO USER CONTENT.
-
-
- BLUESKY ASSUMES NO RESPONSIBILITY FOR ANY USER’S OR THIRD
- PARTY’S FAILURE TO COMPLY WITH ANY APPLICABLE LAWS AND
- REGULATIONS. WE EXPLICITLY DISCLAIM ALL LIABILITY FOR ANY ACT OR
- OMISSION OF ANY USER OR OTHER THIRD PARTY. WE DO NOT AND CANNOT
- CONTROL YOUR INTERACTION WITH ANY USER OR OTHER THIRD PARTY, AND WE
- EXPRESSLY DISCLAIM ANY LIABILITY ARISING FROM SUCH INTERACTION.
-
-
-
- Indemnity. You will indemnify and hold harmless
- Bluesky and its officers, directors, employees and agents, from and
- against any claims, disputes, demands, liabilities, damages, losses,
- and costs and expenses, including, without limitation, reasonable
- legal and accounting fees arising out of or in any way connected with
- (a) your access to or use of the Services, (b) your User Content, or
- (c) your violation of these Terms.
-
-
- Limitation of Liability.
-
-
- For the purposes of this Section 16, “Bluesky”,
- “we”, or “us” shall include Bluesky, its
- subsidiaries, affiliates, investors, agents, and successors and
- assigns.
-
-
- TO THE MAXIMUM EXTENT PERMITTED BY LAW, NEITHER BLUESKY NOR ANY
- OTHER PARTY INVOLVED IN CREATING, PRODUCING, OR DELIVERING THE
- SERVICES WILL BE LIABLE FOR ANY INCIDENTAL, SPECIAL, EXEMPLARY OR
- CONSEQUENTIAL DAMAGES, OR DAMAGES FOR LOST PROFITS, LOST REVENUES,
- LOST SAVINGS, LOST BUSINESS OPPORTUNITY, LOSS OF DATA OR GOODWILL,
- SERVICE INTERRUPTION, COMPUTER DAMAGE OR SYSTEM FAILURE OR THE
- COST OF SUBSTITUTE SERVICES OF ANY KIND ARISING OUT OF OR IN
- CONNECTION WITH THESE TERMS OR FROM THE USE OF OR INABILITY TO USE
- THE SERVICES OR FOR ANY ERROR OR DEFECT IN THE SERVICES, WHETHER
- BASED ON WARRANTY, CONTRACT, TORT (INCLUDING NEGLIGENCE), PRODUCT
- LIABILITY OR ANY OTHER LEGAL THEORY, AND WHETHER OR NOT BLUESKY OR
- ANY OTHER PARTY HAS BEEN INFORMED OF THE POSSIBILITY OF SUCH
- DAMAGE, EVEN IF A LIMITED REMEDY SET FORTH HEREIN IS FOUND TO HAVE
- FAILED OF ITS ESSENTIAL PURPOSE. YOU ACKNOWLEDGE THAT BLUESKY
- SHALL NOT BE RESPONSIBLE OR LIABLE FOR CONTENT OR THE DEFAMATORY,
- OFFENSIVE, OR ILLEGAL CONDUCT OF ANY THIRD PARTY OR ANY THIRD
- PARTY’S CONTENT (INCLUDING ANY USER CONTENT), WHETHER OR NOT
- SUCH CONTENT IS ACCESSED THROUGH THE SERVICES, AND THAT ANY RISK
- OF HARM OR DAMAGE FROM THE FOREGOING RESTS ENTIRELY WITH YOU.
-
-
- TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT
- WILL BLUESKY’S TOTAL LIABILITY ARISING OUT OF OR IN
- CONNECTION WITH THESE TERMS OR FROM THE USE OF OR INABILITY TO USE
- THE SERVICES EXCEED TWENTY DOLLARS ($20).
-
-
- THE EXCLUSIONS AND LIMITATIONS OF DAMAGES SET FORTH ABOVE ARE
- FUNDAMENTAL ELEMENTS OF THE BASIS OF THE BARGAIN BETWEEN BLUESKY
- AND YOU.
-
-
-
-
- Governing Law and Forum Choice. These Terms and any
- action related thereto will be governed by the Federal Arbitration
- Act, federal arbitration law, and the laws of the State of Delaware,
- without regard to its conflict of laws provisions. Except as otherwise
- expressly set forth in Section 17 “Dispute Resolution,”
- the exclusive jurisdiction for all Disputes (defined below) that you
- and Bluesky are not required to arbitrate will be the state and
- federal courts located in Delaware, and you and Bluesky each waive any
- objection to jurisdiction and venue in such courts.
-
-
- Dispute Resolution.
-
-
- Mandatory Arbitration of Disputes . We each agree that any
- dispute, claim or controversy arising out of or relating to these
- Terms or the breach, termination, enforcement, interpretation or
- validity thereof or the use of the Services (collectively,
- “Disputes”) will be resolved solely by binding,
- individual arbitration and not in a class, representative or
- consolidated action or proceeding . You and Bluesky agree that the
- U.S. Federal Arbitration Act governs the interpretation and
- enforcement of these Terms, and that you and Bluesky are each
- waiving the right to a trial by jury or to participate in a class
- action. This arbitration provision shall survive termination of
- these Terms.
-
-
- Exceptions . As limited exceptions to Section 18 (a)
- above: (i) we both may seek to resolve a Dispute in small
- claims court if it qualifies; and (ii) we each retain the right to
- seek injunctive or other equitable relief from a court to prevent
- (or enjoin) the infringement or misappropriation of our
- intellectual property rights.
-
-
-
- Conducting Arbitration and Arbitration Rules . The
- arbitration will be conducted by the American Arbitration
- Association (“AAA”) under its Consumer Arbitration
- Rules (the “AAA Rules”) then in effect, except as
- modified by these Terms. The AAA Rules are available at{' '}
- www.adr.org or by calling
- 1-800-778-7879. A party who wishes to start arbitration must
- submit a written Demand for Arbitration to AAA and give notice
- to the other party as specified in the AAA Rules. The AAA
- provides a form Demand for Arbitration at{' '}
-
- www.adr.org
-
- .
-
-
- Any arbitration hearings will take place in the county (or
- parish) where you live, unless we both agree to a different
- location. The parties agree that the arbitrator shall have
- exclusive authority to decide all issues relating to the
- interpretation, applicability, enforceability and scope of this
- arbitration agreement.
-
-
-
- Arbitration Costs . Payment of all filing, administration
- and arbitrator fees will be governed by the AAA Rules. If we
- prevail in arbitration we’ll pay all of our attorneys’
- fees and costs and won’t seek to recover them from you. If
- you prevail in arbitration you will be entitled to an award of
- attorneys’ fees and expenses to the extent provided under
- applicable law.
-
-
- Injunctive and Declaratory Relief . Except as provided in
- Section 18(b) above, the arbitrator shall determine all issues of
- liability on the merits of any claim asserted by either party and
- may award declaratory or injunctive relief only in favor of the
- individual party seeking relief and only to the extent necessary
- to provide relief warranted by that party’s individual
- claim. To the extent that you or we prevail on a claim and seek
- public injunctive relief (that is, injunctive relief that has the
- primary purpose and effect of prohibiting unlawful acts that
- threaten future injury to the public), the entitlement to and
- extent of such relief must be litigated in a civil court of
- competent jurisdiction and not in arbitration. The parties agree
- that litigation of any issues of public injunctive relief shall be
- stayed pending the outcome of the merits of any individual claims
- in arbitration.
-
-
- Class Action Waiver . YOU AND BLUESKY AGREE THAT EACH MAY
- BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL
- CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED
- CLASS OR REPRESENTATIVE PROCEEDING. Further, if the parties’
- Dispute is resolved through arbitration, the arbitrator may not
- consolidate another person’s claims with your claims, and
- may not otherwise preside over any form of a representative or
- class proceeding. If this specific provision is found to be
- unenforceable, then the entirety of this Dispute Resolution
- section shall be null and void.
-
-
- Severability . With the exception of any of the provisions
- in Section 18(f) of these Terms (“Class Action
- Waiver”), if an arbitrator or court of competent
- jurisdiction decides that any part of these Terms is invalid or
- unenforceable, the other parts of these Terms will still apply.
-
-
-
-
- General Terms.
-
-
- Entire Agreement . These Terms constitute the entire and
- exclusive understanding and agreement between Bluesky and you
- regarding the Services and Content, and these Terms supersede and
- replace all prior oral or written understandings or agreements
- between Bluesky and you regarding the Services and Content. If any
- provision of these Terms is held invalid or unenforceable by an
- arbitrator or a court of competent jurisdiction, that provision
- will be enforced to the maximum extent permissible and the other
- provisions of these Terms will remain in full force and effect.
- You may not assign or transfer these Terms, by operation of law or
- otherwise, without Bluesky’s prior written consent. Any
- attempt by you to assign or transfer these Terms, without such
- consent, will be null. Bluesky may freely assign or transfer these
- Terms without restriction. Subject to the foregoing, these Terms
- will bind and inure to the benefit of the parties, their
- successors and permitted assigns.
-
-
- Notices . Any notices or other communications provided by
- Bluesky under these Terms will be given: (i) via email; or
- (ii) by posting to the Services. For notices made by email,
- the date of receipt will be deemed the date on which such notice
- is transmitted.
-
-
- Waiver of Rights . Bluesky’s failure to enforce any
- right or provision of these Terms will not be considered a waiver
- of such right or provision. The waiver of any such right or
- provision will be effective only if in writing and signed by a
- duly authorized representative of Bluesky. Except as expressly set
- forth in these Terms, the exercise by either party of any of its
- remedies under these Terms will be without prejudice to its other
- remedies under these Terms or otherwise.
-
-
-
-
- Contact Information. If you have any questions about
- these Terms or the Services, please contact Bluesky at:
- support@bsky.app.
-
-
- >
- )
-}
diff --git a/src/locale/languages.ts b/src/locale/languages.ts
index 31c1a9f70a..269e2fa9ab 100644
--- a/src/locale/languages.ts
+++ b/src/locale/languages.ts
@@ -23,7 +23,7 @@ export const LANGUAGES: Language[] = [
{code3: 'alt', code2: '', name: 'Southern Altai'},
{code3: 'amh', code2: 'am', name: 'Amharic'},
{code3: 'ang', code2: '', name: 'English, Old (ca.450-1100)'},
- {code3: 'anp ', code2: 'Angika', name: 'angika'},
+ {code3: 'anp ', code2: 'Angika', name: 'Angika'},
{code3: 'apa', code2: '', name: 'Apache languages'},
{code3: 'ara', code2: 'ar', name: 'Arabic'},
{
diff --git a/src/platform/detection.ts b/src/platform/detection.ts
index 5d2ffcb22c..da33fdca7a 100644
--- a/src/platform/detection.ts
+++ b/src/platform/detection.ts
@@ -4,8 +4,9 @@ export const isIOS = Platform.OS === 'ios'
export const isAndroid = Platform.OS === 'android'
export const isNative = isIOS || isAndroid
export const isWeb = !isNative
+export const isMobileWebMediaQuery = 'only screen and (max-width: 1230px)'
export const isMobileWeb =
isWeb &&
// @ts-ignore we know window exists -prf
- global.window.matchMedia('only screen and (max-width: 1000px)')?.matches
+ global.window.matchMedia(isMobileWebMediaQuery)?.matches
export const isDesktopWeb = isWeb && !isMobileWeb
diff --git a/src/platform/polyfills.ts b/src/platform/polyfills.ts
index a64c2c33a6..d5028c294e 100644
--- a/src/platform/polyfills.ts
+++ b/src/platform/polyfills.ts
@@ -23,7 +23,7 @@ globalThis.atob = (str: string): string => {
)
}
- // Adding the padding if missing, for semplicity
+ // Adding the padding if missing, for simplicity
str += '=='.slice(2 - (str.length & 3))
var bitmap,
result = '',
diff --git a/src/routes.ts b/src/routes.ts
index 7ae281424b..54faba22d1 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -3,16 +3,27 @@ import {Router} from 'lib/routes/router'
export const router = new Router({
Home: '/',
Search: '/search',
+ Feeds: '/feeds',
+ DiscoverFeeds: '/search/feeds',
Notifications: '/notifications',
Settings: '/settings',
+ Moderation: '/moderation',
+ ModerationMuteLists: '/moderation/mute-lists',
+ ModerationMutedAccounts: '/moderation/muted-accounts',
+ ModerationBlockedAccounts: '/moderation/blocked-accounts',
Profile: '/profile/:name',
ProfileFollowers: '/profile/:name/followers',
ProfileFollows: '/profile/:name/follows',
+ ProfileList: '/profile/:name/lists/:rkey',
PostThread: '/profile/:name/post/:rkey',
PostLikedBy: '/profile/:name/post/:rkey/liked-by',
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
+ CustomFeed: '/profile/:name/feed/:rkey',
+ CustomFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
Debug: '/sys/debug',
Log: '/sys/log',
+ AppPasswords: '/settings/app-passwords',
+ SavedFeeds: '/settings/saved-feeds',
Support: '/support',
PrivacyPolicy: '/support/privacy',
TermsOfService: '/support/tos',
diff --git a/src/state/index.ts b/src/state/index.ts
index 4755c28f4c..42687a229a 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -7,7 +7,7 @@ import * as storage from 'lib/storage'
export const LOCAL_DEV_SERVICE =
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
-export const STAGING_SERVICE = 'https://pds.staging.bsky.dev'
+export const STAGING_SERVICE = 'https://staging.bsky.dev'
export const PROD_SERVICE = 'https://bsky.social'
export const DEFAULT_SERVICE = PROD_SERVICE
const ROOT_STATE_STORAGE_KEY = 'root'
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts
index bbfb9612bc..c30a68f4dd 100644
--- a/src/state/models/cache/image-sizes.ts
+++ b/src/state/models/cache/image-sizes.ts
@@ -16,6 +16,7 @@ export class ImageSizesCache {
if (Dimensions) {
return Dimensions
}
+
const prom =
this.activeRequests.get(uri) ||
new Promise(resolve => {
diff --git a/src/state/models/content/list-membership.ts b/src/state/models/content/list-membership.ts
new file mode 100644
index 0000000000..20d9b60afa
--- /dev/null
+++ b/src/state/models/content/list-membership.ts
@@ -0,0 +1,123 @@
+import {makeAutoObservable} from 'mobx'
+import {AtUri, AppBskyGraphListitem} from '@atproto/api'
+import {runInAction} from 'mobx'
+import {RootStoreModel} from '../root-store'
+
+const PAGE_SIZE = 100
+interface Membership {
+ uri: string
+ value: AppBskyGraphListitem.Record
+}
+
+interface ListitemRecord {
+ uri: string
+ value: AppBskyGraphListitem.Record
+}
+
+interface ListitemListResponse {
+ cursor?: string
+ records: ListitemRecord[]
+}
+
+export class ListMembershipModel {
+ // data
+ memberships: Membership[] = []
+
+ constructor(public rootStore: RootStoreModel, public subject: string) {
+ makeAutoObservable(
+ this,
+ {
+ rootStore: false,
+ },
+ {autoBind: true},
+ )
+ }
+
+ // public api
+ // =
+
+ async fetch() {
+ // NOTE
+ // this approach to determining list membership is too inefficient to work at any scale
+ // it needs to be replaced with server side list membership queries
+ // -prf
+ let cursor
+ let records: ListitemRecord[] = []
+ for (let i = 0; i < 100; i++) {
+ const res: ListitemListResponse =
+ await this.rootStore.agent.app.bsky.graph.listitem.list({
+ repo: this.rootStore.me.did,
+ cursor,
+ limit: PAGE_SIZE,
+ })
+ records = records.concat(
+ res.records.filter(record => record.value.subject === this.subject),
+ )
+ cursor = res.cursor
+ if (!cursor) {
+ break
+ }
+ }
+ runInAction(() => {
+ this.memberships = records
+ })
+ }
+
+ getMembership(listUri: string) {
+ return this.memberships.find(m => m.value.list === listUri)
+ }
+
+ isMember(listUri: string) {
+ return !!this.getMembership(listUri)
+ }
+
+ async add(listUri: string) {
+ if (this.isMember(listUri)) {
+ return
+ }
+ const res = await this.rootStore.agent.app.bsky.graph.listitem.create(
+ {
+ repo: this.rootStore.me.did,
+ },
+ {
+ subject: this.subject,
+ list: listUri,
+ createdAt: new Date().toISOString(),
+ },
+ )
+ const {rkey} = new AtUri(res.uri)
+ const record = await this.rootStore.agent.app.bsky.graph.listitem.get({
+ repo: this.rootStore.me.did,
+ rkey,
+ })
+ runInAction(() => {
+ this.memberships = this.memberships.concat([record])
+ })
+ }
+
+ async remove(listUri: string) {
+ const membership = this.getMembership(listUri)
+ if (!membership) {
+ return
+ }
+ const {rkey} = new AtUri(membership.uri)
+ await this.rootStore.agent.app.bsky.graph.listitem.delete({
+ repo: this.rootStore.me.did,
+ rkey,
+ })
+ runInAction(() => {
+ this.memberships = this.memberships.filter(m => m.value.list !== listUri)
+ })
+ }
+
+ async updateTo(uris: string[]) {
+ for (const uri of uris) {
+ await this.add(uri)
+ }
+ for (const membership of this.memberships) {
+ if (!uris.includes(membership.value.list)) {
+ await this.remove(membership.value.list)
+ }
+ }
+ }
+}
diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts
new file mode 100644
index 0000000000..3913d3e62c
--- /dev/null
+++ b/src/state/models/content/list.ts
@@ -0,0 +1,282 @@
+import {makeAutoObservable} from 'mobx'
+import {
+ AtUri,
+ AppBskyGraphGetList as GetList,
+ AppBskyGraphDefs as GraphDefs,
+ AppBskyGraphList,
+ AppBskyGraphListitem,
+} from '@atproto/api'
+import {Image as RNImage} from 'react-native-image-crop-picker'
+import {RootStoreModel} from '../root-store'
+import * as apilib from 'lib/api/index'
+import {cleanError} from 'lib/strings/errors'
+import {bundleAsync} from 'lib/async/bundle'
+
+const PAGE_SIZE = 30
+
+interface ListitemRecord {
+ uri: string
+ value: AppBskyGraphListitem.Record
+}
+
+interface ListitemListResponse {
+ cursor?: string
+ records: ListitemRecord[]
+}
+
+export class ListModel {
+ // state
+ isLoading = false
+ isRefreshing = false
+ hasLoaded = false
+ error = ''
+ loadMoreError = ''
+ hasMore = true
+ loadMoreCursor?: string
+
+ // data
+ list: GraphDefs.ListView | null = null
+ items: GraphDefs.ListItemView[] = []
+
+ static async createModList(
+ rootStore: RootStoreModel,
+ {
+ name,
+ description,
+ avatar,
+ }: {name: string; description: string; avatar: RNImage | null | undefined},
+ ) {
+ const record: AppBskyGraphList.Record = {
+ purpose: 'app.bsky.graph.defs#modlist',
+ name,
+ description,
+ avatar: undefined,
+ createdAt: new Date().toISOString(),
+ }
+ if (avatar) {
+ const blobRes = await apilib.uploadBlob(
+ rootStore,
+ avatar.path,
+ avatar.mime,
+ )
+ record.avatar = blobRes.data.blob
+ }
+ const res = await rootStore.agent.app.bsky.graph.list.create(
+ {
+ repo: rootStore.me.did,
+ },
+ record,
+ )
+ await rootStore.agent.app.bsky.graph.muteActorList({list: res.uri})
+ return res
+ }
+
+ constructor(public rootStore: RootStoreModel, public uri: string) {
+ makeAutoObservable(
+ this,
+ {
+ rootStore: false,
+ },
+ {autoBind: true},
+ )
+ }
+
+ get hasContent() {
+ return this.items.length > 0
+ }
+
+ get hasError() {
+ return this.error !== ''
+ }
+
+ get isEmpty() {
+ return this.hasLoaded && !this.hasContent
+ }
+
+ get isOwner() {
+ return this.list?.creator.did === this.rootStore.me.did
+ }
+
+ // public api
+ // =
+
+ async refresh() {
+ return this.loadMore(true)
+ }
+
+ loadMore = bundleAsync(async (replace: boolean = false) => {
+ if (!replace && !this.hasMore) {
+ return
+ }
+ this._xLoading(replace)
+ try {
+ const res = await this.rootStore.agent.app.bsky.graph.getList({
+ list: this.uri,
+ limit: PAGE_SIZE,
+ cursor: replace ? undefined : this.loadMoreCursor,
+ })
+ if (replace) {
+ this._replaceAll(res)
+ } else {
+ this._appendAll(res)
+ }
+ this._xIdle()
+ } catch (e: any) {
+ this._xIdle(replace ? e : undefined, !replace ? e : undefined)
+ }
+ })
+
+ async updateMetadata({
+ name,
+ description,
+ avatar,
+ }: {
+ name: string
+ description: string
+ avatar: RNImage | null | undefined
+ }) {
+ if (!this.list) {
+ return
+ }
+ if (!this.isOwner) {
+ throw new Error('Cannot edit this list')
+ }
+
+ // get the current record
+ const {rkey} = new AtUri(this.uri)
+ const {value: record} = await this.rootStore.agent.app.bsky.graph.list.get({
+ repo: this.rootStore.me.did,
+ rkey,
+ })
+
+ // update the fields
+ record.name = name
+ record.description = description
+ if (avatar) {
+ const blobRes = await apilib.uploadBlob(
+ this.rootStore,
+ avatar.path,
+ avatar.mime,
+ )
+ record.avatar = blobRes.data.blob
+ } else if (avatar === null) {
+ record.avatar = undefined
+ }
+ return await this.rootStore.agent.com.atproto.repo.putRecord({
+ repo: this.rootStore.me.did,
+ collection: 'app.bsky.graph.list',
+ rkey,
+ record,
+ })
+ }
+
+ async delete() {
+ if (!this.list) {
+ return
+ }
+
+ // fetch all the listitem records that belong to this list
+ let cursor
+ let records: ListitemRecord[] = []
+ for (let i = 0; i < 100; i++) {
+ const res: ListitemListResponse =
+ await this.rootStore.agent.app.bsky.graph.listitem.list({
+ repo: this.rootStore.me.did,
+ cursor,
+ limit: PAGE_SIZE,
+ })
+ records = records.concat(
+ res.records.filter(record => record.value.list === this.uri),
+ )
+ cursor = res.cursor
+ if (!cursor) {
+ break
+ }
+ }
+
+ // batch delete the list and listitem records
+ const createDel = (uri: string) => {
+ const urip = new AtUri(uri)
+ return {
+ $type: 'com.atproto.repo.applyWrites#delete',
+ collection: urip.collection,
+ 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)),
+ ),
+ })
+ }
+
+ async subscribe() {
+ if (!this.list) {
+ return
+ }
+ await this.rootStore.agent.app.bsky.graph.muteActorList({
+ list: this.list.uri,
+ })
+ await this.refresh()
+ }
+
+ async unsubscribe() {
+ if (!this.list) {
+ return
+ }
+ await this.rootStore.agent.app.bsky.graph.unmuteActorList({
+ list: this.list.uri,
+ })
+ await this.refresh()
+ }
+
+ /**
+ * Attempt to load more again after a failure
+ */
+ async retryLoadMore() {
+ this.loadMoreError = ''
+ this.hasMore = true
+ return this.loadMore()
+ }
+
+ // state transitions
+ // =
+
+ _xLoading(isRefreshing = false) {
+ this.isLoading = true
+ this.isRefreshing = isRefreshing
+ this.error = ''
+ }
+
+ _xIdle(err?: any, loadMoreErr?: any) {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = true
+ this.error = cleanError(err)
+ this.loadMoreError = cleanError(loadMoreErr)
+ if (err) {
+ this.rootStore.log.error('Failed to fetch user items', err)
+ }
+ if (loadMoreErr) {
+ this.rootStore.log.error('Failed to fetch user items', loadMoreErr)
+ }
+ }
+
+ // helper functions
+ // =
+
+ _replaceAll(res: GetList.Response) {
+ this.items = []
+ this._appendAll(res)
+ }
+
+ _appendAll(res: GetList.Response) {
+ this.loadMoreCursor = res.data.cursor
+ this.hasMore = !!this.loadMoreCursor
+ this.list = res.data.list
+ this.items = this.items.concat(
+ res.data.items.map(item => ({...item, _reactKey: item.subject})),
+ )
+ }
+}
diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts
index 794beae205..577b76e013 100644
--- a/src/state/models/content/post-thread.ts
+++ b/src/state/models/content/post-thread.ts
@@ -10,13 +10,17 @@ import {RootStoreModel} from '../root-store'
import * as apilib from 'lib/api/index'
import {cleanError} from 'lib/strings/errors'
import {updateDataOptimistically} from 'lib/async/revertible'
-
-function* reactKeyGenerator(): Generator {
- let counter = 0
- while (true) {
- yield `item-${counter++}`
- }
-}
+import {PostLabelInfo, PostModeration} from 'lib/labeling/types'
+import {
+ getEmbedLabels,
+ getEmbedMuted,
+ getEmbedMutedByList,
+ getEmbedBlocking,
+ getEmbedBlockedBy,
+ filterAccountLabels,
+ filterProfileLabels,
+ getPostModeration,
+} from 'lib/labeling/helpers'
export class PostThreadItemModel {
// ui state
@@ -30,7 +34,10 @@ export class PostThreadItemModel {
// data
post: AppBskyFeedDefs.PostView
postRecord?: FeedPost.Record
- parent?: PostThreadItemModel | AppBskyFeedDefs.NotFoundPost
+ parent?:
+ | PostThreadItemModel
+ | AppBskyFeedDefs.NotFoundPost
+ | AppBskyFeedDefs.BlockedPost
replies?: (PostThreadItemModel | AppBskyFeedDefs.NotFoundPost)[]
richText?: RichText
@@ -42,12 +49,51 @@ export class PostThreadItemModel {
return this.postRecord?.reply?.parent.uri
}
+ get rootUri(): string {
+ if (this.postRecord?.reply?.root.uri) {
+ return this.postRecord.reply.root.uri
+ }
+ return this.uri
+ }
+
+ get isThreadMuted() {
+ return this.rootStore.mutedThreads.uris.has(this.rootUri)
+ }
+
+ get labelInfo(): PostLabelInfo {
+ return {
+ postLabels: (this.post.labels || []).concat(
+ getEmbedLabels(this.post.embed),
+ ),
+ accountLabels: filterAccountLabels(this.post.author.labels),
+ profileLabels: filterProfileLabels(this.post.author.labels),
+ isMuted:
+ this.post.author.viewer?.muted ||
+ getEmbedMuted(this.post.embed) ||
+ false,
+ mutedByList:
+ this.post.author.viewer?.mutedByList ||
+ getEmbedMutedByList(this.post.embed),
+ isBlocking:
+ !!this.post.author.viewer?.blocking ||
+ getEmbedBlocking(this.post.embed) ||
+ false,
+ isBlockedBy:
+ !!this.post.author.viewer?.blockedBy ||
+ getEmbedBlockedBy(this.post.embed) ||
+ false,
+ }
+ }
+
+ get moderation(): PostModeration {
+ return getPostModeration(this.rootStore, this.labelInfo)
+ }
+
constructor(
public rootStore: RootStoreModel,
- reactKey: string,
v: AppBskyFeedDefs.ThreadViewPost,
) {
- this._reactKey = reactKey
+ this._reactKey = `thread-${v.post.uri}`
this.post = v.post
if (FeedPost.isRecord(this.post.record)) {
const valid = FeedPost.validateRecord(this.post.record)
@@ -71,28 +117,22 @@ export class PostThreadItemModel {
}
assignTreeModels(
- keyGen: Generator,
v: AppBskyFeedDefs.ThreadViewPost,
- higlightedPostUri: string,
+ highlightedPostUri: string,
includeParent = true,
includeChildren = true,
) {
// parents
if (includeParent && v.parent) {
if (AppBskyFeedDefs.isThreadViewPost(v.parent)) {
- const parentModel = new PostThreadItemModel(
- this.rootStore,
- keyGen.next().value,
- v.parent,
- )
+ const parentModel = new PostThreadItemModel(this.rootStore, v.parent)
parentModel._depth = this._depth - 1
parentModel._showChildReplyLine = true
if (v.parent.parent) {
- parentModel._showParentReplyLine = true //parentModel.uri !== higlightedPostUri
+ parentModel._showParentReplyLine = true
parentModel.assignTreeModels(
- keyGen,
v.parent,
- higlightedPostUri,
+ highlightedPostUri,
true,
false,
)
@@ -100,6 +140,8 @@ export class PostThreadItemModel {
this.parent = parentModel
} else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) {
this.parent = v.parent
+ } else if (AppBskyFeedDefs.isBlockedPost(v.parent)) {
+ this.parent = v.parent
}
}
// replies
@@ -107,23 +149,13 @@ export class PostThreadItemModel {
const replies = []
for (const item of v.replies) {
if (AppBskyFeedDefs.isThreadViewPost(item)) {
- const itemModel = new PostThreadItemModel(
- this.rootStore,
- keyGen.next().value,
- item,
- )
+ const itemModel = new PostThreadItemModel(this.rootStore, item)
itemModel._depth = this._depth + 1
itemModel._showParentReplyLine =
- itemModel.parentUri !== higlightedPostUri
+ itemModel.parentUri !== highlightedPostUri && replies.length === 0
if (item.replies?.length) {
itemModel._showChildReplyLine = true
- itemModel.assignTreeModels(
- keyGen,
- item,
- higlightedPostUri,
- false,
- true,
- )
+ itemModel.assignTreeModels(item, highlightedPostUri, false, true)
}
replies.push(itemModel)
} else if (AppBskyFeedDefs.isNotFoundPost(item)) {
@@ -188,6 +220,14 @@ export class PostThreadItemModel {
}
}
+ async toggleThreadMute() {
+ if (this.isThreadMuted) {
+ this.rootStore.mutedThreads.uris.delete(this.rootUri)
+ } else {
+ this.rootStore.mutedThreads.uris.add(this.rootUri)
+ }
+ }
+
async delete() {
await this.rootStore.agent.deletePost(this.post.uri)
this.rootStore.emitPostDeleted(this.post.uri)
@@ -206,6 +246,7 @@ export class PostThreadModel {
// data
thread?: PostThreadItemModel
+ isBlocked = false
constructor(
public rootStore: RootStoreModel,
@@ -222,6 +263,19 @@ export class PostThreadModel {
this.params = params
}
+ static fromPostView(
+ rootStore: RootStoreModel,
+ postView: AppBskyFeedDefs.PostView,
+ ) {
+ const model = new PostThreadModel(rootStore, {uri: postView.uri})
+ model.resolvedUri = postView.uri
+ model.hasLoaded = true
+ model.thread = new PostThreadItemModel(rootStore, {
+ post: postView,
+ })
+ return model
+ }
+
get hasContent() {
return typeof this.thread !== 'undefined'
}
@@ -230,6 +284,19 @@ export class PostThreadModel {
return this.error !== ''
}
+ get rootUri(): string {
+ if (this.thread) {
+ if (this.thread.postRecord?.reply?.root.uri) {
+ return this.thread.postRecord.reply.root.uri
+ }
+ }
+ return this.resolvedUri
+ }
+
+ get isThreadMuted() {
+ return this.rootStore.mutedThreads.uris.has(this.rootUri)
+ }
+
// public api
// =
@@ -279,6 +346,14 @@ export class PostThreadModel {
this.refresh()
}
+ async toggleThreadMute() {
+ if (this.isThreadMuted) {
+ this.rootStore.mutedThreads.uris.delete(this.rootUri)
+ } else {
+ this.rootStore.mutedThreads.uris.add(this.rootUri)
+ }
+ }
+
// state transitions
// =
@@ -320,6 +395,9 @@ export class PostThreadModel {
}
async _load(isRefreshing = false) {
+ if (this.hasLoaded && !isRefreshing) {
+ return
+ }
this._xLoading(isRefreshing)
try {
const res = await this.rootStore.agent.getPostThread(
@@ -328,21 +406,24 @@ export class PostThreadModel {
this._replaceAll(res)
this._xIdle()
} catch (e: any) {
+ console.log(e)
this._xIdle(e)
}
}
_replaceAll(res: GetPostThread.Response) {
+ this.isBlocked = AppBskyFeedDefs.isBlockedPost(res.data.thread)
+ if (this.isBlocked) {
+ return
+ }
+ pruneReplies(res.data.thread)
sortThread(res.data.thread)
- const keyGen = reactKeyGenerator()
const thread = new PostThreadItemModel(
this.rootStore,
- keyGen.next().value,
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
)
thread._isHighlightedPost = true
thread.assignTreeModels(
- keyGen,
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
thread.uri,
)
@@ -353,7 +434,20 @@ export class PostThreadModel {
type MaybePost =
| AppBskyFeedDefs.ThreadViewPost
| AppBskyFeedDefs.NotFoundPost
+ | AppBskyFeedDefs.BlockedPost
| {[k: string]: unknown; $type: string}
+function pruneReplies(post: MaybePost) {
+ if (post.replies) {
+ post.replies = (post.replies as MaybePost[]).filter((reply: MaybePost) => {
+ if (reply.blocked) {
+ return false
+ }
+ pruneReplies(reply)
+ return true
+ })
+ }
+}
+
function sortThread(post: MaybePost) {
if (post.notFound) {
return
diff --git a/src/state/models/content/post.ts b/src/state/models/content/post.ts
deleted file mode 100644
index b5d95bf01c..0000000000
--- a/src/state/models/content/post.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {AppBskyFeedPost as Post} from '@atproto/api'
-import {AtUri} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {cleanError} from 'lib/strings/errors'
-
-type RemoveIndex = {
- [P in keyof T as string extends P
- ? never
- : number extends P
- ? never
- : P]: T[P]
-}
-export class PostModel implements RemoveIndex {
- // state
- isLoading = false
- hasLoaded = false
- error = ''
- uri: string = ''
-
- // data
- text: string = ''
- entities?: Post.Entity[]
- reply?: Post.ReplyRef
- createdAt: string = ''
-
- constructor(public rootStore: RootStoreModel, uri: string) {
- makeAutoObservable(
- this,
- {
- rootStore: false,
- uri: false,
- },
- {autoBind: true},
- )
- this.uri = uri
- }
-
- get hasContent() {
- return this.createdAt !== ''
- }
-
- get hasError() {
- return this.error !== ''
- }
-
- get isEmpty() {
- return this.hasLoaded && !this.hasContent
- }
-
- // public api
- // =
-
- async setup() {
- await this._load()
- }
-
- // state transitions
- // =
-
- _xLoading() {
- this.isLoading = true
- this.error = ''
- }
-
- _xIdle(err?: any) {
- this.isLoading = false
- this.hasLoaded = true
- this.error = cleanError(err)
- if (err) {
- this.rootStore.log.error('Failed to fetch post', err)
- }
- }
-
- // loader functions
- // =
-
- async _load() {
- this._xLoading()
- try {
- const urip = new AtUri(this.uri)
- const res = await this.rootStore.agent.getPost({
- repo: urip.host,
- rkey: urip.rkey,
- })
- // TODO
- // if (!res.valid) {
- // throw new Error(res.error)
- // }
- this._replaceAll(res.value)
- this._xIdle()
- } catch (e: any) {
- this._xIdle(e)
- }
- }
-
- _replaceAll(res: Post.Record) {
- this.text = res.text
- this.entities = res.entities
- this.reply = res.reply
- this.createdAt = res.createdAt
- }
-}
diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts
index c26dc8749f..9d8378f795 100644
--- a/src/state/models/content/profile.ts
+++ b/src/state/models/content/profile.ts
@@ -1,6 +1,8 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {
+ AtUri,
ComAtprotoLabelDefs,
+ AppBskyGraphDefs,
AppBskyActorGetProfile as GetProfile,
AppBskyActorProfile,
RichText,
@@ -10,13 +12,20 @@ import * as apilib from 'lib/api/index'
import {cleanError} from 'lib/strings/errors'
import {FollowState} from '../cache/my-follows'
import {Image as RNImage} from 'react-native-image-crop-picker'
-
-export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
+import {ProfileLabelInfo, ProfileModeration} from 'lib/labeling/types'
+import {
+ getProfileModeration,
+ filterAccountLabels,
+ filterProfileLabels,
+} from 'lib/labeling/helpers'
export class ProfileViewerModel {
muted?: boolean
+ mutedByList?: AppBskyGraphDefs.ListViewBasic
following?: string
followedBy?: string
+ blockedBy?: boolean
+ blocking?: string
constructor() {
makeAutoObservable(this)
@@ -75,6 +84,20 @@ export class ProfileModel {
return this.hasLoaded && !this.hasContent
}
+ get labelInfo(): ProfileLabelInfo {
+ return {
+ accountLabels: filterAccountLabels(this.labels),
+ profileLabels: filterProfileLabels(this.labels),
+ isMuted: this.viewer?.muted || false,
+ isBlocking: !!this.viewer?.blocking || false,
+ isBlockedBy: !!this.viewer?.blockedBy || false,
+ }
+ }
+
+ get moderation(): ProfileModeration {
+ return getProfileModeration(this.rootStore, this.labelInfo)
+ }
+
// public api
// =
@@ -167,6 +190,33 @@ export class ProfileModel {
await this.refresh()
}
+ async blockAccount() {
+ const res = await this.rootStore.agent.app.bsky.graph.block.create(
+ {
+ repo: this.rootStore.me.did,
+ },
+ {
+ subject: this.did,
+ createdAt: new Date().toISOString(),
+ },
+ )
+ this.viewer.blocking = res.uri
+ await this.refresh()
+ }
+
+ async unblockAccount() {
+ if (!this.viewer.blocking) {
+ return
+ }
+ const {rkey} = new AtUri(this.viewer.blocking)
+ await this.rootStore.agent.app.bsky.graph.block.delete({
+ repo: this.rootStore.me.did,
+ rkey,
+ })
+ this.viewer.blocking = undefined
+ await this.refresh()
+ }
+
// state transitions
// =
diff --git a/src/state/models/discovery/feeds.ts b/src/state/models/discovery/feeds.ts
new file mode 100644
index 0000000000..26a8d650c6
--- /dev/null
+++ b/src/state/models/discovery/feeds.ts
@@ -0,0 +1,97 @@
+import {makeAutoObservable} from 'mobx'
+import {AppBskyUnspeccedGetPopularFeedGenerators} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {bundleAsync} from 'lib/async/bundle'
+import {cleanError} from 'lib/strings/errors'
+import {CustomFeedModel} from '../feeds/custom-feed'
+
+export class FeedsDiscoveryModel {
+ // state
+ isLoading = false
+ isRefreshing = false
+ hasLoaded = false
+ error = ''
+
+ // data
+ feeds: CustomFeedModel[] = []
+
+ constructor(public rootStore: RootStoreModel) {
+ makeAutoObservable(
+ this,
+ {
+ rootStore: false,
+ },
+ {autoBind: true},
+ )
+ }
+
+ get hasMore() {
+ return false
+ }
+
+ get hasContent() {
+ return this.feeds.length > 0
+ }
+
+ get hasError() {
+ return this.error !== ''
+ }
+
+ get isEmpty() {
+ return this.hasLoaded && !this.hasContent
+ }
+
+ // public api
+ // =
+
+ refresh = bundleAsync(async () => {
+ this._xLoading()
+ try {
+ const res =
+ await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators(
+ {},
+ )
+ this._replaceAll(res)
+ this._xIdle()
+ } catch (e: any) {
+ this._xIdle(e)
+ }
+ })
+
+ clear() {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = false
+ this.error = ''
+ this.feeds = []
+ }
+
+ // state transitions
+ // =
+
+ _xLoading() {
+ this.isLoading = true
+ this.isRefreshing = true
+ this.error = ''
+ }
+
+ _xIdle(err?: any) {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = true
+ this.error = cleanError(err)
+ if (err) {
+ this.rootStore.log.error('Failed to fetch popular feeds', err)
+ }
+ }
+
+ // helper functions
+ // =
+
+ _replaceAll(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) {
+ this.feeds = []
+ for (const f of res.data.feeds) {
+ this.feeds.push(new CustomFeedModel(this.rootStore, f))
+ }
+ }
+}
diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts
index f6e3157b78..4bbd32807e 100644
--- a/src/state/models/discovery/foafs.ts
+++ b/src/state/models/discovery/foafs.ts
@@ -1,4 +1,7 @@
-import {AppBskyActorDefs} from '@atproto/api'
+import {
+ AppBskyActorDefs,
+ AppBskyGraphGetFollows as GetFollows,
+} from '@atproto/api'
import {makeAutoObservable, runInAction} from 'mobx'
import sampleSize from 'lodash.samplesize'
import {bundleAsync} from 'lib/async/bundle'
@@ -43,11 +46,12 @@ export class FoafsModel {
{
let cursor
for (let i = 0; i < 10; i++) {
- const res = await this.rootStore.agent.getFollows({
- actor: this.rootStore.me.did,
- cursor,
- limit: 100,
- })
+ const res: GetFollows.Response =
+ await this.rootStore.agent.getFollows({
+ actor: this.rootStore.me.did,
+ cursor,
+ limit: 100,
+ })
this.rootStore.me.follows.hydrateProfiles(res.data.follows)
if (!res.data.cursor) {
break
diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts
index dca81dc90f..50faae6148 100644
--- a/src/state/models/discovery/suggested-actors.ts
+++ b/src/state/models/discovery/suggested-actors.ts
@@ -1,10 +1,8 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {AppBskyActorDefs} from '@atproto/api'
-import shuffle from 'lodash.shuffle'
import {RootStoreModel} from '../root-store'
import {cleanError} from 'lib/strings/errors'
import {bundleAsync} from 'lib/async/bundle'
-import {SUGGESTED_FOLLOWS} from 'lib/constants'
const PAGE_SIZE = 30
@@ -18,11 +16,9 @@ export class SuggestedActorsModel {
isLoading = false
isRefreshing = false
hasLoaded = false
+ loadMoreCursor: string | undefined = undefined
error = ''
- hasMore = true
- loadMoreCursor?: string
-
- hardCodedSuggestions: SuggestedActor[] | undefined
+ hasMore = false
// data
suggestions: SuggestedActor[] = []
@@ -60,45 +56,39 @@ export class SuggestedActorsModel {
}
loadMore = bundleAsync(async (replace: boolean = false) => {
- if (!replace && !this.hasMore) {
- return
- }
if (replace) {
- this.hardCodedSuggestions = undefined
+ this.hasMore = true
+ this.loadMoreCursor = undefined
+ }
+ if (!this.hasMore) {
+ return
}
this._xLoading(replace)
try {
- let items: SuggestedActor[] = this.suggestions
- if (replace) {
- items = []
- this.loadMoreCursor = undefined
- }
- let res
- do {
- await this.fetchHardcodedSuggestions()
- if (this.hardCodedSuggestions && this.hardCodedSuggestions.length > 0) {
- // pull from the hard-coded suggestions
- const newItems = this.hardCodedSuggestions.splice(0, this.pageSize)
- items = items.concat(newItems)
- this.hasMore = true
- this.loadMoreCursor = undefined
- } else {
- // pull from the PDS' algo
- res = await this.rootStore.agent.app.bsky.actor.getSuggestions({
- limit: this.pageSize,
- cursor: this.loadMoreCursor,
- })
- this.loadMoreCursor = res.data.cursor
- this.hasMore = !!this.loadMoreCursor
- items = items.concat(
- res.data.actors.filter(
- actor => !items.find(i => i.did === actor.did),
- ),
- )
- }
- } while (items.length < this.pageSize && this.hasMore)
+ const res = await this.rootStore.agent.app.bsky.actor.getSuggestions({
+ limit: 25,
+ cursor: this.loadMoreCursor,
+ })
+ const {actors, cursor} = res.data
+ this.rootStore.me.follows.hydrateProfiles(actors)
+
runInAction(() => {
- this.suggestions = items
+ if (replace) {
+ this.suggestions = []
+ }
+ this.loadMoreCursor = cursor
+ this.hasMore = !!cursor
+ this.suggestions = this.suggestions.concat(
+ actors.filter(actor => {
+ if (actor.viewer?.following) {
+ return false
+ }
+ if (actor.did === this.rootStore.me.did) {
+ return false
+ }
+ return true
+ }),
+ )
})
this._xIdle()
} catch (e: any) {
@@ -106,52 +96,6 @@ export class SuggestedActorsModel {
}
})
- async fetchHardcodedSuggestions() {
- if (this.hardCodedSuggestions) {
- return
- }
- try {
- // clone the array so we can mutate it
- const actors = [
- ...SUGGESTED_FOLLOWS(
- this.rootStore.session.currentSession?.service || '',
- ),
- ]
-
- // fetch the profiles in chunks of 25 (the limit allowed by `getProfiles`)
- let profiles: AppBskyActorDefs.ProfileView[] = []
- do {
- const res = await this.rootStore.agent.getProfiles({
- actors: actors.splice(0, 25),
- })
- profiles = profiles.concat(res.data.profiles)
- } while (actors.length)
-
- this.rootStore.me.follows.hydrateProfiles(profiles)
-
- runInAction(() => {
- profiles = profiles.filter(profile => {
- if (profile.viewer?.following) {
- return false
- }
- if (profile.did === this.rootStore.me.did) {
- return false
- }
- return true
- })
- this.hardCodedSuggestions = shuffle(profiles)
- })
- } catch (e) {
- this.rootStore.log.error(
- 'Failed to getProfiles() for suggested follows',
- {e},
- )
- runInAction(() => {
- this.hardCodedSuggestions = []
- })
- }
- }
-
// state transitions
// =
diff --git a/src/state/models/discovery/suggested-posts.ts b/src/state/models/discovery/suggested-posts.ts
deleted file mode 100644
index 6c8de3023c..0000000000
--- a/src/state/models/discovery/suggested-posts.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {RootStoreModel} from '../root-store'
-import {PostsFeedItemModel} from '../feeds/posts'
-import {cleanError} from 'lib/strings/errors'
-import {TEAM_HANDLES} from 'lib/constants'
-import {
- getMultipleAuthorsPosts,
- mergePosts,
-} from 'lib/api/build-suggested-posts'
-
-export class SuggestedPostsModel {
- // state
- isLoading = false
- hasLoaded = false
- error = ''
-
- // data
- posts: PostsFeedItemModel[] = []
-
- constructor(public rootStore: RootStoreModel) {
- makeAutoObservable(
- this,
- {
- rootStore: false,
- },
- {autoBind: true},
- )
- }
-
- get hasContent() {
- return this.posts.length > 0
- }
-
- get hasError() {
- return this.error !== ''
- }
-
- get isEmpty() {
- return this.hasLoaded && !this.hasContent
- }
-
- // public api
- // =
-
- async setup() {
- this._xLoading()
- try {
- const responses = await getMultipleAuthorsPosts(
- this.rootStore,
- TEAM_HANDLES(String(this.rootStore.agent.service)),
- undefined,
- 30,
- )
- runInAction(() => {
- const finalPosts = mergePosts(responses, {repostsOnly: true})
- // hydrate into models
- this.posts = finalPosts.map((post, i) => {
- // strip the reasons to hide that these are reposts
- delete post.reason
- return new PostsFeedItemModel(this.rootStore, `post-${i}`, post)
- })
- })
- this._xIdle()
- } catch (e: any) {
- this.rootStore.log.error('SuggestedPostsView: Failed to load posts', {
- e,
- })
- this._xIdle() // dont bubble to the user
- }
- }
-
- // state transitions
- // =
-
- _xLoading() {
- this.isLoading = true
- this.error = ''
- }
-
- _xIdle(err?: any) {
- this.isLoading = false
- this.hasLoaded = true
- this.error = cleanError(err)
- if (err) {
- this.rootStore.log.error('Failed to fetch suggested posts', err)
- }
- }
-}
diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts
new file mode 100644
index 0000000000..8fc1eb1ec9
--- /dev/null
+++ b/src/state/models/feeds/custom-feed.ts
@@ -0,0 +1,120 @@
+import {AppBskyFeedDefs} from '@atproto/api'
+import {makeAutoObservable, runInAction} from 'mobx'
+import {RootStoreModel} from 'state/models/root-store'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {updateDataOptimistically} from 'lib/async/revertible'
+
+export class CustomFeedModel {
+ // data
+ _reactKey: string
+ data: AppBskyFeedDefs.GeneratorView
+ isOnline: boolean
+ isValid: boolean
+
+ constructor(
+ public rootStore: RootStoreModel,
+ view: AppBskyFeedDefs.GeneratorView,
+ isOnline?: boolean,
+ isValid?: boolean,
+ ) {
+ this._reactKey = view.uri
+ this.data = view
+ this.isOnline = isOnline ?? true
+ this.isValid = isValid ?? true
+ makeAutoObservable(
+ this,
+ {
+ rootStore: false,
+ },
+ {autoBind: true},
+ )
+ }
+
+ // local actions
+ // =
+
+ get uri() {
+ return this.data.uri
+ }
+
+ get displayName() {
+ if (this.data.displayName) {
+ return sanitizeDisplayName(this.data.displayName)
+ }
+ return `Feed by @${this.data.creator.handle}`
+ }
+
+ get isSaved() {
+ return this.rootStore.preferences.savedFeeds.includes(this.uri)
+ }
+
+ get isLiked() {
+ return this.data.viewer?.like
+ }
+
+ // public apis
+ // =
+
+ async save() {
+ await this.rootStore.preferences.addSavedFeed(this.uri)
+ }
+
+ async unsave() {
+ await this.rootStore.preferences.removeSavedFeed(this.uri)
+ }
+
+ async like() {
+ try {
+ await updateDataOptimistically(
+ this.data,
+ () => {
+ this.data.viewer = this.data.viewer || {}
+ this.data.viewer.like = 'pending'
+ this.data.likeCount = (this.data.likeCount || 0) + 1
+ },
+ () => this.rootStore.agent.like(this.data.uri, this.data.cid),
+ res => {
+ this.data.viewer = this.data.viewer || {}
+ this.data.viewer.like = res.uri
+ },
+ )
+ } catch (e: any) {
+ this.rootStore.log.error('Failed to like feed', e)
+ }
+ }
+
+ async unlike() {
+ if (!this.data.viewer?.like) {
+ return
+ }
+ try {
+ const likeUri = this.data.viewer.like
+ await updateDataOptimistically(
+ this.data,
+ () => {
+ this.data.viewer = this.data.viewer || {}
+ this.data.viewer.like = undefined
+ this.data.likeCount = (this.data.likeCount || 1) - 1
+ },
+ () => this.rootStore.agent.deleteLike(likeUri),
+ )
+ } catch (e: any) {
+ this.rootStore.log.error('Failed to unlike feed', e)
+ }
+ }
+
+ async reload() {
+ const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerator({
+ feed: this.data.uri,
+ })
+ runInAction(() => {
+ this.data = res.data.view
+ this.isOnline = res.data.isOnline
+ this.isValid = res.data.isValid
+ })
+ }
+
+ serialize() {
+ return JSON.stringify(this.data)
+ }
+}
diff --git a/src/state/models/feeds/multi-feed.ts b/src/state/models/feeds/multi-feed.ts
new file mode 100644
index 0000000000..c2ca8d72f4
--- /dev/null
+++ b/src/state/models/feeds/multi-feed.ts
@@ -0,0 +1,225 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {AtUri} from '@atproto/api'
+import {bundleAsync} from 'lib/async/bundle'
+import {RootStoreModel} from '../root-store'
+import {CustomFeedModel} from './custom-feed'
+import {PostsFeedModel} from './posts'
+import {PostsFeedSliceModel} from './post'
+
+const FEED_PAGE_SIZE = 10
+const FEEDS_PAGE_SIZE = 3
+
+export type MultiFeedItem =
+ | {
+ _reactKey: string
+ type: 'header'
+ }
+ | {
+ _reactKey: string
+ type: 'feed-header'
+ avatar: string | undefined
+ title: string
+ }
+ | {
+ _reactKey: string
+ type: 'feed-slice'
+ slice: PostsFeedSliceModel
+ }
+ | {
+ _reactKey: string
+ type: 'feed-loading'
+ }
+ | {
+ _reactKey: string
+ type: 'feed-error'
+ error: string
+ }
+ | {
+ _reactKey: string
+ type: 'feed-footer'
+ title: string
+ uri: string
+ }
+ | {
+ _reactKey: string
+ type: 'footer'
+ }
+
+export class PostsMultiFeedModel {
+ // state
+ isLoading = false
+ isRefreshing = false
+ hasLoaded = false
+ hasMore = true
+
+ // data
+ feedInfos: CustomFeedModel[] = []
+ feeds: PostsFeedModel[] = []
+
+ constructor(public rootStore: RootStoreModel) {
+ makeAutoObservable(this, {rootStore: false}, {autoBind: true})
+ }
+
+ get hasContent() {
+ return this.feeds.length !== 0
+ }
+
+ get isEmpty() {
+ return this.hasLoaded && !this.hasContent
+ }
+
+ get items() {
+ const items: MultiFeedItem[] = [{_reactKey: '__header__', type: 'header'}]
+ for (let i = 0; i < this.feedInfos.length; i++) {
+ if (!this.feeds[i]) {
+ break
+ }
+ const feed = this.feeds[i]
+ const feedInfo = this.feedInfos[i]
+ const urip = new AtUri(feedInfo.uri)
+ items.push({
+ _reactKey: `__feed_header_${i}__`,
+ type: 'feed-header',
+ avatar: feedInfo.data.avatar,
+ title: feedInfo.displayName,
+ })
+ if (feed.isLoading) {
+ items.push({
+ _reactKey: `__feed_loading_${i}__`,
+ type: 'feed-loading',
+ })
+ } else if (feed.hasError) {
+ items.push({
+ _reactKey: `__feed_error_${i}__`,
+ type: 'feed-error',
+ error: feed.error,
+ })
+ } else {
+ for (let j = 0; j < feed.slices.length; j++) {
+ items.push({
+ _reactKey: `__feed_slice_${i}_${j}__`,
+ type: 'feed-slice',
+ slice: feed.slices[j],
+ })
+ }
+ }
+ items.push({
+ _reactKey: `__feed_footer_${i}__`,
+ type: 'feed-footer',
+ title: feedInfo.displayName,
+ uri: `/profile/${feedInfo.data.creator.did}/feed/${urip.rkey}`,
+ })
+ }
+ if (!this.hasMore) {
+ items.push({_reactKey: '__footer__', type: 'footer'})
+ }
+ return items
+ }
+
+ // public api
+ // =
+
+ /**
+ * Nuke all data
+ */
+ clear() {
+ this.rootStore.log.debug('MultiFeedModel:clear')
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = false
+ this.hasMore = true
+ this.feeds = []
+ }
+
+ /**
+ * Register any event listeners. Returns a cleanup function.
+ */
+ registerListeners() {
+ const sub = this.rootStore.onPostDeleted(this.onPostDeleted.bind(this))
+ return () => sub.remove()
+ }
+
+ /**
+ * Reset and load
+ */
+ async refresh() {
+ this.feedInfos = this.rootStore.me.savedFeeds.all.slice() // capture current feeds
+ await this.loadMore(true)
+ }
+
+ /**
+ * Load latest in the active feeds
+ */
+ loadLatest() {
+ for (const feed of this.feeds) {
+ /* dont await */ feed.refresh()
+ }
+ }
+
+ /**
+ * Load more posts to the end of the feed
+ */
+ loadMore = bundleAsync(async (isRefreshing: boolean = false) => {
+ if (!isRefreshing && !this.hasMore) {
+ return
+ }
+ if (isRefreshing) {
+ this.isRefreshing = true // set optimistically for UI
+ this.feeds = []
+ }
+ this._xLoading(isRefreshing)
+ const start = this.feeds.length
+ const newFeeds: PostsFeedModel[] = []
+ for (
+ let i = start;
+ i < start + FEEDS_PAGE_SIZE && i < this.feedInfos.length;
+ i++
+ ) {
+ const feed = new PostsFeedModel(this.rootStore, 'custom', {
+ feed: this.feedInfos[i].uri,
+ })
+ feed.pageSize = FEED_PAGE_SIZE
+ await feed.setup()
+ newFeeds.push(feed)
+ }
+ runInAction(() => {
+ this.feeds = this.feeds.concat(newFeeds)
+ this.hasMore = this.feeds.length < this.feedInfos.length
+ })
+ this._xIdle()
+ })
+
+ /**
+ * Attempt to load more again after a failure
+ */
+ async retryLoadMore() {
+ this.hasMore = true
+ return this.loadMore()
+ }
+
+ /**
+ * Removes posts from the feed upon deletion.
+ */
+ onPostDeleted(uri: string) {
+ for (const f of this.feeds) {
+ f.onPostDeleted(uri)
+ }
+ }
+
+ // state transitions
+ // =
+
+ _xLoading(isRefreshing = false) {
+ this.isLoading = true
+ this.isRefreshing = isRefreshing
+ }
+
+ _xIdle() {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = true
+ }
+
+ // helper functions
+ // =
+}
diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts
index ff77ab9796..5005f1d913 100644
--- a/src/state/models/feeds/notifications.ts
+++ b/src/state/models/feeds/notifications.ts
@@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
import {
AppBskyNotificationListNotifications as ListNotifications,
AppBskyActorDefs,
+ AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedLike,
@@ -9,10 +10,21 @@ import {
ComAtprotoLabelDefs,
} from '@atproto/api'
import AwaitLock from 'await-lock'
+import chunk from 'lodash.chunk'
import {bundleAsync} from 'lib/async/bundle'
import {RootStoreModel} from '../root-store'
import {PostThreadModel} from '../content/post-thread'
import {cleanError} from 'lib/strings/errors'
+import {
+ PostLabelInfo,
+ PostModeration,
+ ModerationBehaviorCode,
+} from 'lib/labeling/types'
+import {
+ getPostModeration,
+ filterAccountLabels,
+ filterProfileLabels,
+} from 'lib/labeling/helpers'
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
const PAGE_SIZE = 30
@@ -88,6 +100,29 @@ export class NotificationsFeedItemModel {
}
}
+ get labelInfo(): PostLabelInfo {
+ const addedInfo = this.additionalPost?.thread?.labelInfo
+ return {
+ postLabels: (this.labels || []).concat(addedInfo?.postLabels || []),
+ accountLabels: filterAccountLabels(this.author.labels).concat(
+ addedInfo?.accountLabels || [],
+ ),
+ profileLabels: filterProfileLabels(this.author.labels).concat(
+ addedInfo?.profileLabels || [],
+ ),
+ isMuted: this.author.viewer?.muted || addedInfo?.isMuted || false,
+ mutedByList: this.author.viewer?.mutedByList || addedInfo?.mutedByList,
+ isBlocking:
+ !!this.author.viewer?.blocking || addedInfo?.isBlocking || false,
+ isBlockedBy:
+ !!this.author.viewer?.blockedBy || addedInfo?.isBlockedBy || false,
+ }
+ }
+
+ get moderation(): PostModeration {
+ return getPostModeration(this.rootStore, this.labelInfo)
+ }
+
get numUnreadInGroup(): number {
if (this.additional?.length) {
return (
@@ -146,6 +181,14 @@ export class NotificationsFeedItemModel {
return false
}
+ get additionalDataUri(): string | undefined {
+ if (this.isReply || this.isQuote || this.isMention) {
+ return this.uri
+ } else if (this.isLike || this.isRepost) {
+ return this.subjectUri
+ }
+ }
+
get subjectUri(): string {
if (this.reasonSubject) {
return this.reasonSubject
@@ -160,6 +203,13 @@ export class NotificationsFeedItemModel {
return ''
}
+ get reasonSubjectRootUri(): string | undefined {
+ if (this.additionalPost) {
+ return this.additionalPost.rootUri
+ }
+ return undefined
+ }
+
toSupportedRecord(v: unknown): SupportedRecord | undefined {
for (const ns of [
AppBskyFeedPost,
@@ -186,28 +236,11 @@ export class NotificationsFeedItemModel {
)
}
- async fetchAdditionalData() {
- if (!this.needsAdditionalData) {
- return
- }
- let postUri
- if (this.isReply || this.isQuote || this.isMention) {
- postUri = this.uri
- } else if (this.isLike || this.isRepost) {
- postUri = this.subjectUri
- }
- if (postUri) {
- this.additionalPost = new PostThreadModel(this.rootStore, {
- uri: postUri,
- depth: 0,
- })
- await this.additionalPost.setup().catch(e => {
- this.rootStore.log.error(
- 'Failed to load post needed by notification',
- e,
- )
- })
- }
+ setAdditionalData(additionalPost: AppBskyFeedDefs.PostView) {
+ this.additionalPost = PostThreadModel.fromPostView(
+ this.rootStore,
+ additionalPost,
+ )
}
}
@@ -227,7 +260,7 @@ export class NotificationsFeedModel {
// data
notifications: NotificationsFeedItemModel[] = []
- queuedNotifications: undefined | ListNotifications.Notification[] = undefined
+ queuedNotifications: undefined | NotificationsFeedItemModel[] = undefined
unreadCount = 0
// this is used to help trigger push notifications
@@ -257,7 +290,9 @@ export class NotificationsFeedModel {
}
get hasNewLatest() {
- return this.queuedNotifications && this.queuedNotifications?.length > 0
+ return Boolean(
+ this.queuedNotifications && this.queuedNotifications?.length > 0,
+ )
}
get unreadCountLabel(): string {
@@ -354,7 +389,13 @@ export class NotificationsFeedModel {
queue.push(notif)
}
- this._setQueued(this._filterNotifications(queue))
+ // NOTE
+ // because filtering depends on the added information we have to fetch
+ // the full models here. this is *not* ideal performance and we need
+ // to update the notifications route to give all the info we need
+ // -prf
+ const queueModels = await this._fetchItemModels(queue)
+ this._setQueued(this._filterNotifications(queueModels))
this._countUnread()
} catch (e) {
this.rootStore.log.error('NotificationsModel:syncQueue failed', {e})
@@ -451,8 +492,15 @@ export class NotificationsFeedModel {
'mostRecent',
res.data.notifications[0],
)
- await notif.fetchAdditionalData()
- return notif
+ const addedUri = notif.additionalDataUri
+ if (addedUri) {
+ const postsRes = await this.rootStore.agent.app.bsky.feed.getPosts({
+ uris: [addedUri],
+ })
+ notif.setAdditionalData(postsRes.data.posts[0])
+ }
+ const filtered = this._filterNotifications([notif])
+ return filtered[0]
}
// state transitions
@@ -505,43 +553,78 @@ export class NotificationsFeedModel {
}
_filterNotifications(
- items: ListNotifications.Notification[],
- ): ListNotifications.Notification[] {
- return items.filter(item => {
- return (
- this.rootStore.preferences.getLabelPreference(item.labels).pref !==
- 'hide'
- )
- })
+ items: NotificationsFeedItemModel[],
+ ): NotificationsFeedItemModel[] {
+ return items
+ .filter(item => {
+ const hideByLabel =
+ item.moderation.list.behavior === ModerationBehaviorCode.Hide
+ let mutedThread = !!(
+ item.reasonSubjectRootUri &&
+ this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri)
+ )
+ return !hideByLabel && !mutedThread
+ })
+ .map(item => {
+ if (item.additional?.length) {
+ item.additional = this._filterNotifications(item.additional)
+ }
+ return item
+ })
}
- async _processNotifications(
+ async _fetchItemModels(
items: ListNotifications.Notification[],
): Promise {
- const promises = []
+ // construct item models and track who needs more data
const itemModels: NotificationsFeedItemModel[] = []
- items = this._filterNotifications(items)
- for (const item of groupNotifications(items)) {
+ const addedPostMap = new Map()
+ for (const item of items) {
const itemModel = new NotificationsFeedItemModel(
this.rootStore,
`item-${_idCounter++}`,
item,
)
- if (itemModel.needsAdditionalData) {
- promises.push(itemModel.fetchAdditionalData())
+ const uri = itemModel.additionalDataUri
+ if (uri) {
+ const models = addedPostMap.get(uri) || []
+ models.push(itemModel)
+ addedPostMap.set(uri, models)
}
itemModels.push(itemModel)
}
- await Promise.all(promises).catch(e => {
- this.rootStore.log.error(
- 'Uncaught failure during notifications _processNotifications()',
- e,
+
+ // fetch additional data
+ if (addedPostMap.size > 0) {
+ const uriChunks = chunk(Array.from(addedPostMap.keys()), 25)
+ const postsChunks = await Promise.all(
+ uriChunks.map(uris =>
+ this.rootStore.agent.app.bsky.feed
+ .getPosts({uris})
+ .then(res => res.data.posts),
+ ),
)
- })
+ for (const post of postsChunks.flat()) {
+ const models = addedPostMap.get(post.uri)
+ if (models?.length) {
+ for (const model of models) {
+ model.setAdditionalData(post)
+ }
+ }
+ }
+ }
+
return itemModels
}
- _setQueued(queued: undefined | ListNotifications.Notification[]) {
+ async _processNotifications(
+ items: ListNotifications.Notification[],
+ ): Promise {
+ const itemModels = await this._fetchItemModels(groupNotifications(items))
+ return this._filterNotifications(itemModels)
+ }
+
+ _setQueued(queued: undefined | NotificationsFeedItemModel[]) {
this.queuedNotifications = queued
}
diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts
new file mode 100644
index 0000000000..18a90ee82f
--- /dev/null
+++ b/src/state/models/feeds/post.ts
@@ -0,0 +1,265 @@
+import {makeAutoObservable} from 'mobx'
+import {AppBskyFeedDefs, AppBskyFeedPost, RichText} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {updateDataOptimistically} from 'lib/async/revertible'
+import {PostLabelInfo, PostModeration} from 'lib/labeling/types'
+import {FeedViewPostsSlice} from 'lib/api/feed-manip'
+import {
+ getEmbedLabels,
+ getEmbedMuted,
+ getEmbedMutedByList,
+ getEmbedBlocking,
+ getEmbedBlockedBy,
+ getPostModeration,
+ filterAccountLabels,
+ filterProfileLabels,
+ mergePostModerations,
+} from 'lib/labeling/helpers'
+
+type FeedViewPost = AppBskyFeedDefs.FeedViewPost
+type ReasonRepost = AppBskyFeedDefs.ReasonRepost
+type PostView = AppBskyFeedDefs.PostView
+
+let _idCounter = 0
+
+export class PostsFeedItemModel {
+ // ui state
+ _reactKey: string = ''
+
+ // data
+ post: PostView
+ postRecord?: AppBskyFeedPost.Record
+ reply?: FeedViewPost['reply']
+ reason?: FeedViewPost['reason']
+ richText?: RichText
+
+ constructor(
+ public rootStore: RootStoreModel,
+ reactKey: string,
+ v: FeedViewPost,
+ ) {
+ this._reactKey = reactKey
+ this.post = v.post
+ if (AppBskyFeedPost.isRecord(this.post.record)) {
+ const valid = AppBskyFeedPost.validateRecord(this.post.record)
+ if (valid.success) {
+ this.postRecord = this.post.record
+ this.richText = new RichText(this.postRecord, {cleanNewlines: true})
+ } else {
+ this.postRecord = undefined
+ this.richText = undefined
+ rootStore.log.warn(
+ 'Received an invalid app.bsky.feed.post record',
+ valid.error,
+ )
+ }
+ } else {
+ this.postRecord = undefined
+ this.richText = undefined
+ rootStore.log.warn(
+ 'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type',
+ this.post.record,
+ )
+ }
+ this.reply = v.reply
+ this.reason = v.reason
+ makeAutoObservable(this, {rootStore: false})
+ }
+
+ get rootUri(): string {
+ if (typeof this.reply?.root.uri === 'string') {
+ return this.reply.root.uri
+ }
+ return this.post.uri
+ }
+
+ get isThreadMuted() {
+ return this.rootStore.mutedThreads.uris.has(this.rootUri)
+ }
+
+ get labelInfo(): PostLabelInfo {
+ return {
+ postLabels: (this.post.labels || []).concat(
+ getEmbedLabels(this.post.embed),
+ ),
+ accountLabels: filterAccountLabels(this.post.author.labels),
+ profileLabels: filterProfileLabels(this.post.author.labels),
+ isMuted:
+ this.post.author.viewer?.muted ||
+ getEmbedMuted(this.post.embed) ||
+ false,
+ mutedByList:
+ this.post.author.viewer?.mutedByList ||
+ getEmbedMutedByList(this.post.embed),
+ isBlocking:
+ !!this.post.author.viewer?.blocking ||
+ getEmbedBlocking(this.post.embed) ||
+ false,
+ isBlockedBy:
+ !!this.post.author.viewer?.blockedBy ||
+ getEmbedBlockedBy(this.post.embed) ||
+ false,
+ }
+ }
+
+ get moderation(): PostModeration {
+ return getPostModeration(this.rootStore, this.labelInfo)
+ }
+
+ copy(v: FeedViewPost) {
+ this.post = v.post
+ this.reply = v.reply
+ this.reason = v.reason
+ }
+
+ copyMetrics(v: FeedViewPost) {
+ this.post.replyCount = v.post.replyCount
+ this.post.repostCount = v.post.repostCount
+ this.post.likeCount = v.post.likeCount
+ this.post.viewer = v.post.viewer
+ }
+
+ get reasonRepost(): ReasonRepost | undefined {
+ if (this.reason?.$type === 'app.bsky.feed.defs#reasonRepost') {
+ return this.reason as ReasonRepost
+ }
+ }
+
+ async toggleLike() {
+ this.post.viewer = this.post.viewer || {}
+ if (this.post.viewer.like) {
+ const url = this.post.viewer.like
+ await updateDataOptimistically(
+ this.post,
+ () => {
+ this.post.likeCount = (this.post.likeCount || 0) - 1
+ this.post.viewer!.like = undefined
+ },
+ () => this.rootStore.agent.deleteLike(url),
+ )
+ } else {
+ await updateDataOptimistically(
+ this.post,
+ () => {
+ this.post.likeCount = (this.post.likeCount || 0) + 1
+ this.post.viewer!.like = 'pending'
+ },
+ () => this.rootStore.agent.like(this.post.uri, this.post.cid),
+ res => {
+ this.post.viewer!.like = res.uri
+ },
+ )
+ }
+ }
+
+ async toggleRepost() {
+ this.post.viewer = this.post.viewer || {}
+ if (this.post.viewer?.repost) {
+ const url = this.post.viewer.repost
+ await updateDataOptimistically(
+ this.post,
+ () => {
+ this.post.repostCount = (this.post.repostCount || 0) - 1
+ this.post.viewer!.repost = undefined
+ },
+ () => this.rootStore.agent.deleteRepost(url),
+ )
+ } else {
+ await updateDataOptimistically(
+ this.post,
+ () => {
+ this.post.repostCount = (this.post.repostCount || 0) + 1
+ this.post.viewer!.repost = 'pending'
+ },
+ () => this.rootStore.agent.repost(this.post.uri, this.post.cid),
+ res => {
+ this.post.viewer!.repost = res.uri
+ },
+ )
+ }
+ }
+
+ async toggleThreadMute() {
+ if (this.isThreadMuted) {
+ this.rootStore.mutedThreads.uris.delete(this.rootUri)
+ } else {
+ this.rootStore.mutedThreads.uris.add(this.rootUri)
+ }
+ }
+
+ async delete() {
+ await this.rootStore.agent.deletePost(this.post.uri)
+ this.rootStore.emitPostDeleted(this.post.uri)
+ }
+}
+
+export class PostsFeedSliceModel {
+ // ui state
+ _reactKey: string = ''
+
+ // data
+ items: PostsFeedItemModel[] = []
+
+ constructor(
+ public rootStore: RootStoreModel,
+ reactKey: string,
+ slice: FeedViewPostsSlice,
+ ) {
+ this._reactKey = reactKey
+ for (const item of slice.items) {
+ this.items.push(
+ new PostsFeedItemModel(rootStore, `slice-${_idCounter++}`, item),
+ )
+ }
+ makeAutoObservable(this, {rootStore: false})
+ }
+
+ get uri() {
+ if (this.isReply) {
+ return this.items[1].post.uri
+ }
+ return this.items[0].post.uri
+ }
+
+ get isThread() {
+ return (
+ this.items.length > 1 &&
+ this.items.every(
+ item => item.post.author.did === this.items[0].post.author.did,
+ )
+ )
+ }
+
+ get isReply() {
+ return this.items.length > 1 && !this.isThread
+ }
+
+ get rootItem() {
+ if (this.isReply) {
+ return this.items[1]
+ }
+ return this.items[0]
+ }
+
+ get moderation() {
+ return mergePostModerations(this.items.map(item => item.moderation))
+ }
+
+ containsUri(uri: string) {
+ return !!this.items.find(item => item.post.uri === uri)
+ }
+
+ isThreadParentAt(i: number) {
+ if (this.items.length === 1) {
+ return false
+ }
+ return i < this.items.length - 1
+ }
+
+ isThreadChildAt(i: number) {
+ if (this.items.length === 1) {
+ return false
+ }
+ return i > 0
+ }
+}
diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts
index 38faf658a3..b7d4def137 100644
--- a/src/state/models/feeds/posts.ts
+++ b/src/state/models/feeds/posts.ts
@@ -1,229 +1,27 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {
AppBskyFeedGetTimeline as GetTimeline,
- AppBskyFeedDefs,
- AppBskyFeedPost,
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
- RichText,
- jsonToLex,
+ AppBskyFeedGetFeed as GetCustomFeed,
} from '@atproto/api'
import AwaitLock from 'await-lock'
import {bundleAsync} from 'lib/async/bundle'
-import sampleSize from 'lodash.samplesize'
import {RootStoreModel} from '../root-store'
import {cleanError} from 'lib/strings/errors'
-import {SUGGESTED_FOLLOWS} from 'lib/constants'
-import {
- getCombinedCursors,
- getMultipleAuthorsPosts,
- mergePosts,
-} from 'lib/api/build-suggested-posts'
import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip'
-import {updateDataOptimistically} from 'lib/async/revertible'
-
-type FeedViewPost = AppBskyFeedDefs.FeedViewPost
-type ReasonRepost = AppBskyFeedDefs.ReasonRepost
-type PostView = AppBskyFeedDefs.PostView
+import {PostsFeedSliceModel} from './post'
const PAGE_SIZE = 30
let _idCounter = 0
-export class PostsFeedItemModel {
- // ui state
- _reactKey: string = ''
-
- // data
- post: PostView
- postRecord?: AppBskyFeedPost.Record
- reply?: FeedViewPost['reply']
- reason?: FeedViewPost['reason']
- richText?: RichText
-
- constructor(
- public rootStore: RootStoreModel,
- reactKey: string,
- v: FeedViewPost,
- ) {
- this._reactKey = reactKey
- this.post = v.post
- if (AppBskyFeedPost.isRecord(this.post.record)) {
- const valid = AppBskyFeedPost.validateRecord(this.post.record)
- if (valid.success) {
- this.postRecord = this.post.record
- this.richText = new RichText(this.postRecord, {cleanNewlines: true})
- } else {
- this.postRecord = undefined
- this.richText = undefined
- rootStore.log.warn(
- 'Received an invalid app.bsky.feed.post record',
- valid.error,
- )
- }
- } else {
- this.postRecord = undefined
- this.richText = undefined
- rootStore.log.warn(
- 'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type',
- this.post.record,
- )
- }
- this.reply = v.reply
- this.reason = v.reason
- makeAutoObservable(this, {rootStore: false})
- }
-
- copy(v: FeedViewPost) {
- this.post = v.post
- this.reply = v.reply
- this.reason = v.reason
- }
-
- copyMetrics(v: FeedViewPost) {
- this.post.replyCount = v.post.replyCount
- this.post.repostCount = v.post.repostCount
- this.post.likeCount = v.post.likeCount
- this.post.viewer = v.post.viewer
- }
-
- get reasonRepost(): ReasonRepost | undefined {
- if (this.reason?.$type === 'app.bsky.feed.defs#reasonRepost') {
- return this.reason as ReasonRepost
- }
- }
-
- async toggleLike() {
- this.post.viewer = this.post.viewer || {}
- if (this.post.viewer.like) {
- const url = this.post.viewer.like
- await updateDataOptimistically(
- this.post,
- () => {
- this.post.likeCount = (this.post.likeCount || 0) - 1
- this.post.viewer!.like = undefined
- },
- () => this.rootStore.agent.deleteLike(url),
- )
- } else {
- await updateDataOptimistically(
- this.post,
- () => {
- this.post.likeCount = (this.post.likeCount || 0) + 1
- this.post.viewer!.like = 'pending'
- },
- () => this.rootStore.agent.like(this.post.uri, this.post.cid),
- res => {
- this.post.viewer!.like = res.uri
- },
- )
- }
- }
-
- async toggleRepost() {
- this.post.viewer = this.post.viewer || {}
- if (this.post.viewer?.repost) {
- const url = this.post.viewer.repost
- await updateDataOptimistically(
- this.post,
- () => {
- this.post.repostCount = (this.post.repostCount || 0) - 1
- this.post.viewer!.repost = undefined
- },
- () => this.rootStore.agent.deleteRepost(url),
- )
- } else {
- await updateDataOptimistically(
- this.post,
- () => {
- this.post.repostCount = (this.post.repostCount || 0) + 1
- this.post.viewer!.repost = 'pending'
- },
- () => this.rootStore.agent.repost(this.post.uri, this.post.cid),
- res => {
- this.post.viewer!.repost = res.uri
- },
- )
- }
- }
-
- async delete() {
- await this.rootStore.agent.deletePost(this.post.uri)
- this.rootStore.emitPostDeleted(this.post.uri)
- }
-}
-
-export class PostsFeedSliceModel {
- // ui state
- _reactKey: string = ''
-
- // data
- items: PostsFeedItemModel[] = []
-
- constructor(
- public rootStore: RootStoreModel,
- reactKey: string,
- slice: FeedViewPostsSlice,
- ) {
- this._reactKey = reactKey
- for (const item of slice.items) {
- this.items.push(
- new PostsFeedItemModel(rootStore, `item-${_idCounter++}`, item),
- )
- }
- makeAutoObservable(this, {rootStore: false})
- }
-
- get uri() {
- if (this.isReply) {
- return this.items[1].post.uri
- }
- return this.items[0].post.uri
- }
-
- get isThread() {
- return (
- this.items.length > 1 &&
- this.items.every(
- item => item.post.author.did === this.items[0].post.author.did,
- )
- )
- }
-
- get isReply() {
- return this.items.length > 1 && !this.isThread
- }
-
- get rootItem() {
- if (this.isReply) {
- return this.items[1]
- }
- return this.items[0]
- }
-
- containsUri(uri: string) {
- return !!this.items.find(item => item.post.uri === uri)
- }
-
- isThreadParentAt(i: number) {
- if (this.items.length === 1) {
- return false
- }
- return i < this.items.length - 1
- }
-
- isThreadChildAt(i: number) {
- if (this.items.length === 1) {
- return false
- }
- return i > 0
- }
-}
-
export class PostsFeedModel {
// state
isLoading = false
isRefreshing = false
hasNewLatest = false
hasLoaded = false
+ isBlocking = false
+ isBlockedBy = false
error = ''
loadMoreError = ''
params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams
@@ -231,17 +29,24 @@ export class PostsFeedModel {
loadMoreCursor: string | undefined
pollCursor: string | undefined
tuner = new FeedTuner()
+ pageSize = PAGE_SIZE
// used to linearize async modifications to state
lock = new AwaitLock()
+ // used to track if what's hot is coming up empty
+ emptyFetches = 0
+
// data
slices: PostsFeedSliceModel[] = []
constructor(
public rootStore: RootStoreModel,
- public feedType: 'home' | 'author' | 'suggested' | 'goodstuff',
- params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams,
+ public feedType: 'home' | 'author' | 'custom',
+ params:
+ | GetTimeline.QueryParams
+ | GetAuthorFeed.QueryParams
+ | GetCustomFeed.QueryParams,
) {
makeAutoObservable(
this,
@@ -275,12 +80,10 @@ export class PostsFeedModel {
const isRepost =
item?.reasonRepost?.by?.handle === params.actor ||
item?.reasonRepost?.by?.did === params.actor
- return (
- !item.reply || // not a reply
- isRepost || // but allow if it's a repost
- (slice.isThread && // or a thread by the user
- item.reply?.root.author.did === item.post.author.did)
- )
+ const allow =
+ !item.postRecord?.reply || // not a reply
+ isRepost // but allow if it's a repost
+ return allow
})
} else {
return this.slices
@@ -311,19 +114,10 @@ export class PostsFeedModel {
this.tuner.reset()
}
- switchFeedType(feedType: 'home' | 'suggested') {
- if (this.feedType === feedType) {
- return
- }
- this.feedType = feedType
- return this.setup()
- }
-
get feedTuners() {
- if (this.feedType === 'goodstuff') {
+ if (this.feedType === 'custom') {
return [
FeedTuner.dedupReposts,
- FeedTuner.likedRepliesOnly,
FeedTuner.preferredLangOnly(
this.rootStore.preferences.contentLanguages,
),
@@ -349,7 +143,7 @@ export class PostsFeedModel {
this.tuner.reset()
this._xLoading(isRefreshing)
try {
- const res = await this._getFeed({limit: PAGE_SIZE})
+ const res = await this._getFeed({limit: this.pageSize})
await this._replaceAll(res)
this._xIdle()
} catch (e: any) {
@@ -388,7 +182,7 @@ export class PostsFeedModel {
try {
const res = await this._getFeed({
cursor: this.loadMoreCursor,
- limit: PAGE_SIZE,
+ limit: this.pageSize,
})
await this._appendAll(res)
this._xIdle()
@@ -453,36 +247,38 @@ export class PostsFeedModel {
/**
* Check if new posts are available
*/
- async checkForLatest({autoPrepend}: {autoPrepend?: boolean} = {}) {
- if (this.hasNewLatest || this.feedType === 'suggested') {
+ async checkForLatest() {
+ if (this.hasNewLatest) {
return
}
- const res = await this._getFeed({limit: PAGE_SIZE})
+ const res = await this._getFeed({limit: this.pageSize})
const tuner = new FeedTuner()
const slices = tuner.tune(res.data.feed, this.feedTuners)
- if (slices[0]?.uri !== this.slices[0]?.uri) {
- if (!autoPrepend) {
- this.setHasNewLatest(true)
- } else {
- this.setHasNewLatest(false)
- runInAction(() => {
- const slicesModels = slices.map(
- slice =>
- new PostsFeedSliceModel(
- this.rootStore,
- `item-${_idCounter++}`,
- slice,
- ),
- )
- this.slices = slicesModels.concat(
- this.slices.filter(slice1 =>
- slicesModels.find(slice2 => slice1.uri === slice2.uri),
- ),
- )
- })
- }
- } else {
- this.setHasNewLatest(false)
+ this.setHasNewLatest(slices[0]?.uri !== this.slices[0]?.uri)
+ }
+
+ /**
+ * Fetches the given post and adds it to the top
+ * Used by the composer to add their new posts
+ */
+ async addPostToTop(uri: string) {
+ if (!this.slices.length) {
+ return this.refresh()
+ }
+ try {
+ const res = await this.rootStore.agent.app.bsky.feed.getPosts({
+ uris: [uri],
+ })
+ const toPrepend = new PostsFeedSliceModel(
+ this.rootStore,
+ uri,
+ new FeedViewPostsSlice(res.data.posts.map(post => ({post}))),
+ )
+ runInAction(() => {
+ this.slices = [toPrepend].concat(this.slices)
+ })
+ } catch (e) {
+ this.rootStore.log.error('Failed to load post to prepend', {e})
}
}
@@ -512,6 +308,8 @@ export class PostsFeedModel {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
+ this.isBlocking = error instanceof GetAuthorFeed.BlockedActorError
+ this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError
this.error = cleanError(error)
this.loadMoreError = cleanError(loadMoreError)
if (error) {
@@ -528,17 +326,22 @@ export class PostsFeedModel {
// helper functions
// =
- async _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
+ async _replaceAll(
+ res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response,
+ ) {
this.pollCursor = res.data.feed[0]?.post.uri
return this._appendAll(res, true)
}
async _appendAll(
- res: GetTimeline.Response | GetAuthorFeed.Response,
+ res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response,
replace = false,
) {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
+ if (replace) {
+ this.emptyFetches = 0
+ }
this.rootStore.me.follows.hydrateProfiles(
res.data.feed.map(item => item.post.author),
@@ -561,10 +364,18 @@ export class PostsFeedModel {
} else {
this.slices = this.slices.concat(toAppend)
}
+ if (toAppend.length === 0) {
+ this.emptyFetches++
+ if (this.emptyFetches >= 10) {
+ this.hasMore = false
+ }
+ }
})
}
- _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
+ _updateAll(
+ res: GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response,
+ ) {
for (const item of res.data.feed) {
const existingSlice = this.slices.find(slice =>
slice.containsUri(item.post.uri),
@@ -581,37 +392,27 @@ export class PostsFeedModel {
}
protected async _getFeed(
- params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams = {},
- ): Promise {
+ params:
+ | GetTimeline.QueryParams
+ | GetAuthorFeed.QueryParams
+ | GetCustomFeed.QueryParams,
+ ): Promise<
+ GetTimeline.Response | GetAuthorFeed.Response | GetCustomFeed.Response
+ > {
params = Object.assign({}, this.params, params)
- if (this.feedType === 'suggested') {
- const responses = await getMultipleAuthorsPosts(
- this.rootStore,
- sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20),
- params.cursor,
- 20,
- )
- const combinedCursor = getCombinedCursors(responses)
- const finalData = mergePosts(responses, {bestOfOnly: true})
- const lastHeaders = responses[responses.length - 1].headers
- return {
- success: true,
- data: {
- feed: finalData,
- cursor: combinedCursor,
- },
- headers: lastHeaders,
- }
- } else if (this.feedType === 'home') {
+ if (this.feedType === 'home') {
return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams)
- } else if (this.feedType === 'goodstuff') {
- const res = await getGoodStuff(
- this.rootStore.session.currentSession?.accessJwt || '',
- params as GetTimeline.QueryParams,
- )
- res.data.feed = (res.data.feed || []).filter(
- item => !item.post.author.viewer?.muted,
+ } else if (this.feedType === 'custom') {
+ const res = await this.rootStore.agent.app.bsky.feed.getFeed(
+ params as GetCustomFeed.QueryParams,
)
+ // NOTE
+ // some custom feeds fail to enforce the pagination limit
+ // so we manually truncate here
+ // -prf
+ if (params.limit && res.data.feed.length > params.limit) {
+ res.data.feed = res.data.feed.slice(0, params.limit)
+ }
return res
} else {
return this.rootStore.agent.getAuthorFeed(
@@ -620,45 +421,3 @@ export class PostsFeedModel {
}
}
}
-
-// HACK
-// temporary off-spec route to get the good stuff
-// -prf
-async function getGoodStuff(
- accessJwt: string,
- params: GetTimeline.QueryParams,
-): Promise {
- const controller = new AbortController()
- const to = setTimeout(() => controller.abort(), 15e3)
-
- const uri = new URL('https://bsky.social/xrpc/app.bsky.unspecced.getPopular')
- let k: keyof GetTimeline.QueryParams
- for (k in params) {
- if (typeof params[k] !== 'undefined') {
- uri.searchParams.set(k, String(params[k]))
- }
- }
-
- const res = await fetch(String(uri), {
- method: 'get',
- headers: {
- accept: 'application/json',
- authorization: `Bearer ${accessJwt}`,
- },
- signal: controller.signal,
- })
-
- const resHeaders: Record = {}
- res.headers.forEach((value: string, key: string) => {
- resHeaders[key] = value
- })
- let resBody = await res.json()
-
- clearTimeout(to)
-
- return {
- success: res.status === 200,
- headers: resHeaders,
- data: jsonToLex(resBody),
- }
-}
diff --git a/src/state/models/invited-users.ts b/src/state/models/invited-users.ts
index 121161a320..a28e0309a6 100644
--- a/src/state/models/invited-users.ts
+++ b/src/state/models/invited-users.ts
@@ -4,6 +4,7 @@ import {RootStoreModel} from './root-store'
import {isObj, hasProp, isStrArray} from 'lib/type-guards'
export class InvitedUsers {
+ copiedInvites: string[] = []
seenDids: string[] = []
profiles: AppBskyActorDefs.ProfileViewDetailed[] = []
@@ -20,13 +21,20 @@ export class InvitedUsers {
}
serialize() {
- return {seenDids: this.seenDids}
+ return {seenDids: this.seenDids, copiedInvites: this.copiedInvites}
}
hydrate(v: unknown) {
if (isObj(v) && hasProp(v, 'seenDids') && isStrArray(v.seenDids)) {
this.seenDids = v.seenDids
}
+ if (
+ isObj(v) &&
+ hasProp(v, 'copiedInvites') &&
+ isStrArray(v.copiedInvites)
+ ) {
+ this.copiedInvites = v.copiedInvites
+ }
}
async fetch(invites: ComAtprotoServerDefs.InviteCode[]) {
@@ -63,6 +71,16 @@ export class InvitedUsers {
}
}
+ isInviteCopied(invite: string) {
+ return this.copiedInvites.includes(invite)
+ }
+
+ setInviteCopied(invite: string) {
+ if (!this.isInviteCopied(invite)) {
+ this.copiedInvites.push(invite)
+ }
+ }
+
markSeen(did: string) {
this.seenDids.push(did)
this.profiles = this.profiles.filter(profile => profile.did !== did)
diff --git a/src/state/models/lists/actor-feeds.ts b/src/state/models/lists/actor-feeds.ts
new file mode 100644
index 0000000000..0f20605814
--- /dev/null
+++ b/src/state/models/lists/actor-feeds.ts
@@ -0,0 +1,120 @@
+import {makeAutoObservable} from 'mobx'
+import {AppBskyFeedGetActorFeeds as GetActorFeeds} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {bundleAsync} from 'lib/async/bundle'
+import {cleanError} from 'lib/strings/errors'
+import {CustomFeedModel} from '../feeds/custom-feed'
+
+const PAGE_SIZE = 30
+
+export class ActorFeedsModel {
+ // state
+ isLoading = false
+ isRefreshing = false
+ hasLoaded = false
+ error = ''
+ hasMore = true
+ loadMoreCursor?: string
+
+ // data
+ feeds: CustomFeedModel[] = []
+
+ constructor(
+ public rootStore: RootStoreModel,
+ public params: GetActorFeeds.QueryParams,
+ ) {
+ makeAutoObservable(
+ this,
+ {
+ rootStore: false,
+ },
+ {autoBind: true},
+ )
+ }
+
+ get hasContent() {
+ return this.feeds.length > 0
+ }
+
+ get hasError() {
+ return this.error !== ''
+ }
+
+ get isEmpty() {
+ return this.hasLoaded && !this.hasContent
+ }
+
+ // public api
+ // =
+
+ async refresh() {
+ return this.loadMore(true)
+ }
+
+ clear() {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = false
+ this.error = ''
+ this.hasMore = true
+ this.loadMoreCursor = undefined
+ this.feeds = []
+ }
+
+ loadMore = bundleAsync(async (replace: boolean = false) => {
+ if (!replace && !this.hasMore) {
+ return
+ }
+ this._xLoading(replace)
+ try {
+ const res = await this.rootStore.agent.app.bsky.feed.getActorFeeds({
+ actor: this.params.actor,
+ limit: PAGE_SIZE,
+ cursor: replace ? undefined : this.loadMoreCursor,
+ })
+ if (replace) {
+ this._replaceAll(res)
+ } else {
+ this._appendAll(res)
+ }
+ this._xIdle()
+ } catch (e: any) {
+ this._xIdle(e)
+ }
+ })
+
+ // state transitions
+ // =
+
+ _xLoading(isRefreshing = false) {
+ this.isLoading = true
+ this.isRefreshing = isRefreshing
+ this.error = ''
+ }
+
+ _xIdle(err?: any) {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = true
+ this.error = cleanError(err)
+ if (err) {
+ this.rootStore.log.error('Failed to fetch user followers', err)
+ }
+ }
+
+ // helper functions
+ // =
+
+ _replaceAll(res: GetActorFeeds.Response) {
+ this.feeds = []
+ this._appendAll(res)
+ }
+
+ _appendAll(res: GetActorFeeds.Response) {
+ this.loadMoreCursor = res.data.cursor
+ this.hasMore = !!this.loadMoreCursor
+ for (const f of res.data.feeds) {
+ this.feeds.push(new CustomFeedModel(this.rootStore, f))
+ }
+ }
+}
diff --git a/src/state/models/lists/blocked-accounts.ts b/src/state/models/lists/blocked-accounts.ts
new file mode 100644
index 0000000000..20eef8affa
--- /dev/null
+++ b/src/state/models/lists/blocked-accounts.ts
@@ -0,0 +1,106 @@
+import {makeAutoObservable} from 'mobx'
+import {
+ AppBskyGraphGetBlocks as GetBlocks,
+ AppBskyActorDefs as ActorDefs,
+} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {cleanError} from 'lib/strings/errors'
+import {bundleAsync} from 'lib/async/bundle'
+
+const PAGE_SIZE = 30
+
+export class BlockedAccountsModel {
+ // state
+ isLoading = false
+ isRefreshing = false
+ hasLoaded = false
+ error = ''
+ hasMore = true
+ loadMoreCursor?: string
+
+ // data
+ blocks: ActorDefs.ProfileView[] = []
+
+ constructor(public rootStore: RootStoreModel) {
+ makeAutoObservable(
+ this,
+ {
+ rootStore: false,
+ },
+ {autoBind: true},
+ )
+ }
+
+ get hasContent() {
+ return this.blocks.length > 0
+ }
+
+ get hasError() {
+ return this.error !== ''
+ }
+
+ get isEmpty() {
+ return this.hasLoaded && !this.hasContent
+ }
+
+ // public api
+ // =
+
+ async refresh() {
+ return this.loadMore(true)
+ }
+
+ loadMore = bundleAsync(async (replace: boolean = false) => {
+ if (!replace && !this.hasMore) {
+ return
+ }
+ this._xLoading(replace)
+ try {
+ const res = await this.rootStore.agent.app.bsky.graph.getBlocks({
+ limit: PAGE_SIZE,
+ cursor: replace ? undefined : this.loadMoreCursor,
+ })
+ if (replace) {
+ this._replaceAll(res)
+ } else {
+ this._appendAll(res)
+ }
+ this._xIdle()
+ } catch (e: any) {
+ this._xIdle(e)
+ }
+ })
+
+ // state transitions
+ // =
+
+ _xLoading(isRefreshing = false) {
+ this.isLoading = true
+ this.isRefreshing = isRefreshing
+ this.error = ''
+ }
+
+ _xIdle(err?: any) {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = true
+ this.error = cleanError(err)
+ if (err) {
+ this.rootStore.log.error('Failed to fetch user followers', err)
+ }
+ }
+
+ // helper functions
+ // =
+
+ _replaceAll(res: GetBlocks.Response) {
+ this.blocks = []
+ this._appendAll(res)
+ }
+
+ _appendAll(res: GetBlocks.Response) {
+ this.loadMoreCursor = res.data.cursor
+ this.hasMore = !!this.loadMoreCursor
+ this.blocks = this.blocks.concat(res.data.blocks)
+ }
+}
diff --git a/src/state/models/lists/lists-list.ts b/src/state/models/lists/lists-list.ts
new file mode 100644
index 0000000000..6618c3bf6c
--- /dev/null
+++ b/src/state/models/lists/lists-list.ts
@@ -0,0 +1,215 @@
+import {makeAutoObservable} from 'mobx'
+import {
+ AppBskyGraphGetLists as GetLists,
+ AppBskyGraphGetListMutes as GetListMutes,
+ AppBskyGraphDefs as GraphDefs,
+} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {cleanError} from 'lib/strings/errors'
+import {bundleAsync} from 'lib/async/bundle'
+
+const PAGE_SIZE = 30
+
+export class ListsListModel {
+ // state
+ isLoading = false
+ isRefreshing = false
+ hasLoaded = false
+ error = ''
+ loadMoreError = ''
+ hasMore = true
+ loadMoreCursor?: string
+
+ // data
+ lists: GraphDefs.ListView[] = []
+
+ constructor(
+ public rootStore: RootStoreModel,
+ public source: 'my-modlists' | string,
+ ) {
+ makeAutoObservable(
+ this,
+ {
+ rootStore: false,
+ },
+ {autoBind: true},
+ )
+ }
+
+ get hasContent() {
+ return this.lists.length > 0
+ }
+
+ get hasError() {
+ return this.error !== ''
+ }
+
+ get isEmpty() {
+ return this.hasLoaded && !this.hasContent
+ }
+
+ // public api
+ // =
+
+ async refresh() {
+ return this.loadMore(true)
+ }
+
+ loadMore = bundleAsync(async (replace: boolean = false) => {
+ if (!replace && !this.hasMore) {
+ return
+ }
+ this._xLoading(replace)
+ try {
+ let res: GetLists.Response
+ if (this.source === 'my-modlists') {
+ res = {
+ success: true,
+ headers: {},
+ data: {
+ subject: undefined,
+ lists: [],
+ },
+ }
+ const [res1, res2] = await Promise.all([
+ fetchAllUserLists(this.rootStore, this.rootStore.me.did),
+ fetchAllMyMuteLists(this.rootStore),
+ ])
+ for (let list of res1.data.lists) {
+ if (list.purpose === 'app.bsky.graph.defs#modlist') {
+ res.data.lists.push(list)
+ }
+ }
+ for (let list of res2.data.lists) {
+ if (
+ list.purpose === 'app.bsky.graph.defs#modlist' &&
+ !res.data.lists.find(l => l.uri === list.uri)
+ ) {
+ res.data.lists.push(list)
+ }
+ }
+ } else {
+ res = await this.rootStore.agent.app.bsky.graph.getLists({
+ actor: this.source,
+ limit: PAGE_SIZE,
+ cursor: replace ? undefined : this.loadMoreCursor,
+ })
+ }
+ if (replace) {
+ this._replaceAll(res)
+ } else {
+ this._appendAll(res)
+ }
+ this._xIdle()
+ } catch (e: any) {
+ this._xIdle(replace ? e : undefined, !replace ? e : undefined)
+ }
+ })
+
+ /**
+ * Attempt to load more again after a failure
+ */
+ async retryLoadMore() {
+ this.loadMoreError = ''
+ this.hasMore = true
+ return this.loadMore()
+ }
+
+ // state transitions
+ // =
+
+ _xLoading(isRefreshing = false) {
+ this.isLoading = true
+ this.isRefreshing = isRefreshing
+ this.error = ''
+ }
+
+ _xIdle(err?: any, loadMoreErr?: any) {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = true
+ this.error = cleanError(err)
+ this.loadMoreError = cleanError(loadMoreErr)
+ if (err) {
+ this.rootStore.log.error('Failed to fetch user lists', err)
+ }
+ if (loadMoreErr) {
+ this.rootStore.log.error('Failed to fetch user lists', loadMoreErr)
+ }
+ }
+
+ // helper functions
+ // =
+
+ _replaceAll(res: GetLists.Response | GetListMutes.Response) {
+ this.lists = []
+ this._appendAll(res)
+ }
+
+ _appendAll(res: GetLists.Response | GetListMutes.Response) {
+ this.loadMoreCursor = res.data.cursor
+ this.hasMore = !!this.loadMoreCursor
+ this.lists = this.lists.concat(
+ res.data.lists.map(list => ({...list, _reactKey: list.uri})),
+ )
+ }
+}
+
+async function fetchAllUserLists(
+ store: RootStoreModel,
+ did: string,
+): Promise {
+ let acc: GetLists.Response = {
+ success: true,
+ headers: {},
+ data: {
+ subject: undefined,
+ lists: [],
+ },
+ }
+
+ let cursor
+ for (let i = 0; i < 100; i++) {
+ const res: GetLists.Response = await store.agent.app.bsky.graph.getLists({
+ actor: did,
+ cursor,
+ limit: 50,
+ })
+ cursor = res.data.cursor
+ acc.data.lists = acc.data.lists.concat(res.data.lists)
+ if (!cursor) {
+ break
+ }
+ }
+
+ return acc
+}
+
+async function fetchAllMyMuteLists(
+ store: RootStoreModel,
+): Promise {
+ let acc: GetListMutes.Response = {
+ success: true,
+ headers: {},
+ data: {
+ subject: undefined,
+ lists: [],
+ },
+ }
+
+ let cursor
+ for (let i = 0; i < 100; i++) {
+ const res: GetListMutes.Response =
+ await store.agent.app.bsky.graph.getListMutes({
+ cursor,
+ limit: 50,
+ })
+ cursor = res.data.cursor
+ acc.data.lists = acc.data.lists.concat(res.data.lists)
+ if (!cursor) {
+ break
+ }
+ }
+
+ return acc
+}
diff --git a/src/state/models/lists/muted-accounts.ts b/src/state/models/lists/muted-accounts.ts
new file mode 100644
index 0000000000..9c3e1157b6
--- /dev/null
+++ b/src/state/models/lists/muted-accounts.ts
@@ -0,0 +1,106 @@
+import {makeAutoObservable} from 'mobx'
+import {
+ AppBskyGraphGetMutes as GetMutes,
+ AppBskyActorDefs as ActorDefs,
+} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {cleanError} from 'lib/strings/errors'
+import {bundleAsync} from 'lib/async/bundle'
+
+const PAGE_SIZE = 30
+
+export class MutedAccountsModel {
+ // state
+ isLoading = false
+ isRefreshing = false
+ hasLoaded = false
+ error = ''
+ hasMore = true
+ loadMoreCursor?: string
+
+ // data
+ mutes: ActorDefs.ProfileView[] = []
+
+ constructor(public rootStore: RootStoreModel) {
+ makeAutoObservable(
+ this,
+ {
+ rootStore: false,
+ },
+ {autoBind: true},
+ )
+ }
+
+ get hasContent() {
+ return this.mutes.length > 0
+ }
+
+ get hasError() {
+ return this.error !== ''
+ }
+
+ get isEmpty() {
+ return this.hasLoaded && !this.hasContent
+ }
+
+ // public api
+ // =
+
+ async refresh() {
+ return this.loadMore(true)
+ }
+
+ loadMore = bundleAsync(async (replace: boolean = false) => {
+ if (!replace && !this.hasMore) {
+ return
+ }
+ this._xLoading(replace)
+ try {
+ const res = await this.rootStore.agent.app.bsky.graph.getMutes({
+ limit: PAGE_SIZE,
+ cursor: replace ? undefined : this.loadMoreCursor,
+ })
+ if (replace) {
+ this._replaceAll(res)
+ } else {
+ this._appendAll(res)
+ }
+ this._xIdle()
+ } catch (e: any) {
+ this._xIdle(e)
+ }
+ })
+
+ // state transitions
+ // =
+
+ _xLoading(isRefreshing = false) {
+ this.isLoading = true
+ this.isRefreshing = isRefreshing
+ this.error = ''
+ }
+
+ _xIdle(err?: any) {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = true
+ this.error = cleanError(err)
+ if (err) {
+ this.rootStore.log.error('Failed to fetch user followers', err)
+ }
+ }
+
+ // helper functions
+ // =
+
+ _replaceAll(res: GetMutes.Response) {
+ this.mutes = []
+ this._appendAll(res)
+ }
+
+ _appendAll(res: GetMutes.Response) {
+ this.loadMoreCursor = res.data.cursor
+ this.hasMore = !!this.loadMoreCursor
+ this.mutes = this.mutes.concat(res.data.mutes)
+ }
+}
diff --git a/src/state/models/log.ts b/src/state/models/log.ts
index d80617139f..7c9c37c0d9 100644
--- a/src/state/models/log.ts
+++ b/src/state/models/log.ts
@@ -27,6 +27,7 @@ function genId(): string {
export class LogModel {
entries: LogEntry[] = []
+ timers = new Map()
constructor() {
makeAutoObservable(this)
@@ -74,6 +75,21 @@ export class LogModel {
ts: Date.now(),
})
}
+
+ time = (label = 'default') => {
+ this.timers.set(label, performance.now())
+ }
+
+ timeEnd = (label = 'default', warn = false) => {
+ const endTime = performance.now()
+ if (this.timers.has(label)) {
+ const elapsedTime = endTime - this.timers.get(label)!
+ console.log(`${label}: ${elapsedTime.toFixed(3)}ms`)
+ this.timers.delete(label)
+ } else {
+ warn && console.warn(`Timer with label '${label}' does not exist.`)
+ }
+ }
}
function detailsToStr(details?: any) {
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index e8b8e1ed05..59d79f0568 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -1,10 +1,14 @@
import {makeAutoObservable, runInAction} from 'mobx'
-import {ComAtprotoServerDefs} from '@atproto/api'
+import {
+ ComAtprotoServerDefs,
+ ComAtprotoServerListAppPasswords,
+} from '@atproto/api'
import {RootStoreModel} from './root-store'
import {PostsFeedModel} from './feeds/posts'
import {NotificationsFeedModel} from './feeds/notifications'
import {MyFollowsCache} from './cache/my-follows'
import {isObj, hasProp} from 'lib/type-guards'
+import {SavedFeedsModel} from './ui/saved-feeds'
const PROFILE_UPDATE_INTERVAL = 10 * 60 * 1e3 // 10min
const NOTIFS_UPDATE_INTERVAL = 30 * 1e3 // 30sec
@@ -18,9 +22,11 @@ export class MeModel {
followsCount: number | undefined
followersCount: number | undefined
mainFeed: PostsFeedModel
+ savedFeeds: SavedFeedsModel
notifications: NotificationsFeedModel
follows: MyFollowsCache
invites: ComAtprotoServerDefs.InviteCode[] = []
+ appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = []
lastProfileStateUpdate = Date.now()
lastNotifsUpdate = Date.now()
@@ -37,8 +43,9 @@ export class MeModel {
this.mainFeed = new PostsFeedModel(this.rootStore, 'home', {
algorithm: 'reverse-chronological',
})
- this.notifications = new NotificationsFeedModel(this.rootStore, {})
+ this.notifications = new NotificationsFeedModel(this.rootStore)
this.follows = new MyFollowsCache(this.rootStore)
+ this.savedFeeds = new SavedFeedsModel(this.rootStore)
}
clear() {
@@ -51,6 +58,7 @@ export class MeModel {
this.description = ''
this.avatar = ''
this.invites = []
+ this.appPasswords = []
}
serialize(): unknown {
@@ -99,16 +107,15 @@ export class MeModel {
this.handle = sess.currentSession?.handle || ''
await this.fetchProfile()
this.mainFeed.clear()
- await Promise.all([
- this.mainFeed.setup().catch(e => {
- this.rootStore.log.error('Failed to setup main feed model', e)
- }),
- this.notifications.setup().catch(e => {
- this.rootStore.log.error('Failed to setup notifications model', e)
- }),
- ])
+ /* dont await */ this.mainFeed.setup().catch(e => {
+ this.rootStore.log.error('Failed to setup main feed model', e)
+ })
+ /* dont await */ this.notifications.setup().catch(e => {
+ this.rootStore.log.error('Failed to setup notifications model', e)
+ })
this.rootStore.emitSessionLoaded()
await this.fetchInviteCodes()
+ await this.fetchAppPasswords()
} else {
this.clear()
}
@@ -120,6 +127,7 @@ export class MeModel {
this.lastProfileStateUpdate = Date.now()
await this.fetchProfile()
await this.fetchInviteCodes()
+ await this.fetchAppPasswords()
}
if (Date.now() - this.lastNotifsUpdate > NOTIFS_UPDATE_INTERVAL) {
this.lastNotifsUpdate = Date.now()
@@ -173,6 +181,56 @@ export class MeModel {
await this.rootStore.invitedUsers.fetch(this.invites)
}
}
+
+ async fetchAppPasswords() {
+ if (this.rootStore.session) {
+ try {
+ const res =
+ await this.rootStore.agent.com.atproto.server.listAppPasswords({})
+ runInAction(() => {
+ this.appPasswords = res.data.passwords
+ })
+ } catch (e) {
+ this.rootStore.log.error('Failed to fetch user app passwords', e)
+ }
+ }
+ }
+
+ async createAppPassword(name: string) {
+ if (this.rootStore.session) {
+ try {
+ if (this.appPasswords.find(p => p.name === name)) {
+ // TODO: this should be handled by the backend but it's not
+ throw new Error('App password with this name already exists')
+ }
+ const res =
+ await this.rootStore.agent.com.atproto.server.createAppPassword({
+ name,
+ })
+ runInAction(() => {
+ this.appPasswords.push(res.data)
+ })
+ return res.data
+ } catch (e) {
+ this.rootStore.log.error('Failed to create app password', e)
+ }
+ }
+ }
+
+ async deleteAppPassword(name: string) {
+ if (this.rootStore.session) {
+ try {
+ await this.rootStore.agent.com.atproto.server.revokeAppPassword({
+ name: name,
+ })
+ runInAction(() => {
+ this.appPasswords = this.appPasswords.filter(p => p.name !== name)
+ })
+ } catch (e) {
+ this.rootStore.log.error('Failed to delete app password', e)
+ }
+ }
+ }
}
function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean {
diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts
index fbe6c92a0a..e53e861e2d 100644
--- a/src/state/models/media/gallery.ts
+++ b/src/state/models/media/gallery.ts
@@ -4,7 +4,7 @@ import {ImageModel} from './image'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {openPicker} from 'lib/media/picker'
import {getImageDim} from 'lib/media/manip'
-import {getDataUriSize} from 'lib/media/util'
+import {isNative} from 'platform/detection'
export class GalleryModel {
images: ImageModel[] = []
@@ -23,13 +23,7 @@ export class GalleryModel {
return this.images.length
}
- get paths() {
- return this.images.map(image =>
- image.compressed === undefined ? image.path : image.compressed.path,
- )
- }
-
- async add(image_: RNImage) {
+ async add(image_: Omit) {
if (this.size >= 4) {
return
}
@@ -37,10 +31,21 @@ export class GalleryModel {
// Temporarily enforce uniqueness but can eventually also use index
if (!this.images.some(i => i.path === image_.path)) {
const image = new ImageModel(this.rootStore, image_)
- await image.compress()
- runInAction(() => {
- this.images.push(image)
+ // Initial resize
+ image.manipulate({})
+ this.images.push(image)
+ }
+ }
+
+ async edit(image: ImageModel) {
+ if (isNative) {
+ this.crop(image)
+ } else {
+ this.rootStore.shell.openModal({
+ name: 'edit-image',
+ image,
+ gallery: this,
})
}
}
@@ -52,11 +57,10 @@ export class GalleryModel {
const {width, height} = await getImageDim(uri)
- const image: RNImage = {
+ const image = {
path: uri,
height,
width,
- size: getDataUriSize(uri),
mime: 'image/jpeg',
}
@@ -65,6 +69,10 @@ export class GalleryModel {
})
}
+ setAltText(image: ImageModel, altText: string) {
+ image.setAltText(altText)
+ }
+
crop(image: ImageModel) {
image.crop()
}
@@ -74,12 +82,20 @@ export class GalleryModel {
this.images.splice(index, 1)
}
+ async previous(image: ImageModel) {
+ image.previous()
+ }
+
async pick() {
- const images = await openPicker(this.rootStore, {
- multiple: true,
- maxFiles: 4 - this.images.length,
+ const images = await openPicker({
+ selectionLimit: 4 - this.size,
+ allowsMultipleSelection: true,
})
- await Promise.all(images.map(image => this.add(image)))
+ return await Promise.all(
+ images.map(image => {
+ this.add(image)
+ }),
+ )
}
}
diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts
index 584bf90cc9..e524c49de0 100644
--- a/src/state/models/media/image.ts
+++ b/src/state/models/media/image.ts
@@ -1,25 +1,46 @@
import {Image as RNImage} from 'react-native-image-crop-picker'
import {RootStoreModel} from 'state/index'
-import {compressAndResizeImageForPost} from 'lib/media/manip'
import {makeAutoObservable, runInAction} from 'mobx'
-import {openCropper} from 'lib/media/picker'
import {POST_IMG_MAX} from 'lib/constants'
-import {scaleDownDimensions} from 'lib/media/util'
+import * as ImageManipulator from 'expo-image-manipulator'
+import {getDataUriSize} from 'lib/media/util'
+import {openCropper} from 'lib/media/picker'
+import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
+import {Position} from 'react-avatar-editor'
+import {Dimensions} from 'lib/media/types'
-// TODO: EXIF embed
-// Cases to consider: ExternalEmbed
-export class ImageModel implements RNImage {
+export interface ImageManipulationAttributes {
+ aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
+ rotate?: number
+ scale?: number
+ position?: Position
+ flipHorizontal?: boolean
+ flipVertical?: boolean
+}
+
+const MAX_IMAGE_SIZE_IN_BYTES = 976560
+
+export class ImageModel implements Omit {
path: string
mime = 'image/jpeg'
width: number
height: number
- size: number
+ altText = ''
cropped?: RNImage = undefined
compressed?: RNImage = undefined
- scaledWidth: number = POST_IMG_MAX.width
- scaledHeight: number = POST_IMG_MAX.height
- constructor(public rootStore: RootStoreModel, image: RNImage) {
+ // Web manipulation
+ prev?: RNImage
+ attributes: ImageManipulationAttributes = {
+ aspectRatio: '1:1',
+ scale: 1,
+ flipHorizontal: false,
+ flipVertical: false,
+ rotate: 0,
+ }
+ prevAttributes: ImageManipulationAttributes = {}
+
+ constructor(public rootStore: RootStoreModel, image: Omit) {
makeAutoObservable(this, {
rootStore: false,
})
@@ -27,28 +48,135 @@ export class ImageModel implements RNImage {
this.path = image.path
this.width = image.width
this.height = image.height
- this.size = image.size
- this.calcScaledDimensions()
}
- calcScaledDimensions() {
- const {width, height} = scaleDownDimensions(
- {width: this.width, height: this.height},
- POST_IMG_MAX,
- )
-
- this.scaledWidth = width
- this.scaledHeight = height
+ setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
+ this.attributes.aspectRatio = aspectRatio
}
+ setRotate(degrees: number) {
+ this.attributes.rotate = degrees
+ this.manipulate({})
+ }
+
+ flipVertical() {
+ this.attributes.flipVertical = !this.attributes.flipVertical
+ this.manipulate({})
+ }
+
+ flipHorizontal() {
+ this.attributes.flipHorizontal = !this.attributes.flipHorizontal
+ this.manipulate({})
+ }
+
+ get ratioMultipliers() {
+ return {
+ '4:3': 4 / 3,
+ '1:1': 1,
+ '3:4': 3 / 4,
+ None: this.width / this.height,
+ }
+ }
+
+ getUploadDimensions(
+ dimensions: Dimensions,
+ maxDimensions: Dimensions = POST_IMG_MAX,
+ as: ImageManipulationAttributes['aspectRatio'] = 'None',
+ ) {
+ const {width, height} = dimensions
+ const {width: maxWidth, height: maxHeight} = maxDimensions
+
+ return width < maxWidth && height < maxHeight
+ ? {
+ width,
+ height,
+ }
+ : this.getResizedDimensions(as, POST_IMG_MAX.width)
+ }
+
+ getResizedDimensions(
+ as: ImageManipulationAttributes['aspectRatio'] = 'None',
+ maxSide: number,
+ ) {
+ const ratioMultiplier = this.ratioMultipliers[as]
+
+ if (ratioMultiplier === 1) {
+ return {
+ height: maxSide,
+ width: maxSide,
+ }
+ }
+
+ if (ratioMultiplier < 1) {
+ return {
+ width: maxSide * ratioMultiplier,
+ height: maxSide,
+ }
+ }
+
+ return {
+ width: maxSide,
+ height: maxSide / ratioMultiplier,
+ }
+ }
+
+ async setAltText(altText: string) {
+ this.altText = altText
+ }
+
+ // Only compress prior to upload
+ async compress() {
+ for (let i = 10; i > 0; i--) {
+ // Float precision
+ const factor = Math.round(i) / 10
+ const compressed = await ImageManipulator.manipulateAsync(
+ this.cropped?.path ?? this.path,
+ undefined,
+ {
+ compress: factor,
+ base64: true,
+ format: SaveFormat.JPEG,
+ },
+ )
+
+ if (compressed.base64 !== undefined) {
+ const size = getDataUriSize(compressed.base64)
+
+ if (size < MAX_IMAGE_SIZE_IN_BYTES) {
+ runInAction(() => {
+ this.compressed = {
+ mime: 'image/jpeg',
+ path: compressed.uri,
+ size,
+ ...compressed,
+ }
+ })
+ return
+ }
+ }
+ }
+
+ // Compression fails when removing redundant information is not possible.
+ // This can be tested with images that have high variance in noise.
+ throw new Error('Failed to compress image')
+ }
+
+ // Mobile
async crop() {
try {
+ // openCropper requires an output width and height hence
+ // getting upload dimensions before cropping is necessary.
+ const {width, height} = this.getUploadDimensions({
+ width: this.width,
+ height: this.height,
+ })
+
const cropped = await openCropper(this.rootStore, {
mediaType: 'photo',
path: this.path,
freeStyleCropEnabled: true,
- width: this.scaledWidth,
- height: this.scaledHeight,
+ width,
+ height,
})
runInAction(() => {
@@ -57,29 +185,112 @@ export class ImageModel implements RNImage {
} catch (err) {
this.rootStore.log.error('Failed to crop photo', err)
}
-
- this.compress()
}
- async compress() {
- try {
- const {width, height} = scaleDownDimensions(
- this.cropped
- ? {width: this.cropped.width, height: this.cropped.height}
- : {width: this.width, height: this.height},
- POST_IMG_MAX,
- )
- const compressed = await compressAndResizeImageForPost({
- ...(this.cropped === undefined ? this : this.cropped),
- width,
- height,
+ // Web manipulation
+ async manipulate(
+ attributes: {
+ crop?: ActionCrop['crop']
+ } & ImageManipulationAttributes,
+ ) {
+ let uploadWidth: number | undefined
+ let uploadHeight: number | undefined
+
+ const {aspectRatio, crop, position, scale} = attributes
+ const modifiers = []
+
+ if (this.attributes.flipHorizontal) {
+ modifiers.push({flip: FlipType.Horizontal})
+ }
+
+ if (this.attributes.flipVertical) {
+ modifiers.push({flip: FlipType.Vertical})
+ }
+
+ if (this.attributes.rotate !== undefined) {
+ modifiers.push({rotate: this.attributes.rotate})
+ }
+
+ if (crop !== undefined) {
+ const croppedHeight = crop.height * this.height
+ const croppedWidth = crop.width * this.width
+ modifiers.push({
+ crop: {
+ originX: crop.originX * this.width,
+ originY: crop.originY * this.height,
+ height: croppedHeight,
+ width: croppedWidth,
+ },
})
- runInAction(() => {
- this.compressed = compressed
- })
- } catch (err) {
- this.rootStore.log.error('Failed to compress photo', err)
+ const uploadDimensions = this.getUploadDimensions(
+ {width: croppedWidth, height: croppedHeight},
+ POST_IMG_MAX,
+ aspectRatio,
+ )
+
+ uploadWidth = uploadDimensions.width
+ uploadHeight = uploadDimensions.height
+ } else {
+ const uploadDimensions = this.getUploadDimensions(
+ {width: this.width, height: this.height},
+ POST_IMG_MAX,
+ aspectRatio,
+ )
+
+ uploadWidth = uploadDimensions.width
+ uploadHeight = uploadDimensions.height
}
+
+ if (scale !== undefined) {
+ this.attributes.scale = scale
+ }
+
+ if (position !== undefined) {
+ this.attributes.position = position
+ }
+
+ if (aspectRatio !== undefined) {
+ this.attributes.aspectRatio = aspectRatio
+ }
+
+ const ratioMultiplier =
+ this.ratioMultipliers[this.attributes.aspectRatio ?? '1:1']
+
+ const result = await ImageManipulator.manipulateAsync(
+ this.path,
+ [
+ ...modifiers,
+ {
+ resize:
+ ratioMultiplier > 1 ? {width: uploadWidth} : {height: uploadHeight},
+ },
+ ],
+ {
+ base64: true,
+ format: SaveFormat.JPEG,
+ },
+ )
+
+ runInAction(() => {
+ this.cropped = {
+ mime: 'image/jpeg',
+ path: result.uri,
+ size:
+ result.base64 !== undefined
+ ? getDataUriSize(result.base64)
+ : MAX_IMAGE_SIZE_IN_BYTES + 999, // shouldn't hit this unless manipulation fails
+ ...result,
+ }
+ })
+ }
+
+ resetCropped() {
+ this.manipulate({})
+ }
+
+ previous() {
+ this.cropped = this.prev
+ this.attributes = this.prevAttributes
}
}
diff --git a/src/state/models/muted-threads.ts b/src/state/models/muted-threads.ts
new file mode 100644
index 0000000000..e6f2027452
--- /dev/null
+++ b/src/state/models/muted-threads.ts
@@ -0,0 +1,29 @@
+/**
+ * This is a temporary client-side system for storing muted threads
+ * When the system lands on prod we should switch to that
+ */
+
+import {makeAutoObservable} from 'mobx'
+import {isObj, hasProp, isStrArray} from 'lib/type-guards'
+
+export class MutedThreads {
+ uris: Set = new Set()
+
+ constructor() {
+ makeAutoObservable(
+ this,
+ {serialize: false, hydrate: false},
+ {autoBind: true},
+ )
+ }
+
+ serialize() {
+ return {uris: Array.from(this.uris)}
+ }
+
+ hydrate(v: unknown) {
+ if (isObj(v) && hasProp(v, 'uris') && isStrArray(v.uris)) {
+ this.uris = new Set(v.uris)
+ }
+ }
+}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 9207f27ba6..5a3d102aaf 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -20,6 +20,13 @@ import {InvitedUsers} from './invited-users'
import {PreferencesModel} from './ui/preferences'
import {resetToTab} from '../../Navigation'
import {ImageSizesCache} from './cache/image-sizes'
+import {MutedThreads} from './muted-threads'
+import {reset as resetNavigation} from '../../Navigation'
+
+// TEMPORARY (APP-700)
+// remove after backend testing finishes
+// -prf
+import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header'
export const appInfo = z.object({
build: z.string(),
@@ -35,12 +42,13 @@ export class RootStoreModel {
log = new LogModel()
session = new SessionModel(this)
shell = new ShellUiModel(this)
- preferences = new PreferencesModel()
+ preferences = new PreferencesModel(this)
me = new MeModel(this)
invitedUsers = new InvitedUsers(this)
profiles = new ProfilesCache(this)
linkMetas = new LinkMetasCache(this)
imageSizes = new ImageSizesCache()
+ mutedThreads = new MutedThreads()
constructor(agent: BskyAgent) {
this.agent = agent
@@ -64,6 +72,7 @@ export class RootStoreModel {
shell: this.shell.serialize(),
preferences: this.preferences.serialize(),
invitedUsers: this.invitedUsers.serialize(),
+ mutedThreads: this.mutedThreads.serialize(),
}
}
@@ -90,6 +99,9 @@ export class RootStoreModel {
if (hasProp(v, 'invitedUsers')) {
this.invitedUsers.hydrate(v.invitedUsers)
}
+ if (hasProp(v, 'mutedThreads')) {
+ this.mutedThreads.hydrate(v.mutedThreads)
+ }
}
}
@@ -112,11 +124,19 @@ export class RootStoreModel {
/**
* Called by the session model. Refreshes session-oriented state.
*/
- async handleSessionChange(agent: BskyAgent) {
+ async handleSessionChange(
+ agent: BskyAgent,
+ {hadSession}: {hadSession: boolean},
+ ) {
this.log.debug('RootStoreModel:handleSessionChange')
this.agent = agent
+ applyDebugHeader(this.agent)
this.me.clear()
+ /* dont await */ this.preferences.sync()
await this.me.load()
+ if (!hadSession) {
+ resetNavigation()
+ }
}
/**
@@ -148,6 +168,7 @@ export class RootStoreModel {
}
try {
await this.me.updateIfNeeded()
+ await this.preferences.sync()
} catch (e: any) {
this.log.error('Failed to fetch latest state', e)
}
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index 96e058c027..57082b818c 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -10,6 +10,7 @@ import {isObj, hasProp} from 'lib/type-guards'
import {networkRetry} from 'lib/async/retry'
import {z} from 'zod'
import {RootStoreModel} from './root-store'
+import {IS_PROD} from 'lib/constants'
export type ServiceDescription = DescribeServer.OutputSchema
@@ -104,6 +105,13 @@ export class SessionModel {
return this.accounts.filter(acct => acct.did !== this.data?.did)
}
+ get isSandbox() {
+ if (!this.data) {
+ return false
+ }
+ return !IS_PROD(this.data.service)
+ }
+
serialize(): unknown {
return {
data: this.data,
@@ -158,11 +166,12 @@ export class SessionModel {
*/
async setActiveSession(agent: BskyAgent, did: string) {
this._log('SessionModel:setActiveSession')
+ const hadSession = !!this.data
this.data = {
service: agent.service.toString(),
did,
}
- await this.rootStore.handleSessionChange(agent)
+ await this.rootStore.handleSessionChange(agent, {hadSession})
}
/**
@@ -186,7 +195,7 @@ export class SessionModel {
account => account.service === service && account.did === did,
)
- // fall back to any pre-existing access tokens
+ // fall back to any preexisting access tokens
let refreshJwt = session?.refreshJwt || existingAccount?.refreshJwt
let accessJwt = session?.accessJwt || existingAccount?.accessJwt
if (event === 'expired') {
@@ -246,7 +255,7 @@ export class SessionModel {
const res = await agent.getProfile({actor: did}).catch(_e => undefined)
if (res) {
return {
- dispayName: res.data.displayName,
+ displayName: res.data.displayName,
aviUrl: res.data.avatar,
}
}
diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts
index e661cb59dc..3f83dd6a72 100644
--- a/src/state/models/ui/create-account.ts
+++ b/src/state/models/ui/create-account.ts
@@ -6,6 +6,9 @@ import {ComAtprotoServerCreateAccount} from '@atproto/api'
import * as EmailValidator from 'email-validator'
import {createFullHandle} from 'lib/strings/handles'
import {cleanError} from 'lib/strings/errors'
+import {getAge} from 'lib/strings/time'
+
+const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
export class CreateAccountModel {
step: number = 1
@@ -21,7 +24,7 @@ export class CreateAccountModel {
email = ''
password = ''
handle = ''
- is13 = false
+ birthDate = DEFAULT_DATE
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, {}, {autoBind: true})
@@ -32,6 +35,13 @@ export class CreateAccountModel {
next() {
this.error = ''
+ if (this.step === 2) {
+ if (getAge(this.birthDate) < 13) {
+ this.error =
+ 'Unfortunately, you do not meet the requirements to create an account.'
+ return
+ }
+ }
this.step++
}
@@ -124,8 +134,7 @@ export class CreateAccountModel {
return (
(!this.isInviteCodeRequired || this.inviteCode) &&
!!this.email &&
- !!this.password &&
- this.is13
+ !!this.password
)
}
return !!this.handle
@@ -186,7 +195,7 @@ export class CreateAccountModel {
this.handle = v
}
- setIs13(v: boolean) {
- this.is13 = v
+ setBirthDate(v: Date) {
+ this.birthDate = v
}
}
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index ae3f712c43..a42f0a837e 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -1,22 +1,39 @@
-import {makeAutoObservable} from 'mobx'
+import {makeAutoObservable, runInAction} from 'mobx'
import {getLocales} from 'expo-localization'
+import AwaitLock from 'await-lock'
+import isEqual from 'lodash.isequal'
import {isObj, hasProp} from 'lib/type-guards'
-import {ComAtprotoLabelDefs} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {ComAtprotoLabelDefs, AppBskyActorDefs} from '@atproto/api'
+import {LabelValGroup} from 'lib/labeling/types'
import {getLabelValueGroup} from 'lib/labeling/helpers'
import {
- LabelValGroup,
UNKNOWN_LABEL_GROUP,
ILLEGAL_LABEL_GROUP,
+ ALWAYS_FILTER_LABEL_GROUP,
+ ALWAYS_WARN_LABEL_GROUP,
} from 'lib/labeling/const'
+import {DEFAULT_FEEDS} from 'lib/constants'
+import {isIOS} from 'platform/detection'
const deviceLocales = getLocales()
export type LabelPreference = 'show' | 'warn' | 'hide'
+const LABEL_GROUPS = [
+ 'nsfw',
+ 'nudity',
+ 'suggestive',
+ 'gore',
+ 'hate',
+ 'spam',
+ 'impersonation',
+]
+const VISIBILITY_VALUES = ['show', 'warn', 'hide']
export class LabelPreferencesModel {
- nsfw: LabelPreference = 'warn'
- nudity: LabelPreference = 'show'
- suggestive: LabelPreference = 'show'
+ nsfw: LabelPreference = 'hide'
+ nudity: LabelPreference = 'warn'
+ suggestive: LabelPreference = 'warn'
gore: LabelPreference = 'warn'
hate: LabelPreference = 'hide'
spam: LabelPreference = 'hide'
@@ -28,28 +45,34 @@ export class LabelPreferencesModel {
}
export class PreferencesModel {
- _contentLanguages: string[] | undefined
+ adultContentEnabled = !isIOS
+ contentLanguages: string[] =
+ deviceLocales?.map?.(locale => locale.languageCode) || []
contentLabels = new LabelPreferencesModel()
+ savedFeeds: string[] = []
+ pinnedFeeds: string[] = []
- constructor() {
- makeAutoObservable(this, {}, {autoBind: true})
- }
+ // used to linearize async modifications to state
+ lock = new AwaitLock()
- // gives an array of BCP 47 language tags without region codes
- get contentLanguages() {
- if (this._contentLanguages) {
- return this._contentLanguages
- }
- return deviceLocales.map(locale => locale.languageCode)
+ constructor(public rootStore: RootStoreModel) {
+ makeAutoObservable(this, {lock: false}, {autoBind: true})
}
serialize() {
return {
- contentLanguages: this._contentLanguages,
+ contentLanguages: this.contentLanguages,
contentLabels: this.contentLabels,
+ savedFeeds: this.savedFeeds,
+ pinnedFeeds: this.pinnedFeeds,
}
}
+ /**
+ * The function hydrates an object with properties related to content languages, labels, saved feeds,
+ * and pinned feeds that it gets from the parameter `v` (probably local storage)
+ * @param {unknown} v - the data object to hydrate from
+ */
hydrate(v: unknown) {
if (isObj(v)) {
if (
@@ -57,19 +80,209 @@ export class PreferencesModel {
Array.isArray(v.contentLanguages) &&
typeof v.contentLanguages.every(item => typeof item === 'string')
) {
- this._contentLanguages = v.contentLanguages
+ this.contentLanguages = v.contentLanguages
}
if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
Object.assign(this.contentLabels, v.contentLabels)
+ } else {
+ // default to the device languages
+ this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
+ }
+ if (
+ hasProp(v, 'savedFeeds') &&
+ Array.isArray(v.savedFeeds) &&
+ typeof v.savedFeeds.every(item => typeof item === 'string')
+ ) {
+ this.savedFeeds = v.savedFeeds
+ }
+ if (
+ hasProp(v, 'pinnedFeeds') &&
+ Array.isArray(v.pinnedFeeds) &&
+ typeof v.pinnedFeeds.every(item => typeof item === 'string')
+ ) {
+ this.pinnedFeeds = v.pinnedFeeds
}
}
}
- setContentLabelPref(
+ /**
+ * This function fetches preferences and sets defaults for missing items.
+ */
+ async sync({clearCache}: {clearCache?: boolean} = {}) {
+ await this.lock.acquireAsync()
+ try {
+ // fetch preferences
+ let hasSavedFeedsPref = false
+ const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
+ runInAction(() => {
+ for (const pref of res.data.preferences) {
+ if (
+ AppBskyActorDefs.isAdultContentPref(pref) &&
+ AppBskyActorDefs.validateAdultContentPref(pref).success
+ ) {
+ this.adultContentEnabled = pref.enabled
+ } else if (
+ AppBskyActorDefs.isContentLabelPref(pref) &&
+ AppBskyActorDefs.validateAdultContentPref(pref).success
+ ) {
+ if (
+ LABEL_GROUPS.includes(pref.label) &&
+ VISIBILITY_VALUES.includes(pref.visibility)
+ ) {
+ this.contentLabels[pref.label as keyof LabelPreferencesModel] =
+ pref.visibility as LabelPreference
+ }
+ } else if (
+ AppBskyActorDefs.isSavedFeedsPref(pref) &&
+ AppBskyActorDefs.validateSavedFeedsPref(pref).success
+ ) {
+ if (!isEqual(this.savedFeeds, pref.saved)) {
+ this.savedFeeds = pref.saved
+ }
+ if (!isEqual(this.pinnedFeeds, pref.pinned)) {
+ this.pinnedFeeds = pref.pinned
+ }
+ hasSavedFeedsPref = true
+ }
+ }
+ })
+
+ // set defaults on missing items
+ if (!hasSavedFeedsPref) {
+ const {saved, pinned} = await DEFAULT_FEEDS(
+ this.rootStore.agent.service.toString(),
+ (handle: string) =>
+ this.rootStore.agent
+ .resolveHandle({handle})
+ .then(({data}) => data.did),
+ )
+ runInAction(() => {
+ this.savedFeeds = saved
+ this.pinnedFeeds = pinned
+ })
+ res.data.preferences.push({
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
+ saved,
+ pinned,
+ })
+ await this.rootStore.agent.app.bsky.actor.putPreferences({
+ preferences: res.data.preferences,
+ })
+ }
+ } finally {
+ this.lock.release()
+ }
+
+ await this.rootStore.me.savedFeeds.updateCache(clearCache)
+ }
+
+ /**
+ * This function updates the preferences of a user and allows for a callback function to be executed
+ * before the update.
+ * @param cb - cb is a callback function that takes in a single parameter of type
+ * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to
+ * update the preferences of the user. The function is called with the current preferences as an
+ * argument and if the callback returns false, the preferences are not updated.
+ * @returns void
+ */
+ async update(
+ cb: (
+ prefs: AppBskyActorDefs.Preferences,
+ ) => AppBskyActorDefs.Preferences | false,
+ ) {
+ await this.lock.acquireAsync()
+ try {
+ const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
+ const newPrefs = cb(res.data.preferences)
+ if (newPrefs === false) {
+ return
+ }
+ await this.rootStore.agent.app.bsky.actor.putPreferences({
+ preferences: newPrefs,
+ })
+ } finally {
+ this.lock.release()
+ }
+ }
+
+ /**
+ * This function resets the preferences to an empty array of no preferences.
+ */
+ async reset() {
+ await this.lock.acquireAsync()
+ try {
+ runInAction(() => {
+ this.contentLabels = new LabelPreferencesModel()
+ this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
+ this.savedFeeds = []
+ this.pinnedFeeds = []
+ })
+ await this.rootStore.agent.app.bsky.actor.putPreferences({
+ preferences: [],
+ })
+ } finally {
+ this.lock.release()
+ }
+ }
+
+ hasContentLanguage(code2: string) {
+ return this.contentLanguages.includes(code2)
+ }
+
+ toggleContentLanguage(code2: string) {
+ if (this.hasContentLanguage(code2)) {
+ this.contentLanguages = this.contentLanguages.filter(
+ lang => lang !== code2,
+ )
+ } else {
+ this.contentLanguages = this.contentLanguages.concat([code2])
+ }
+ }
+
+ async setContentLabelPref(
key: keyof LabelPreferencesModel,
value: LabelPreference,
) {
this.contentLabels[key] = value
+
+ await this.update((prefs: AppBskyActorDefs.Preferences) => {
+ const existing = prefs.find(
+ pref =>
+ AppBskyActorDefs.isContentLabelPref(pref) &&
+ AppBskyActorDefs.validateAdultContentPref(pref).success &&
+ pref.label === key,
+ )
+ if (existing) {
+ existing.visibility = value
+ } else {
+ prefs.push({
+ $type: 'app.bsky.actor.defs#contentLabelPref',
+ label: key,
+ visibility: value,
+ })
+ }
+ return prefs
+ })
+ }
+
+ async setAdultContentEnabled(v: boolean) {
+ this.adultContentEnabled = v
+ await this.update((prefs: AppBskyActorDefs.Preferences) => {
+ const existing = prefs.find(
+ pref =>
+ AppBskyActorDefs.isAdultContentPref(pref) &&
+ AppBskyActorDefs.validateAdultContentPref(pref).success,
+ )
+ if (existing) {
+ existing.enabled = v
+ } else {
+ prefs.push({
+ $type: 'app.bsky.actor.defs#adultContentPref',
+ enabled: v,
+ })
+ }
+ return prefs
+ })
}
getLabelPreference(labels: ComAtprotoLabelDefs.Label[] | undefined): {
@@ -87,6 +300,12 @@ export class PreferencesModel {
const group = getLabelValueGroup(label.val)
if (group.id === 'illegal') {
return {pref: 'hide', desc: ILLEGAL_LABEL_GROUP}
+ } else if (group.id === 'always-filter') {
+ return {pref: 'hide', desc: ALWAYS_FILTER_LABEL_GROUP}
+ } else if (group.id === 'always-warn') {
+ res.pref = 'warn'
+ res.desc = ALWAYS_WARN_LABEL_GROUP
+ continue
} else if (group.id === 'unknown') {
continue
}
@@ -99,6 +318,66 @@ export class PreferencesModel {
res.desc = group
}
}
+ if (res.desc.isAdultImagery && !this.adultContentEnabled) {
+ res.pref = 'hide'
+ }
return res
}
+
+ async setSavedFeeds(saved: string[], pinned: string[]) {
+ const oldSaved = this.savedFeeds
+ const oldPinned = this.pinnedFeeds
+ this.savedFeeds = saved
+ this.pinnedFeeds = pinned
+ try {
+ await this.update((prefs: AppBskyActorDefs.Preferences) => {
+ let feedsPref = prefs.find(
+ pref =>
+ AppBskyActorDefs.isSavedFeedsPref(pref) &&
+ AppBskyActorDefs.validateSavedFeedsPref(pref).success,
+ )
+ if (feedsPref) {
+ feedsPref.saved = saved
+ feedsPref.pinned = pinned
+ } else {
+ feedsPref = {
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
+ saved,
+ pinned,
+ }
+ }
+ return prefs
+ .filter(pref => !AppBskyActorDefs.isSavedFeedsPref(pref))
+ .concat([feedsPref])
+ })
+ } catch (e) {
+ runInAction(() => {
+ this.savedFeeds = oldSaved
+ this.pinnedFeeds = oldPinned
+ })
+ throw e
+ }
+ }
+
+ async addSavedFeed(v: string) {
+ return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds)
+ }
+
+ async removeSavedFeed(v: string) {
+ return this.setSavedFeeds(
+ this.savedFeeds.filter(uri => uri !== v),
+ this.pinnedFeeds.filter(uri => uri !== v),
+ )
+ }
+
+ async addPinnedFeed(v: string) {
+ return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v])
+ }
+
+ async removePinnedFeed(v: string) {
+ return this.setSavedFeeds(
+ this.savedFeeds,
+ this.pinnedFeeds.filter(uri => uri !== v),
+ )
+ }
}
diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts
index d06a196f3d..81daf797fc 100644
--- a/src/state/models/ui/profile.ts
+++ b/src/state/models/ui/profile.ts
@@ -2,14 +2,16 @@ import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from '../root-store'
import {ProfileModel} from '../content/profile'
import {PostsFeedModel} from '../feeds/posts'
+import {ActorFeedsModel} from '../lists/actor-feeds'
+import {ListsListModel} from '../lists/lists-list'
export enum Sections {
Posts = 'Posts',
PostsWithReplies = 'Posts & replies',
+ CustomAlgorithms = 'Feeds',
+ Lists = 'Lists',
}
-const USER_SELECTOR_ITEMS = [Sections.Posts, Sections.PostsWithReplies]
-
export interface ProfileUiParams {
user: string
}
@@ -22,6 +24,8 @@ export class ProfileUiModel {
// data
profile: ProfileModel
feed: PostsFeedModel
+ algos: ActorFeedsModel
+ lists: ListsListModel
// ui state
selectedViewIndex = 0
@@ -43,14 +47,21 @@ export class ProfileUiModel {
actor: params.user,
limit: 10,
})
+ this.algos = new ActorFeedsModel(rootStore, {actor: params.user})
+ this.lists = new ListsListModel(rootStore, params.user)
}
- get currentView(): PostsFeedModel {
+ get currentView(): PostsFeedModel | ActorFeedsModel | ListsListModel {
if (
this.selectedView === Sections.Posts ||
this.selectedView === Sections.PostsWithReplies
) {
return this.feed
+ } else if (this.selectedView === Sections.Lists) {
+ return this.lists
+ }
+ if (this.selectedView === Sections.CustomAlgorithms) {
+ return this.algos
}
throw new Error(`Invalid selector value: ${this.selectedViewIndex}`)
}
@@ -65,7 +76,14 @@ export class ProfileUiModel {
}
get selectorItems() {
- return USER_SELECTOR_ITEMS
+ const items = [Sections.Posts, Sections.PostsWithReplies]
+ if (this.algos.hasLoaded && !this.algos.isEmpty) {
+ items.push(Sections.CustomAlgorithms)
+ }
+ if (this.lists.hasLoaded && !this.lists.isEmpty) {
+ items.push(Sections.Lists)
+ }
+ return items
}
get selectedView() {
@@ -74,9 +92,11 @@ export class ProfileUiModel {
get uiItems() {
let arr: any[] = []
+ // if loading, return loading item to show loading spinner
if (this.isInitialLoading) {
arr = arr.concat([ProfileUiModel.LOADING_ITEM])
} else if (this.currentView.hasError) {
+ // if error, return error item to show error message
arr = arr.concat([
{
_reactKey: '__error__',
@@ -84,12 +104,16 @@ export class ProfileUiModel {
},
])
} else {
+ // not loading, no error, show content
if (
this.selectedView === Sections.Posts ||
- this.selectedView === Sections.PostsWithReplies
+ this.selectedView === Sections.PostsWithReplies ||
+ this.selectedView === Sections.CustomAlgorithms
) {
if (this.feed.hasContent) {
- if (this.selectedView === Sections.Posts) {
+ if (this.selectedView === Sections.CustomAlgorithms) {
+ arr = this.algos.feeds
+ } else if (this.selectedView === Sections.Posts) {
arr = this.feed.nonReplyFeed
} else {
arr = this.feed.slices.slice()
@@ -100,7 +124,14 @@ export class ProfileUiModel {
} else if (this.feed.isEmpty) {
arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
}
+ } else if (this.selectedView === Sections.Lists) {
+ if (this.lists.hasContent) {
+ arr = this.lists.lists
+ } else if (this.lists.isEmpty) {
+ arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
+ }
} else {
+ // fallback, add empty item, to show empty message
arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
}
}
@@ -113,6 +144,8 @@ export class ProfileUiModel {
this.selectedView === Sections.PostsWithReplies
) {
return this.feed.hasContent && this.feed.hasMore && this.feed.isLoading
+ } else if (this.selectedView === Sections.Lists) {
+ return this.lists.hasContent && this.lists.hasMore && this.lists.isLoading
}
return false
}
@@ -133,6 +166,12 @@ export class ProfileUiModel {
.setup()
.catch(err => this.rootStore.log.error('Failed to fetch feed', err)),
])
+ this.algos.refresh()
+ // HACK: need to use the DID as a param, not the username -prf
+ this.lists.source = this.profile.did
+ this.lists
+ .loadMore()
+ .catch(err => this.rootStore.log.error('Failed to fetch lists', err))
}
async update() {
diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts
new file mode 100644
index 0000000000..40265f7cf9
--- /dev/null
+++ b/src/state/models/ui/saved-feeds.ts
@@ -0,0 +1,208 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {RootStoreModel} from '../root-store'
+import {bundleAsync} from 'lib/async/bundle'
+import {cleanError} from 'lib/strings/errors'
+import {CustomFeedModel} from '../feeds/custom-feed'
+
+export class SavedFeedsModel {
+ // state
+ isLoading = false
+ isRefreshing = false
+ hasLoaded = false
+ error = ''
+
+ // data
+ _feedModelCache: Record = {}
+
+ constructor(public rootStore: RootStoreModel) {
+ makeAutoObservable(
+ this,
+ {
+ rootStore: false,
+ },
+ {autoBind: true},
+ )
+ }
+
+ get hasContent() {
+ return this.all.length > 0
+ }
+
+ get hasError() {
+ return this.error !== ''
+ }
+
+ get isEmpty() {
+ return this.hasLoaded && !this.hasContent
+ }
+
+ get pinned() {
+ return this.rootStore.preferences.pinnedFeeds
+ .map(uri => this._feedModelCache[uri] as CustomFeedModel)
+ .filter(Boolean)
+ }
+
+ get unpinned() {
+ return this.rootStore.preferences.savedFeeds
+ .filter(uri => !this.isPinned(uri))
+ .map(uri => this._feedModelCache[uri] as CustomFeedModel)
+ .filter(Boolean)
+ }
+
+ get all() {
+ return [...this.pinned, ...this.unpinned]
+ }
+
+ get pinnedFeedNames() {
+ return this.pinned.map(f => f.displayName)
+ }
+
+ // public api
+ // =
+
+ /**
+ * Syncs the cached models against the current state
+ * - Should only be called by the preferences model after syncing state
+ */
+ updateCache = bundleAsync(async (clearCache?: boolean) => {
+ let newFeedModels: Record = {}
+ if (!clearCache) {
+ newFeedModels = {...this._feedModelCache}
+ }
+
+ // collect the feed URIs that havent been synced yet
+ const neededFeedUris = []
+ for (const feedUri of this.rootStore.preferences.savedFeeds) {
+ if (!(feedUri in newFeedModels)) {
+ neededFeedUris.push(feedUri)
+ }
+ }
+
+ // early exit if no feeds need to be fetched
+ if (!neededFeedUris.length || neededFeedUris.length === 0) {
+ return
+ }
+
+ // fetch the missing models
+ try {
+ for (let i = 0; i < neededFeedUris.length; i += 25) {
+ const res = await this.rootStore.agent.app.bsky.feed.getFeedGenerators({
+ feeds: neededFeedUris.slice(i, 25),
+ })
+ for (const feedInfo of res.data.feeds) {
+ newFeedModels[feedInfo.uri] = new CustomFeedModel(
+ this.rootStore,
+ feedInfo,
+ )
+ }
+ }
+ } catch (error) {
+ console.error('Failed to fetch feed models', error)
+ this.rootStore.log.error('Failed to fetch feed models', error)
+ }
+
+ // merge into the cache
+ runInAction(() => {
+ this._feedModelCache = newFeedModels
+ })
+ })
+
+ /**
+ * Refresh the preferences then reload all feed infos
+ */
+ refresh = bundleAsync(async () => {
+ this._xLoading(true)
+ try {
+ await this.rootStore.preferences.sync({clearCache: true})
+ this._xIdle()
+ } catch (e: any) {
+ this._xIdle(e)
+ }
+ })
+
+ async save(feed: CustomFeedModel) {
+ try {
+ await feed.save()
+ await this.updateCache()
+ } catch (e: any) {
+ this.rootStore.log.error('Failed to save feed', e)
+ }
+ }
+
+ async unsave(feed: CustomFeedModel) {
+ const uri = feed.uri
+ try {
+ if (this.isPinned(feed)) {
+ await this.rootStore.preferences.removePinnedFeed(uri)
+ }
+ await feed.unsave()
+ } catch (e: any) {
+ this.rootStore.log.error('Failed to unsave feed', e)
+ }
+ }
+
+ async togglePinnedFeed(feed: CustomFeedModel) {
+ if (!this.isPinned(feed)) {
+ return this.rootStore.preferences.addPinnedFeed(feed.uri)
+ } else {
+ return this.rootStore.preferences.removePinnedFeed(feed.uri)
+ }
+ }
+
+ async reorderPinnedFeeds(feeds: CustomFeedModel[]) {
+ return this.rootStore.preferences.setSavedFeeds(
+ this.rootStore.preferences.savedFeeds,
+ feeds.filter(feed => this.isPinned(feed)).map(feed => feed.uri),
+ )
+ }
+
+ isPinned(feedOrUri: CustomFeedModel | string) {
+ let uri: string
+ if (typeof feedOrUri === 'string') {
+ uri = feedOrUri
+ } else {
+ uri = feedOrUri.uri
+ }
+ return this.rootStore.preferences.pinnedFeeds.includes(uri)
+ }
+
+ async movePinnedFeed(item: CustomFeedModel, direction: 'up' | 'down') {
+ const pinned = this.rootStore.preferences.pinnedFeeds.slice()
+ const index = pinned.indexOf(item.uri)
+ if (index === -1) {
+ return
+ }
+ if (direction === 'up' && index !== 0) {
+ const temp = pinned[index]
+ pinned[index] = pinned[index - 1]
+ pinned[index - 1] = temp
+ } else if (direction === 'down' && index < pinned.length - 1) {
+ const temp = pinned[index]
+ pinned[index] = pinned[index + 1]
+ pinned[index + 1] = temp
+ }
+ await this.rootStore.preferences.setSavedFeeds(
+ this.rootStore.preferences.savedFeeds,
+ pinned,
+ )
+ }
+
+ // state transitions
+ // =
+
+ _xLoading(isRefreshing = false) {
+ this.isLoading = true
+ this.isRefreshing = isRefreshing
+ this.error = ''
+ }
+
+ _xIdle(err?: any) {
+ this.isLoading = false
+ this.isRefreshing = false
+ this.hasLoaded = true
+ this.error = cleanError(err)
+ if (err) {
+ this.rootStore.log.error('Failed to fetch user feeds', err)
+ }
+ }
+}
diff --git a/src/state/models/ui/search.ts b/src/state/models/ui/search.ts
index 330283a0b4..4ab9db5135 100644
--- a/src/state/models/ui/search.ts
+++ b/src/state/models/ui/search.ts
@@ -1,13 +1,14 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {searchProfiles, searchPosts} from 'lib/api/search'
-import {AppBskyActorDefs} from '@atproto/api'
+import {PostThreadModel} from '../content/post-thread'
+import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {RootStoreModel} from '../root-store'
export class SearchUIModel {
isPostsLoading = false
isProfilesLoading = false
query: string = ''
- postUris: string[] = []
+ posts: PostThreadModel[] = []
profiles: AppBskyActorDefs.ProfileView[] = []
constructor(public rootStore: RootStoreModel) {
@@ -15,7 +16,7 @@ export class SearchUIModel {
}
async fetch(q: string) {
- this.postUris = []
+ this.posts = []
this.profiles = []
this.query = q
if (!q.trim()) {
@@ -29,8 +30,22 @@ export class SearchUIModel {
searchPosts(q).catch(_e => []),
searchProfiles(q).catch(_e => []),
])
+
+ let posts: AppBskyFeedDefs.PostView[] = []
+ if (postsSearch?.length) {
+ do {
+ const res = await this.rootStore.agent.app.bsky.feed.getPosts({
+ uris: postsSearch
+ .splice(0, 25)
+ .map(p => `at://${p.user.did}/${p.tid}`),
+ })
+ posts = posts.concat(res.data.posts)
+ } while (postsSearch.length)
+ }
runInAction(() => {
- this.postUris = postsSearch?.map(p => `at://${p.user.did}/${p.tid}`) || []
+ this.posts = posts.map(post =>
+ PostThreadModel.fromPostView(this.rootStore, post),
+ )
this.isPostsLoading = false
})
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index 47cc0aa825..3853f23956 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -3,13 +3,23 @@ import {RootStoreModel} from '../root-store'
import {makeAutoObservable} from 'mobx'
import {ProfileModel} from '../content/profile'
import {isObj, hasProp} from 'lib/type-guards'
-import {Image} from 'lib/media/types'
+import {Image as RNImage} from 'react-native-image-crop-picker'
+import {ImageModel} from '../media/image'
+import {ListModel} from '../content/list'
+import {GalleryModel} from '../media/gallery'
+
+export type ColorMode = 'system' | 'light' | 'dark'
+
+export function isColorMode(v: unknown): v is ColorMode {
+ return v === 'system' || v === 'light' || v === 'dark'
+}
export interface ConfirmModal {
name: 'confirm'
title: string
message: string | (() => JSX.Element)
onPressConfirm: () => void | Promise
+ onPressCancel?: () => void | Promise
}
export interface EditProfileModal {
@@ -35,10 +45,34 @@ export interface ReportAccountModal {
did: string
}
+export interface CreateOrEditMuteListModal {
+ name: 'create-or-edit-mute-list'
+ list?: ListModel
+ onSave?: (uri: string) => void
+}
+
+export interface ListAddRemoveUserModal {
+ name: 'list-add-remove-user'
+ subject: string
+ displayName: string
+ onUpdate?: () => void
+}
+
+export interface EditImageModal {
+ name: 'edit-image'
+ image: ImageModel
+ gallery: GalleryModel
+}
+
export interface CropImageModal {
name: 'crop-image'
uri: string
- onSelect: (img?: Image) => void
+ onSelect: (img?: RNImage) => void
+}
+
+export interface AltTextImageModal {
+ name: 'alt-text-image'
+ image: ImageModel
}
export interface DeleteAccountModal {
@@ -65,23 +99,48 @@ export interface InviteCodesModal {
name: 'invite-codes'
}
+export interface AddAppPasswordModal {
+ name: 'add-app-password'
+}
+
export interface ContentFilteringSettingsModal {
name: 'content-filtering-settings'
}
+export interface ContentLanguagesSettingsModal {
+ name: 'content-languages-settings'
+}
+
export type Modal =
- | ConfirmModal
- | EditProfileModal
- | ServerInputModal
- | ReportPostModal
- | ReportAccountModal
- | CropImageModal
- | DeleteAccountModal
- | RepostModal
+ // Account
+ | AddAppPasswordModal
| ChangeHandleModal
+ | DeleteAccountModal
+ | EditProfileModal
+
+ // Curation
+ | ContentFilteringSettingsModal
+ | ContentLanguagesSettingsModal
+
+ // Moderation
+ | ReportAccountModal
+ | ReportPostModal
+ | CreateOrEditMuteListModal
+ | ListAddRemoveUserModal
+
+ // Posts
+ | AltTextImageModal
+ | CropImageModal
+ | EditImageModal
+ | ServerInputModal
+ | RepostModal
+
+ // Bluesky access
| WaitlistModal
| InviteCodesModal
- | ContentFilteringSettingsModal
+
+ // Generic
+ | ConfirmModal
interface LightboxModel {}
@@ -92,9 +151,14 @@ export class ProfileImageLightbox implements LightboxModel {
}
}
+interface ImagesLightboxItem {
+ uri: string
+ alt?: string
+}
+
export class ImagesLightbox implements LightboxModel {
name = 'images'
- constructor(public uris: string[], public index: number) {
+ constructor(public images: ImagesLightboxItem[], public index: number) {
makeAutoObservable(this)
}
setIndex(index: number) {
@@ -131,14 +195,14 @@ export interface ComposerOpts {
}
export class ShellUiModel {
- darkMode = false
+ colorMode: ColorMode = 'system'
minimalShellMode = false
isDrawerOpen = false
isDrawerSwipeDisabled = false
isModalActive = false
activeModals: Modal[] = []
isLightboxActive = false
- activeLightbox: ProfileImageLightbox | ImagesLightbox | undefined
+ activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null
isComposerActive = false
composerOpts: ComposerOpts | undefined
@@ -152,26 +216,50 @@ export class ShellUiModel {
serialize(): unknown {
return {
- darkMode: this.darkMode,
+ colorMode: this.colorMode,
}
}
hydrate(v: unknown) {
if (isObj(v)) {
- if (hasProp(v, 'darkMode') && typeof v.darkMode === 'boolean') {
- this.darkMode = v.darkMode
+ if (hasProp(v, 'colorMode') && isColorMode(v.colorMode)) {
+ this.colorMode = v.colorMode
}
}
}
- setDarkMode(v: boolean) {
- this.darkMode = v
+ setColorMode(mode: ColorMode) {
+ this.colorMode = mode
}
setMinimalShellMode(v: boolean) {
this.minimalShellMode = v
}
+ /**
+ * returns true if something was closed
+ * (used by the android hardware back btn)
+ */
+ closeAnyActiveElement(): boolean {
+ if (this.isLightboxActive) {
+ this.closeLightbox()
+ return true
+ }
+ if (this.isModalActive) {
+ this.closeModal()
+ return true
+ }
+ if (this.isComposerActive) {
+ this.closeComposer()
+ return true
+ }
+ if (this.isDrawerOpen) {
+ this.closeDrawer()
+ return true
+ }
+ return false
+ }
+
openDrawer() {
this.isDrawerOpen = true
}
@@ -203,7 +291,7 @@ export class ShellUiModel {
closeLightbox() {
this.isLightboxActive = false
- this.activeLightbox = undefined
+ this.activeLightbox = null
}
openComposer(opts: ComposerOpts) {
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
index f98bed1203..67453f111a 100644
--- a/src/view/com/auth/SplashScreen.tsx
+++ b/src/view/com/auth/SplashScreen.tsx
@@ -28,7 +28,10 @@ export const SplashScreen = ({
+ onPress={onPressCreateAccount}
+ accessibilityRole="button"
+ accessibilityLabel="Create new account"
+ accessibilityHint="Opens flow to create a new Bluesky account">
Create a new account
@@ -36,8 +39,11 @@ export const SplashScreen = ({
- Sign in
+ onPress={onPressSignin}
+ accessibilityRole="button"
+ accessibilityLabel="Sign in"
+ accessibilityHint="Opens flow to sign into your existing Bluesky account">
+ Sign In
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
index 7fac5a8c0d..bb2ac3ef84 100644
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -43,7 +43,9 @@ export const SplashScreen = ({
+ onPress={onPressCreateAccount}
+ // TODO: web accessibility
+ accessibilityRole="button">
Create a new account
@@ -51,8 +53,10 @@ export const SplashScreen = ({
- Sign in
+ onPress={onPressSignin}
+ // TODO: web accessibility
+ accessibilityRole="button">
+ Sign In
Bluesky will launch soon.{' '}
-
+
Join the waitlist
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
index 612be6484e..97200709b4 100644
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ b/src/view/com/auth/create/CreateAccount.tsx
@@ -10,7 +10,7 @@ import {
import {observer} from 'mobx-react-lite'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
-import {s, colors} from 'lib/styles'
+import {s} from 'lib/styles'
import {useStores} from 'state/index'
import {CreateAccountModel} from 'state/models/ui/create-account'
import {usePalette} from 'lib/hooks/usePalette'
@@ -72,14 +72,20 @@ export const CreateAccount = observer(
{model.step === 3 && }
-
+
Back
{model.canNext ? (
-
+
{model.isProcessing ? (
) : (
@@ -91,7 +97,11 @@ export const CreateAccount = observer(
) : model.didServiceDescriptionFetchFail ? (
+ onPress={onPressRetryConnect}
+ accessibilityRole="button"
+ accessibilityLabel="Retry"
+ accessibilityHint="Retries account creation"
+ accessibilityLiveRegion="polite">
Retry
@@ -117,118 +127,4 @@ const styles = StyleSheet.create({
paddingHorizontal: 20,
paddingVertical: 20,
},
-
- noTopBorder: {
- borderTopWidth: 0,
- },
- logoHero: {
- paddingTop: 30,
- paddingBottom: 40,
- },
- group: {
- borderWidth: 1,
- borderRadius: 10,
- marginBottom: 20,
- marginHorizontal: 20,
- },
- groupLabel: {
- paddingHorizontal: 20,
- paddingBottom: 5,
- },
- groupContent: {
- borderTopWidth: 1,
- flexDirection: 'row',
- alignItems: 'center',
- },
- groupContentIcon: {
- marginLeft: 10,
- },
- textInput: {
- flex: 1,
- width: '100%',
- paddingVertical: 10,
- paddingHorizontal: 12,
- fontSize: 17,
- letterSpacing: 0.25,
- fontWeight: '400',
- borderRadius: 10,
- },
- textBtn: {
- flexDirection: 'row',
- flex: 1,
- alignItems: 'center',
- },
- textBtnLabel: {
- flex: 1,
- paddingVertical: 10,
- paddingHorizontal: 12,
- },
- textBtnFakeInnerBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- borderRadius: 6,
- paddingVertical: 6,
- paddingHorizontal: 8,
- marginHorizontal: 6,
- },
- textBtnFakeInnerBtnIcon: {
- marginRight: 4,
- },
- picker: {
- flex: 1,
- width: '100%',
- paddingVertical: 10,
- paddingHorizontal: 12,
- fontSize: 17,
- borderRadius: 10,
- },
- pickerLabel: {
- fontSize: 17,
- },
- checkbox: {
- borderWidth: 1,
- borderRadius: 2,
- width: 16,
- height: 16,
- marginLeft: 16,
- },
- checkboxFilled: {
- borderWidth: 1,
- borderRadius: 2,
- width: 16,
- height: 16,
- marginLeft: 16,
- },
- policies: {
- flexDirection: 'row',
- alignItems: 'flex-start',
- paddingHorizontal: 20,
- paddingBottom: 20,
- },
- error: {
- backgroundColor: colors.red4,
- flexDirection: 'row',
- alignItems: 'center',
- marginTop: -5,
- marginHorizontal: 20,
- marginBottom: 15,
- borderRadius: 8,
- paddingHorizontal: 8,
- paddingVertical: 8,
- },
- errorFloating: {
- marginBottom: 20,
- marginHorizontal: 20,
- borderRadius: 8,
- },
- errorIcon: {
- borderWidth: 1,
- borderColor: colors.white,
- borderRadius: 30,
- width: 16,
- height: 16,
- alignItems: 'center',
- justifyContent: 'center',
- marginRight: 5,
- },
})
diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx
index ca964ede2b..57747f070e 100644
--- a/src/view/com/auth/create/Step1.tsx
+++ b/src/view/com/auth/create/Step1.tsx
@@ -32,7 +32,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
model.setServiceDescription(undefined)
}, [setIsDefaultSelected, model])
- const fetchServiceDesription = React.useMemo(
+ const fetchServiceDescription = React.useMemo(
() => debounce(() => model.fetchServiceDescription(), 1e3),
[model],
)
@@ -40,9 +40,9 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
const onChangeServiceUrl = React.useCallback(
(v: string) => {
model.setServiceUrl(v)
- fetchServiceDesription()
+ fetchServiceDescription()
},
- [model, fetchServiceDesription],
+ [model, fetchServiceDescription],
)
const onDebugChangeServiceUrl = React.useCallback(
@@ -57,7 +57,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
- This is the company that keeps you online.
+ This is the service that keeps you online.
{
label="Other"
onPress={onPressOther}>
-
+
Enter the address of your provider:
{
value={model.serviceUrl}
editable
onChange={onChangeServiceUrl}
+ accessibilityHint="Input hosting provider address"
+ accessibilityLabel="Hosting provider address"
+ accessibilityLabelledBy="addressProvider"
/>
{LOGIN_INCLUDE_DEV_SERVERS && (
@@ -136,7 +139,12 @@ function Option({
return (
-
+
{isSelected ? (
diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx
index 5a70e31a03..1e014f18e5 100644
--- a/src/view/com/auth/create/Step2.tsx
+++ b/src/view/com/auth/create/Step2.tsx
@@ -1,14 +1,9 @@
import React from 'react'
-import {
- StyleSheet,
- TouchableOpacity,
- TouchableWithoutFeedback,
- View,
-} from 'react-native'
+import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import {observer} from 'mobx-react-lite'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {CreateAccountModel} from 'state/models/ui/create-account'
import {Text} from 'view/com/util/text/Text'
+import {DateInput} from 'view/com/util/forms/DateInput'
import {StepHeader} from './StepHeader'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
@@ -41,14 +36,21 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
value={model.inviteCode}
editable
onChange={model.setInviteCode}
+ accessibilityRole="button"
+ accessibilityLabel="Invite code"
+ accessibilityHint="Input invite code to proceed"
/>
)}
{!model.inviteCode && model.isInviteCodeRequired ? (
-
+
Don't have an invite code?{' '}
-
+
Join the waitlist
{' '}
to try the beta before it's publicly available.
@@ -56,7 +58,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
) : (
<>
-
+
Email address
{
value={model.email}
editable
onChange={model.setEmail}
+ accessibilityLabel="Email"
+ accessibilityHint="Input email for Bluesky waitlist"
+ accessibilityLabelledBy="email"
/>
-
+
Password
{
editable
secureTextEntry
onChange={model.setPassword}
+ accessibilityLabel="Password"
+ accessibilityHint="Set password"
+ accessibilityLabelledBy="password"
/>
-
- Legal check
+
+ Your birth date
- model.setIs13(!model.is13)}>
-
- {model.is13 && (
-
- )}
-
-
- I am 13 years old or older
-
-
+
{model.serviceDescription && (
@@ -121,26 +133,9 @@ const styles = StyleSheet.create({
marginTop: 10,
},
- toggleBtn: {
- flexDirection: 'row',
- flex: 1,
- alignItems: 'center',
+ dateInputButton: {
borderWidth: 1,
- paddingHorizontal: 10,
- paddingVertical: 10,
borderRadius: 6,
- },
- toggleBtnLabel: {
- flex: 1,
- paddingHorizontal: 10,
- },
-
- checkbox: {
- borderWidth: 1,
- borderRadius: 2,
- width: 24,
- height: 24,
- alignItems: 'center',
- justifyContent: 'center',
+ paddingVertical: 14,
},
})
diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx
index 13ab39a10f..bf26231a0a 100644
--- a/src/view/com/auth/create/Step3.tsx
+++ b/src/view/com/auth/create/Step3.tsx
@@ -19,10 +19,13 @@ export const Step3 = observer(({model}: {model: CreateAccountModel}) => {
Your full handle will be{' '}
diff --git a/src/view/com/auth/create/StepHeader.tsx b/src/view/com/auth/create/StepHeader.tsx
index 8c852b640a..4b4eb5d23b 100644
--- a/src/view/com/auth/create/StepHeader.tsx
+++ b/src/view/com/auth/create/StepHeader.tsx
@@ -7,10 +7,12 @@ export function StepHeader({step, title}: {step: string; title: string}) {
const pal = usePalette('default')
return (
-
+
{step === '3' ? 'Last step!' : <>Step {step} of 3>}
- {title}
+
+ {title}
+
)
}
diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx
index 7c5b145f30..af4f01874a 100644
--- a/src/view/com/auth/login/Login.tsx
+++ b/src/view/com/auth/login/Login.tsx
@@ -1,4 +1,4 @@
-import React, {useState, useEffect} from 'react'
+import React, {useState, useEffect, useRef} from 'react'
import {
ActivityIndicator,
Keyboard,
@@ -195,7 +195,10 @@ const ChooseAccountForm = ({
testID={`chooseAccountBtn-${account.handle}`}
key={account.did}
style={[pal.view, pal.border, styles.account]}
- onPress={() => onTryAccount(account)}>
+ onPress={() => onTryAccount(account)}
+ accessibilityRole="button"
+ accessibilityLabel={`Sign in as ${account.handle}`}
+ accessibilityHint="Double tap to sign in">
@@ -220,7 +223,10 @@ const ChooseAccountForm = ({
onSelectAccount(undefined)}>
+ onPress={() => onSelectAccount(undefined)}
+ accessibilityRole="button"
+ accessibilityLabel="Login to account that is not listed"
+ accessibilityHint="">
@@ -235,7 +241,7 @@ const ChooseAccountForm = ({
-
+
Back
@@ -276,6 +282,7 @@ const LoginForm = ({
const [isProcessing, setIsProcessing] = useState(false)
const [identifier, setIdentifier] = useState(initialHandle)
const [password, setPassword] = useState('')
+ const passwordInputRef = useRef(null)
const onPressSelectService = () => {
store.shell.openModal({
@@ -288,6 +295,7 @@ const LoginForm = ({
}
const onPressNext = async () => {
+ Keyboard.dismiss()
setError('')
setIsProcessing(true)
@@ -351,7 +359,10 @@ const LoginForm = ({
+ onPress={onPressSelectService}
+ accessibilityRole="button"
+ accessibilityLabel="Select service"
+ accessibilityHint="Sets server for the Bluesky client">
{toNiceDomain(serviceUrl)}
@@ -382,10 +393,18 @@ const LoginForm = ({
autoCapitalize="none"
autoFocus
autoCorrect={false}
+ autoComplete="username"
+ returnKeyType="next"
+ onSubmitEditing={() => {
+ passwordInputRef.current?.focus()
+ }}
+ blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
keyboardAppearance={theme.colorScheme}
value={identifier}
onChangeText={str => setIdentifier((str || '').toLowerCase())}
editable={!isProcessing}
+ accessibilityLabel="Username or email address"
+ accessibilityHint="Input the username or email address you used at signup"
/>
@@ -395,21 +414,38 @@ const LoginForm = ({
/>
+ onPress={onPressForgotPassword}
+ accessibilityRole="button"
+ accessibilityLabel="Forgot password"
+ accessibilityHint="Opens password reset form">
Forgot
@@ -425,7 +461,7 @@ const LoginForm = ({
) : undefined}
-
+
Back
@@ -434,7 +470,10 @@ const LoginForm = ({
{!serviceDescription && error ? (
+ onPress={onPressRetryConnect}
+ accessibilityRole="button"
+ accessibilityLabel="Retry"
+ accessibilityHint="Retries login">
Retry
@@ -449,7 +488,12 @@ const LoginForm = ({
) : isProcessing ? (
) : isReady ? (
-
+
Next
@@ -539,7 +583,10 @@ const ForgotPasswordForm = ({
+ onPress={onPressSelectService}
+ accessibilityRole="button"
+ accessibilityLabel="Hosting provider"
+ accessibilityHint="Sets hosting provider for password reset">
@@ -586,7 +635,7 @@ const ForgotPasswordForm = ({
) : undefined}
-
+
Back
@@ -599,7 +648,12 @@ const ForgotPasswordForm = ({
Next
) : (
-
+
Next
@@ -649,8 +703,9 @@ const SetNewPasswordForm = ({
try {
const agent = new BskyAgent({service: serviceUrl})
+ const token = resetCode.replace(/\s/g, '')
await agent.com.atproto.server.resetPassword({
- token: resetCode,
+ token,
password,
})
onPasswordSet()
@@ -699,6 +754,9 @@ const SetNewPasswordForm = ({
value={resetCode}
onChangeText={setResetCode}
editable={!isProcessing}
+ accessible={true}
+ accessibilityLabel="Reset code"
+ accessibilityHint="Input code sent to your email for password reset"
/>
@@ -718,6 +776,9 @@ const SetNewPasswordForm = ({
value={password}
onChangeText={setPassword}
editable={!isProcessing}
+ accessible={true}
+ accessibilityLabel="Password"
+ accessibilityHint="Input new password"
/>
@@ -732,7 +793,7 @@ const SetNewPasswordForm = ({
) : undefined}
-
+
Back
@@ -747,7 +808,10 @@ const SetNewPasswordForm = ({
) : (
+ onPress={onPressNext}
+ accessibilityRole="button"
+ accessibilityLabel="Go to next"
+ accessibilityHint="Navigates to the next screen">
Next
@@ -783,7 +847,11 @@ const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
-
+
Okay
diff --git a/src/view/com/auth/util/TextInput.tsx b/src/view/com/auth/util/TextInput.tsx
index 934bf2acfa..38aff03849 100644
--- a/src/view/com/auth/util/TextInput.tsx
+++ b/src/view/com/auth/util/TextInput.tsx
@@ -1,27 +1,17 @@
-import React from 'react'
+import React, {ComponentProps} from 'react'
import {StyleSheet, TextInput as RNTextInput, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
-export function TextInput({
- testID,
- icon,
- value,
- placeholder,
- editable,
- secureTextEntry,
- onChange,
-}: {
+interface Props extends Omit, 'onChange'> {
testID?: string
icon: IconProp
- value: string
- placeholder: string
- editable: boolean
- secureTextEntry?: boolean
onChange: (v: string) => void
-}) {
+}
+
+export function TextInput({testID, icon, onChange, ...props}: Props) {
const theme = useTheme()
const pal = usePalette('default')
return (
@@ -30,15 +20,12 @@ export function TextInput({
onChange(v)}
- editable={editable}
+ {...props}
/>
)
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index ffead4d234..37569fbeca 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -7,7 +7,6 @@ import {
ScrollView,
StyleSheet,
TouchableOpacity,
- TouchableWithoutFeedback,
View,
} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
@@ -19,6 +18,8 @@ import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {ExternalEmbed} from './ExternalEmbed'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
+// TODO: Prevent naming components that coincide with RN primitives
+// due to linting false positives
import {TextInput, TextInputRef} from './text-input/TextInput'
import {CharProgress} from './char-progress/CharProgress'
import {UserAvatar} from '../util/UserAvatar'
@@ -33,11 +34,10 @@ import {OpenCameraBtn} from './photos/OpenCameraBtn'
import {usePalette} from 'lib/hooks/usePalette'
import QuoteEmbed from '../util/post-embeds/QuoteEmbed'
import {useExternalLinkFetch} from './useExternalLinkFetch'
-import {isDesktopWeb} from 'platform/detection'
+import {isDesktopWeb, isAndroid} from 'platform/detection'
import {GalleryModel} from 'state/models/media/gallery'
import {Gallery} from './photos/Gallery'
-
-const MAX_GRAPHEME_LENGTH = 300
+import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
type Props = ComposerOpts & {
onClose: () => void
@@ -87,26 +87,35 @@ export const ComposePost = observer(function ComposePost({
autocompleteView.setup()
}, [autocompleteView])
- useEffect(() => {
- // HACK
- // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
- // -prf
- let to: NodeJS.Timeout | undefined
- if (textInput.current) {
- to = setTimeout(() => {
- textInput.current?.focus()
- }, 250)
- }
- return () => {
- if (to) {
- clearTimeout(to)
- }
- }
- }, [])
+ const onEscape = useCallback(
+ (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ const {shell} = store
- const onPressContainer = useCallback(() => {
- textInput.current?.focus()
- }, [textInput])
+ if (shell.activeModals.some(modal => modal.name === 'confirm')) {
+ store.shell.closeModal()
+ }
+
+ shell.openModal({
+ name: 'confirm',
+ title: 'Cancel draft',
+ onPressConfirm: onClose,
+ onPressCancel: () => {
+ store.shell.closeModal()
+ },
+ message: "Are you sure you'd like to cancel this draft?",
+ })
+ }
+ },
+ [store, onClose],
+ )
+
+ useEffect(() => {
+ if (isDesktopWeb) {
+ window.addEventListener('keydown', onEscape)
+ return () => window.removeEventListener('keydown', onEscape)
+ }
+ }, [onEscape])
const onPressAddLinkCard = useCallback(
(uri: string) => {
@@ -133,16 +142,17 @@ export const ComposePost = observer(function ComposePost({
if (rt.text.trim().length === 0 && gallery.isEmpty) {
setError('Did you want to say anything?')
- return false
+ return
}
setIsProcessing(true)
+ let createdPost
try {
- await apilib.post(store, {
+ createdPost = await apilib.post(store, {
rawText: rt.text,
replyTo: replyTo?.uri,
- images: gallery.paths,
+ images: gallery.images,
quote: quote,
extLink: extLink,
onStateChange: setProcessingState,
@@ -163,7 +173,9 @@ export const ComposePost = observer(function ComposePost({
setIsProcessing(false)
return
}
- store.me.mainFeed.checkForLatest({autoPrepend: true})
+ if (!replyTo) {
+ store.me.mainFeed.addPostToTop(createdPost.uri)
+ }
onPost?.()
hackfixOnClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
@@ -187,16 +199,12 @@ export const ComposePost = observer(function ComposePost({
const canPost = graphemeLength <= MAX_GRAPHEME_LENGTH
- const selectTextInputPlaceholder = replyTo
- ? 'Write your reply'
- : gallery.isEmpty
- ? 'Write a comment'
- : "What's up?"
+ const selectTextInputPlaceholder = replyTo ? 'Write your reply' : "What's up?"
- const canSelectImages = gallery.size <= 4
+ const canSelectImages = gallery.size < 4
const viewStyles = {
- paddingBottom: Platform.OS === 'android' ? insets.bottom : 0,
- paddingTop: Platform.OS === 'android' ? insets.top : 15,
+ paddingBottom: isAndroid ? insets.bottom : 0,
+ paddingTop: isAndroid ? insets.top : isDesktopWeb ? 0 : 15,
}
return (
@@ -204,133 +212,149 @@ export const ComposePost = observer(function ComposePost({
testID="composePostView"
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.outer}>
-
-
-
-
- Cancel
-
-
- {isProcessing ? (
-
-
-
- ) : canPost ? (
- {
- onPressPublish(richtext)
- }}>
-
-
- {replyTo ? 'Reply' : 'Post'}
-
-
-
- ) : (
-
- Post
-
- )}
-
+
+
+
+ Cancel
+
+
{isProcessing ? (
-
- {processingState}
+
+
- ) : undefined}
- {error !== '' && (
-
-
-
-
- {error}
+ ) : canPost ? (
+ {
+ onPressPublish(richtext)
+ }}
+ accessibilityRole="button"
+ accessibilityLabel={replyTo ? 'Publish reply' : 'Publish post'}
+ accessibilityHint={
+ replyTo
+ ? 'Double tap to publish your reply'
+ : 'Double tap to publish your post'
+ }>
+
+
+ {replyTo ? 'Reply' : 'Post'}
+
+
+
+ ) : (
+
+ Post
)}
-
- {replyTo ? (
-
-
-
-
- {sanitizeDisplayName(
- replyTo.author.displayName || replyTo.author.handle,
- )}
-
-
- {replyTo.text}
-
-
-
- ) : undefined}
-
-
-
-
-
-
-
- {gallery.isEmpty && extLink && (
- setExtLink(undefined)}
- />
- )}
- {quote ? (
-
-
-
- ) : undefined}
-
- {!extLink && suggestedLinks.size > 0 ? (
-
- {Array.from(suggestedLinks).map(url => (
- onPressAddLinkCard(url)}>
-
- Add link card: {url}
-
-
- ))}
-
- ) : null}
-
- {canSelectImages ? (
- <>
-
-
- >
- ) : null}
-
-
-
-
+ {isProcessing ? (
+
+ {processingState}
+
+ ) : undefined}
+ {error !== '' && (
+
+
+
+
+ {error}
+
+ )}
+
+ {replyTo ? (
+
+
+
+
+ {sanitizeDisplayName(
+ replyTo.author.displayName || replyTo.author.handle,
+ )}
+
+
+ {replyTo.text}
+
+
+
+ ) : undefined}
+
+
+
+
+
+
+
+ {gallery.isEmpty && extLink && (
+ setExtLink(undefined)}
+ />
+ )}
+ {quote ? (
+
+
+
+ ) : undefined}
+
+ {!extLink && suggestedLinks.size > 0 ? (
+
+ {Array.from(suggestedLinks).map(url => (
+ onPressAddLinkCard(url)}
+ accessibilityRole="button"
+ accessibilityLabel="Add link card"
+ accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
+
+ Add link card: {url}
+
+
+ ))}
+
+ ) : null}
+
+ {canSelectImages ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+
+
)
})
diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx
index b6a45f6a3b..a938562bd4 100644
--- a/src/view/com/composer/ExternalEmbed.tsx
+++ b/src/view/com/composer/ExternalEmbed.tsx
@@ -60,7 +60,13 @@ export const ExternalEmbed = ({
)}
-
+
diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx
index 301b900933..98a10b0f5d 100644
--- a/src/view/com/composer/Prompt.tsx
+++ b/src/view/com/composer/Prompt.tsx
@@ -13,7 +13,10 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
onPressCompose()}>
+ onPress={() => onPressCompose()}
+ accessibilityRole="button"
+ accessibilityLabel="Compose reply"
+ accessibilityHint="Opens composer">
DANGER_LENGTH ? '#e60000' : pal.colors.link
return (
<>
- {MAX_LENGTH - count}
+
+ {MAX_GRAPHEME_LENGTH - count}
+
{count > DANGER_LENGTH ? (
) : (
)}
diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx
index f4dfc88fad..f46c053333 100644
--- a/src/view/com/composer/photos/Gallery.tsx
+++ b/src/view/com/composer/photos/Gallery.tsx
@@ -1,4 +1,5 @@
import React, {useCallback} from 'react'
+import {ImageStyle, Keyboard} from 'react-native'
import {GalleryModel} from 'state/models/media/gallery'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -6,24 +7,40 @@ import {colors} from 'lib/styles'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {ImageModel} from 'state/models/media/image'
import {Image} from 'expo-image'
+import {Text} from 'view/com/util/text/Text'
+import {isDesktopWeb} from 'platform/detection'
+import {openAltTextModal} from 'lib/media/alt-text'
+import {useStores} from 'state/index'
interface Props {
gallery: GalleryModel
}
export const Gallery = observer(function ({gallery}: Props) {
+ const store = useStores()
const getImageStyle = useCallback(() => {
- switch (gallery.size) {
- case 1:
- return styles.image250
- case 2:
- return styles.image175
- default:
- return styles.image85
+ let side: number
+
+ if (gallery.size === 1) {
+ side = 250
+ } else {
+ side = (isDesktopWeb ? 560 : 350) / gallery.size
+ }
+
+ return {
+ height: side,
+ width: side,
}
}, [gallery])
const imageStyle = getImageStyle()
+ const handleAddImageAltText = useCallback(
+ (image: ImageModel) => {
+ Keyboard.dismiss()
+ openAltTextModal(store, image)
+ },
+ [store],
+ )
const handleRemovePhoto = useCallback(
(image: ImageModel) => {
gallery.remove(image)
@@ -33,53 +50,115 @@ export const Gallery = observer(function ({gallery}: Props) {
const handleEditPhoto = useCallback(
(image: ImageModel) => {
- gallery.crop(image)
+ gallery.edit(image)
},
[gallery],
)
+ const isOverflow = !isDesktopWeb && gallery.size > 2
+
+ const imageControlLabelStyle = {
+ borderRadius: 5,
+ paddingHorizontal: 10,
+ position: 'absolute' as const,
+ zIndex: 1,
+ ...(isOverflow
+ ? {
+ left: 4,
+ bottom: 4,
+ }
+ : isDesktopWeb && gallery.size < 3
+ ? {
+ left: 8,
+ top: 8,
+ }
+ : {
+ left: 4,
+ top: 4,
+ }),
+ }
+
+ const imageControlsSubgroupStyle = {
+ display: 'flex' as const,
+ flexDirection: 'row' as const,
+ position: 'absolute' as const,
+ ...(isOverflow
+ ? {
+ top: 4,
+ right: 4,
+ gap: 4,
+ }
+ : isDesktopWeb && gallery.size < 3
+ ? {
+ top: 8,
+ right: 8,
+ gap: 8,
+ }
+ : {
+ top: 4,
+ right: 4,
+ gap: 4,
+ }),
+ zIndex: 1,
+ }
+
return !gallery.isEmpty ? (
- {gallery.images.map(image =>
- image.compressed !== undefined ? (
-
-
- {
- handleEditPhoto(image)
- }}
- style={styles.imageControl}>
-
-
- handleRemovePhoto(image)}
- style={styles.imageControl}>
-
-
-
-
- (
+
+ {
+ handleAddImageAltText(image)
+ }}
+ style={imageControlLabelStyle}>
+ ALT
+
+
+ {
+ handleEditPhoto(image)
}}
- />
+ style={styles.imageControl}>
+
+
+ handleRemovePhoto(image)}
+ style={styles.imageControl}>
+
+
- ) : null,
- )}
+
+
+
+ ))}
) : null
})
@@ -88,36 +167,13 @@ const styles = StyleSheet.create({
gallery: {
flex: 1,
flexDirection: 'row',
+ gap: 8,
marginTop: 16,
},
- imageContainer: {
- margin: 2,
- },
image: {
resizeMode: 'cover',
borderRadius: 8,
},
- image250: {
- width: 250,
- height: 250,
- },
- image175: {
- width: 175,
- height: 175,
- },
- image85: {
- width: 85,
- height: 85,
- },
- imageControls: {
- position: 'absolute',
- display: 'flex',
- flexDirection: 'row',
- gap: 4,
- top: 8,
- right: 8,
- zIndex: 1,
- },
imageControl: {
width: 24,
height: 24,
@@ -127,4 +183,15 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
+ imageControlTextContent: {
+ borderRadius: 6,
+ color: 'white',
+ fontSize: 12,
+ fontWeight: 'bold',
+ letterSpacing: 1,
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
+ borderWidth: 0.5,
+ paddingHorizontal: 10,
+ paddingVertical: 3,
+ },
})
diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx
index 8897394145..0f955984d4 100644
--- a/src/view/com/composer/photos/OpenCameraBtn.tsx
+++ b/src/view/com/composer/photos/OpenCameraBtn.tsx
@@ -1,5 +1,5 @@
import React, {useCallback} from 'react'
-import {TouchableOpacity} from 'react-native'
+import {TouchableOpacity, StyleSheet} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -7,7 +7,6 @@ import {
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics'
import {useStores} from 'state/index'
-import {s} from 'lib/styles'
import {isDesktopWeb} from 'platform/detection'
import {openCamera} from 'lib/media/picker'
import {useCameraPermission} from 'lib/hooks/usePermissions'
@@ -54,8 +53,11 @@ export function OpenCameraBtn({gallery}: Props) {
+ style={styles.button}
+ hitSlop={HITSLOP}
+ accessibilityRole="button"
+ accessibilityLabel="Camera"
+ accessibilityHint="Opens camera on device">
)
}
+
+const styles = StyleSheet.create({
+ button: {
+ paddingHorizontal: 15,
+ },
+})
diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx
index 6e5477752d..6c7bdc9eeb 100644
--- a/src/view/com/composer/photos/SelectPhotoBtn.tsx
+++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx
@@ -1,5 +1,5 @@
import React, {useCallback} from 'react'
-import {TouchableOpacity} from 'react-native'
+import {TouchableOpacity, StyleSheet} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -36,8 +36,11 @@ export function SelectPhotoBtn({gallery}: Props) {
+ style={styles.button}
+ hitSlop={HITSLOP}
+ accessibilityRole="button"
+ accessibilityLabel="Gallery"
+ accessibilityHint="Opens device photo gallery">
)
}
+
+const styles = StyleSheet.create({
+ button: {
+ paddingHorizontal: 15,
+ },
+})
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx
index 10ac52b5d8..7b09da93d1 100644
--- a/src/view/com/composer/text-input/TextInput.tsx
+++ b/src/view/com/composer/text-input/TextInput.tsx
@@ -1,7 +1,14 @@
-import React, {forwardRef, useCallback, useEffect, useRef, useMemo} from 'react'
+import React, {
+ forwardRef,
+ useCallback,
+ useRef,
+ useMemo,
+ ComponentProps,
+} from 'react'
import {
NativeSyntheticEvent,
StyleSheet,
+ TextInput as RNTextInput,
TextInputSelectionChangeEventData,
View,
} from 'react-native'
@@ -27,14 +34,14 @@ export interface TextInputRef {
blur: () => void
}
-interface TextInputProps {
+interface TextInputProps extends ComponentProps {
richtext: RichText
placeholder: string
suggestedLinks: Set
autocompleteView: UserAutocompleteModel
- setRichText: (v: RichText) => void
+ setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void
- onPressPublish: (richtext: RichText) => Promise
+ onPressPublish: (richtext: RichText) => Promise
onSuggestedLinksChanged: (uris: Set) => void
onError: (err: string) => void
}
@@ -55,6 +62,7 @@ export const TextInput = forwardRef(
onPhotoPasted,
onSuggestedLinksChanged,
onError,
+ ...props
}: TextInputProps,
ref,
) => {
@@ -65,26 +73,11 @@ export const TextInput = forwardRef(
React.useImperativeHandle(ref, () => ({
focus: () => textInput.current?.focus(),
- blur: () => textInput.current?.blur(),
+ blur: () => {
+ textInput.current?.blur()
+ },
}))
- useEffect(() => {
- // HACK
- // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
- // -prf
- let to: NodeJS.Timeout | undefined
- if (textInput.current) {
- to = setTimeout(() => {
- textInput.current?.focus()
- }, 250)
- }
- return () => {
- if (to) {
- clearTimeout(to)
- }
- }
- }, [])
-
const onChangeText = useCallback(
async (newText: string) => {
const newRt = new RichText({text: newText})
@@ -206,8 +199,10 @@ export const TextInput = forwardRef(
placeholder={placeholder}
placeholderTextColor={pal.colors.textLight}
keyboardAppearance={theme.colorScheme}
+ autoFocus={true}
multiline
- style={[pal.text, styles.textInput, styles.textInputFormatting]}>
+ style={[pal.text, styles.textInput, styles.textInputFormatting]}
+ {...props}>
{textDecorated}
autocompleteView: UserAutocompleteModel
- setRichText: (v: RichText) => void
+ setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void
- onPressPublish: (richtext: RichText) => Promise
+ onPressPublish: (richtext: RichText) => Promise
onSuggestedLinksChanged: (uris: Set) => void
onError: (err: string) => void
}
@@ -70,6 +72,8 @@ export const TextInput = React.forwardRef(
placeholder,
}),
Text,
+ History,
+ Hardbreak,
],
editorProps: {
attributes: {
@@ -85,7 +89,7 @@ export const TextInput = React.forwardRef(
getImageFromUri(items, onPhotoPasted)
},
handleKeyDown: (_, event) => {
- if (event.metaKey && event.code === 'Enter') {
+ if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') {
// Workaround relying on previous state from `setRichText` to
// get the updated text content during editor initialization
setRichText((state: RichText) => {
@@ -136,6 +140,8 @@ function editorJsonToText(json: JSONContent): string {
}
}
text += '\n'
+ } else if (json.type === 'hardBreak') {
+ text += '\n'
} else if (json.type === 'text') {
text += json.text || ''
} else if (json.type === 'mention') {
diff --git a/src/view/com/composer/text-input/hooks/useGrapheme.tsx b/src/view/com/composer/text-input/hooks/useGrapheme.tsx
new file mode 100644
index 0000000000..25947c3ec7
--- /dev/null
+++ b/src/view/com/composer/text-input/hooks/useGrapheme.tsx
@@ -0,0 +1,36 @@
+import Graphemer from 'graphemer'
+import {useCallback, useMemo} from 'react'
+
+export const useGrapheme = () => {
+ const splitter = useMemo(() => new Graphemer(), [])
+
+ const getGraphemeString = useCallback(
+ (name: string, length: number) => {
+ let remainingCharacters = 0
+
+ if (name.length > length) {
+ const graphemes = splitter.splitGraphemes(name)
+
+ if (graphemes.length > length) {
+ remainingCharacters = 0
+ name = `${graphemes.slice(0, length).join('')}...`
+ } else {
+ remainingCharacters = length - graphemes.length
+ name = graphemes.join('')
+ }
+ } else {
+ remainingCharacters = length - name.length
+ }
+
+ return {
+ name,
+ remainingCharacters,
+ }
+ },
+ [splitter],
+ )
+
+ return {
+ getGraphemeString,
+ }
+}
diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
index 879bac0711..c9b8b84b18 100644
--- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
@@ -5,6 +5,8 @@ import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette'
import {Text} from 'view/com/util/text/Text'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {useGrapheme} from '../hooks/useGrapheme'
export const Autocomplete = observer(
({
@@ -16,6 +18,7 @@ export const Autocomplete = observer(
}) => {
const pal = usePalette('default')
const positionInterp = useAnimatedValue(0)
+ const {getGraphemeString} = useGrapheme()
useEffect(() => {
Animated.timing(positionInterp, {
@@ -35,56 +38,83 @@ export const Autocomplete = observer(
},
],
}
+
return (
-
-
- {view.suggestions.slice(0, 5).map(item => (
- onSelect(item.handle)}>
-
- {item.displayName || item.handle}
-
- @{item.handle}
-
+
+ {view.isActive ? (
+
+ {view.suggestions.length > 0 ? (
+ view.suggestions.slice(0, 5).map(item => {
+ // Eventually use an average length
+ const MAX_CHARS = 40
+ const MAX_HANDLE_CHARS = 20
+
+ // Using this approach because styling is not respecting
+ // bounding box wrapping (before converting to ellipsis)
+ const {name: displayHandle, remainingCharacters} =
+ getGraphemeString(item.handle, MAX_HANDLE_CHARS)
+
+ const {name: displayName} = getGraphemeString(
+ item.displayName ?? item.handle,
+ MAX_CHARS -
+ MAX_HANDLE_CHARS +
+ (remainingCharacters > 0 ? remainingCharacters : 0),
+ )
+
+ return (
+ onSelect(item.handle)}
+ accessibilityLabel={`Select ${item.handle}`}
+ accessibilityHint="">
+
+
+
+ {displayName}
+
+
+
+ @{displayHandle}
+
+
+ )
+ })
+ ) : (
+
+ No result
-
- ))}
-
-
+ )}
+
+ ) : null}
+
)
},
)
const styles = StyleSheet.create({
container: {
- display: 'none',
- height: 250,
- },
- animatedContainer: {
- display: 'none',
- position: 'absolute',
- left: -64,
- right: 0,
- top: 0,
+ marginLeft: -54,
+ top: 10,
borderTopWidth: 1,
},
- visible: {
- display: 'flex',
- },
item: {
borderBottomWidth: 1,
- paddingVertical: 16,
- paddingHorizontal: 16,
- height: 50,
+ paddingVertical: 12,
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: 6,
+ },
+ avatarAndHandle: {
+ display: 'flex',
+ flexDirection: 'row',
+ gap: 6,
+ alignItems: 'center',
+ },
+ noResults: {
+ paddingVertical: 12,
},
})
diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx
index 7c6f8770bc..87820b97bd 100644
--- a/src/view/com/composer/text-input/web/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/web/Autocomplete.tsx
@@ -4,6 +4,7 @@ import React, {
useImperativeHandle,
useState,
} from 'react'
+import {Pressable, StyleSheet, View} from 'react-native'
import {ReactRenderer} from '@tiptap/react'
import tippy, {Instance as TippyInstance} from 'tippy.js'
import {
@@ -12,6 +13,10 @@ import {
SuggestionKeyDownProps,
} from '@tiptap/suggestion'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
+import {usePalette} from 'lib/hooks/usePalette'
+import {Text} from 'view/com/util/text/Text'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {useGrapheme} from '../hooks/useGrapheme'
interface MentionListRef {
onKeyDown: (props: SuggestionKeyDownProps) => boolean
@@ -26,7 +31,7 @@ export function createSuggestion({
async items({query}) {
autocompleteView.setActive(true)
await autocompleteView.setPrefix(query)
- return autocompleteView.suggestions.slice(0, 8).map(s => s.handle)
+ return autocompleteView.suggestions.slice(0, 8)
},
render: () => {
@@ -91,12 +96,14 @@ export function createSuggestion({
const MentionList = forwardRef(
(props: SuggestionProps, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0)
+ const pal = usePalette('default')
+ const {getGraphemeString} = useGrapheme()
const selectItem = (index: number) => {
const item = props.items[index]
if (item) {
- props.command({id: item})
+ props.command({id: item.handle})
}
}
@@ -137,21 +144,92 @@ const MentionList = forwardRef(
},
}))
+ const {items} = props
+
return (
- {props.items.length ? (
- props.items.map((item, index) => (
-
selectItem(index)}>
- {item}
-
- ))
- ) : (
-
No result
- )}
+
+ {items.length > 0 ? (
+ items.map((item, index) => {
+ const {name: displayName} = getGraphemeString(
+ item.displayName ?? item.handle,
+ 30, // Heuristic value; can be modified
+ )
+ const isSelected = selectedIndex === index
+
+ return (
+ {
+ selectItem(index)
+ }}
+ accessibilityRole="button">
+
+
+
+ {displayName}
+
+
+
+ @{item.handle}
+
+
+ )
+ })
+ ) : (
+
+ No result
+
+ )}
+
)
},
)
+
+const styles = StyleSheet.create({
+ container: {
+ width: 500,
+ borderRadius: 6,
+ borderWidth: 1,
+ borderStyle: 'solid',
+ padding: 4,
+ },
+ mentionContainer: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ flexDirection: 'row',
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ gap: 4,
+ },
+ firstMention: {
+ borderTopLeftRadius: 2,
+ borderTopRightRadius: 2,
+ },
+ lastMention: {
+ borderBottomLeftRadius: 2,
+ borderBottomRightRadius: 2,
+ },
+ avatarAndDisplayName: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ },
+ noResult: {
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ },
+})
diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts
index 45c2dfd0db..91f4da0595 100644
--- a/src/view/com/composer/useExternalLinkFetch.ts
+++ b/src/view/com/composer/useExternalLinkFetch.ts
@@ -1,10 +1,11 @@
import {useState, useEffect} from 'react'
import {useStores} from 'state/index'
+import {ImageModel} from 'state/models/media/image'
import * as apilib from 'lib/api/index'
import {getLinkMeta} from 'lib/link-meta/link-meta'
-import {getPostAsQuote} from 'lib/link-meta/bsky'
+import {getPostAsQuote, getFeedAsEmbed} from 'lib/link-meta/bsky'
import {downloadAndResize} from 'lib/media/manip'
-import {isBskyPostUrl} from 'lib/strings/url-helpers'
+import {isBskyPostUrl, isBskyCustomFeedUrl} from 'lib/strings/url-helpers'
import {ComposerOpts} from 'state/models/ui/shell'
import {POST_IMG_MAX} from 'lib/constants'
@@ -41,6 +42,24 @@ export function useExternalLinkFetch({
setExtLink(undefined)
},
)
+ } else if (isBskyCustomFeedUrl(extLink.uri)) {
+ getFeedAsEmbed(store, extLink.uri).then(
+ ({embed, meta}) => {
+ if (aborted) {
+ return
+ }
+ setExtLink({
+ uri: extLink.uri,
+ isLoading: false,
+ meta,
+ embed,
+ })
+ },
+ err => {
+ store.log.error('Failed to fetch feed for embedding', {err})
+ setExtLink(undefined)
+ },
+ )
} else {
getLinkMeta(store, extLink.uri).then(meta => {
if (aborted) {
@@ -72,7 +91,9 @@ export function useExternalLinkFetch({
setExtLink({
...extLink,
isLoading: false, // done
- localThumb,
+ localThumb: localThumb
+ ? new ImageModel(store, localThumb)
+ : undefined,
})
})
return cleanup
diff --git a/src/view/com/discover/SuggestedPosts.tsx b/src/view/com/discover/SuggestedPosts.tsx
deleted file mode 100644
index 6d2f39636e..0000000000
--- a/src/view/com/discover/SuggestedPosts.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React from 'react'
-import {ActivityIndicator, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
-import {SuggestedPostsModel} from 'state/models/discovery/suggested-posts'
-import {s} from 'lib/styles'
-import {FeedItem as Post} from '../posts/FeedItem'
-import {Text} from '../util/text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-
-export const SuggestedPosts = observer(() => {
- const pal = usePalette('default')
- const store = useStores()
- const suggestedPostsView = React.useMemo(
- () => new SuggestedPostsModel(store),
- [store],
- )
-
- React.useEffect(() => {
- if (!suggestedPostsView.hasLoaded) {
- suggestedPostsView.setup()
- }
- }, [store, suggestedPostsView])
-
- return (
- <>
- {(suggestedPostsView.hasContent || suggestedPostsView.isLoading) && (
-
- Recently, on Bluesky...
-
- )}
- {suggestedPostsView.hasContent && (
- <>
-
- {suggestedPostsView.posts.map(item => (
-
- ))}
-
- >
- )}
- {suggestedPostsView.isLoading && (
-
-
-
- )}
- >
- )
-})
-
-const styles = StyleSheet.create({
- heading: {
- fontWeight: 'bold',
- paddingHorizontal: 12,
- paddingTop: 16,
- paddingBottom: 8,
- },
-
- bottomBorder: {
- borderBottomWidth: 1,
- },
-
- loadMore: {
- paddingLeft: 12,
- paddingVertical: 10,
- },
-})
diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx
new file mode 100644
index 0000000000..ef8de8b856
--- /dev/null
+++ b/src/view/com/feeds/CustomFeed.tsx
@@ -0,0 +1,162 @@
+import React from 'react'
+import {
+ Pressable,
+ StyleProp,
+ StyleSheet,
+ View,
+ ViewStyle,
+ TouchableOpacity,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {Text} from '../util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+import {UserAvatar} from '../util/UserAvatar'
+import {observer} from 'mobx-react-lite'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {useNavigation} from '@react-navigation/native'
+import {NavigationProp} from 'lib/routes/types'
+import {useStores} from 'state/index'
+import {pluralize} from 'lib/strings/helpers'
+import {AtUri} from '@atproto/api'
+import * as Toast from 'view/com/util/Toast'
+
+export const CustomFeed = observer(
+ ({
+ item,
+ style,
+ showSaveBtn = false,
+ showDescription = false,
+ showLikes = false,
+ }: {
+ item: CustomFeedModel
+ style?: StyleProp
+ showSaveBtn?: boolean
+ showDescription?: boolean
+ showLikes?: boolean
+ }) => {
+ const store = useStores()
+ const pal = usePalette('default')
+ const navigation = useNavigation()
+
+ const onToggleSaved = React.useCallback(async () => {
+ if (item.isSaved) {
+ store.shell.openModal({
+ name: 'confirm',
+ title: 'Remove from my feeds',
+ message: `Remove ${item.displayName} from my feeds?`,
+ onPressConfirm: async () => {
+ try {
+ await store.me.savedFeeds.unsave(item)
+ Toast.show('Removed from my feeds')
+ } catch (e) {
+ Toast.show('There was an issue contacting your server')
+ store.log.error('Failed to unsave feed', {e})
+ }
+ },
+ })
+ } else {
+ try {
+ await store.me.savedFeeds.save(item)
+ Toast.show('Added to my feeds')
+ } catch (e) {
+ Toast.show('There was an issue contacting your server')
+ store.log.error('Failed to save feed', {e})
+ }
+ }
+ }, [store, item])
+
+ return (
+ {
+ navigation.push('CustomFeed', {
+ name: item.data.creator.did,
+ rkey: new AtUri(item.data.uri).rkey,
+ })
+ }}
+ key={item.data.uri}>
+
+
+
+
+
+
+ {item.displayName}
+
+
+ by @{item.data.creator.handle}
+
+
+ {showSaveBtn && (
+
+
+ {item.isSaved ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+
+ {showDescription && item.data.description ? (
+
+ {item.data.description}
+
+ ) : null}
+
+ {showLikes ? (
+
+ Liked by {item.data.likeCount || 0}{' '}
+ {pluralize(item.data.likeCount || 0, 'user')}
+
+ ) : null}
+
+ )
+ },
+)
+
+const styles = StyleSheet.create({
+ container: {
+ paddingHorizontal: 18,
+ paddingVertical: 20,
+ flexDirection: 'column',
+ flex: 1,
+ borderTopWidth: 1,
+ gap: 14,
+ },
+ headerContainer: {
+ flexDirection: 'row',
+ },
+ headerTextContainer: {
+ flexDirection: 'column',
+ columnGap: 4,
+ flex: 1,
+ },
+ description: {
+ flex: 1,
+ flexWrap: 'wrap',
+ },
+ btn: {
+ paddingVertical: 6,
+ },
+})
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
index 6880008e4a..84e5f90fb4 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
@@ -20,7 +20,11 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => (
+ hitSlop={HIT_SLOP}
+ accessibilityRole="button"
+ accessibilityLabel="Close image"
+ accessibilityHint="Closes viewer for header image"
+ onAccessibilityEscape={onRequestClose}>
✕
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
index 12d37e283a..658735724b 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
@@ -127,7 +127,8 @@ const ImageItem = ({
+ delayLongPress={delayLongPress}
+ accessibilityRole="image">
+
diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx
index d6cc8c254f..18440c55d6 100644
--- a/src/view/com/lightbox/Lightbox.tsx
+++ b/src/view/com/lightbox/Lightbox.tsx
@@ -1,32 +1,75 @@
import React from 'react'
-import {View} from 'react-native'
+import {Pressable, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import ImageView from './ImageViewing'
import {useStores} from 'state/index'
import * as models from 'state/models/ui/shell'
import {saveImageModal} from 'lib/media/manip'
-import {ImageSource} from './ImageViewing/@types'
+import {Text} from '../util/text/Text'
+import {s, colors} from 'lib/styles'
+import {Button} from '../util/forms/Button'
+import {isIOS} from 'platform/detection'
export const Lightbox = observer(function Lightbox() {
const store = useStores()
- if (!store.shell.isLightboxActive) {
- return null
- }
+ const [isAltExpanded, setAltExpanded] = React.useState(false)
- const onClose = () => {
+ const onClose = React.useCallback(() => {
store.shell.closeLightbox()
- }
- const onLongPress = (image: ImageSource) => {
- if (
- typeof image === 'object' &&
- 'uri' in image &&
- typeof image.uri === 'string'
- ) {
- saveImageModal({uri: image.uri})
- }
- }
+ }, [store])
- if (store.shell.activeLightbox?.name === 'profile-image') {
+ const LightboxFooter = React.useCallback(
+ ({imageIndex}: {imageIndex: number}) => {
+ const lightbox = store.shell.activeLightbox
+ if (!lightbox) {
+ return null
+ }
+
+ let altText = ''
+ let uri = ''
+ if (lightbox.name === 'images') {
+ const opts = lightbox as models.ImagesLightbox
+ uri = opts.images[imageIndex].uri
+ altText = opts.images[imageIndex].alt || ''
+ } else if (lightbox.name === 'profile-image') {
+ const opts = lightbox as models.ProfileImageLightbox
+ uri = opts.profileView.avatar || ''
+ }
+
+ return (
+
+ {altText ? (
+ setAltExpanded(!isAltExpanded)}
+ accessibilityRole="button">
+
+ {altText}
+
+
+ ) : null}
+
+ saveImageModal({uri})}>
+
+
+ Share
+
+
+
+
+ )
+ },
+ [store.shell.activeLightbox, isAltExpanded, setAltExpanded],
+ )
+
+ if (!store.shell.activeLightbox) {
+ return null
+ } else if (store.shell.activeLightbox.name === 'profile-image') {
const opts = store.shell.activeLightbox as models.ProfileImageLightbox
return (
)
- } else if (store.shell.activeLightbox?.name === 'images') {
+ } else if (store.shell.activeLightbox.name === 'images') {
const opts = store.shell.activeLightbox as models.ImagesLightbox
return (
({uri}))}
+ images={opts.images.map(({uri}) => ({uri}))}
imageIndex={opts.index}
visible
onRequestClose={onClose}
- onLongPress={onLongPress}
+ FooterComponent={LightboxFooter}
/>
)
} else {
- return
+ return null
}
})
+
+const styles = StyleSheet.create({
+ footer: {
+ paddingTop: 16,
+ paddingBottom: isIOS ? 40 : 24,
+ paddingHorizontal: 24,
+ backgroundColor: '#000d',
+ },
+ footerText: {
+ paddingBottom: isIOS ? 20 : 16,
+ },
+ footerBtns: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+ footerBtn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ backgroundColor: 'transparent',
+ borderColor: colors.white,
+ },
+})
diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx
index f10548351e..f6aa26a3b8 100644
--- a/src/view/com/lightbox/Lightbox.web.tsx
+++ b/src/view/com/lightbox/Lightbox.web.tsx
@@ -1,24 +1,30 @@
-import React from 'react'
+import React, {useCallback, useEffect, useState} from 'react'
import {
Image,
TouchableOpacity,
TouchableWithoutFeedback,
StyleSheet,
View,
+ Pressable,
} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useStores} from 'state/index'
import * as models from 'state/models/ui/shell'
-import {colors} from 'lib/styles'
+import {colors, s} from 'lib/styles'
import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
+import {Text} from '../util/text/Text'
interface Img {
uri: string
+ alt?: string
}
export const Lightbox = observer(function Lightbox() {
const store = useStores()
+
+ const onClose = useCallback(() => store.shell.closeLightbox(), [store.shell])
+
if (!store.shell.isLightboxActive) {
return null
}
@@ -27,8 +33,6 @@ export const Lightbox = observer(function Lightbox() {
const initialIndex =
activeLightbox instanceof models.ImagesLightbox ? activeLightbox.index : 0
- const onClose = () => store.shell.closeLightbox()
-
let imgs: Img[] | undefined
if (activeLightbox instanceof models.ProfileImageLightbox) {
const opts = activeLightbox
@@ -37,7 +41,7 @@ export const Lightbox = observer(function Lightbox() {
}
} else if (activeLightbox instanceof models.ImagesLightbox) {
const opts = activeLightbox
- imgs = opts.uris.map(uri => ({uri}))
+ imgs = opts.images
}
if (!imgs) {
@@ -58,30 +62,61 @@ function LightboxInner({
initialIndex: number
onClose: () => void
}) {
- const [index, setIndex] = React.useState(initialIndex)
+ const [index, setIndex] = useState(initialIndex)
+ const [isAltExpanded, setAltExpanded] = useState(false)
const canGoLeft = index >= 1
const canGoRight = index < imgs.length - 1
- const onPressLeft = () => {
+ const onPressLeft = useCallback(() => {
if (canGoLeft) {
setIndex(index - 1)
}
- }
- const onPressRight = () => {
+ }, [index, canGoLeft])
+ const onPressRight = useCallback(() => {
if (canGoRight) {
setIndex(index + 1)
}
- }
+ }, [index, canGoRight])
+
+ const onKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ onClose()
+ } else if (e.key === 'ArrowLeft') {
+ onPressLeft()
+ } else if (e.key === 'ArrowRight') {
+ onPressRight()
+ }
+ },
+ [onClose, onPressLeft, onPressRight],
+ )
+
+ useEffect(() => {
+ window.addEventListener('keydown', onKeyDown)
+ return () => window.removeEventListener('keydown', onKeyDown)
+ }, [onKeyDown])
return (
-
+
-
+
{canGoLeft && (
+ style={[styles.btn, styles.leftBtn]}
+ accessibilityRole="button"
+ accessibilityLabel="Previous image"
+ accessibilityHint="">
+ style={[styles.btn, styles.rightBtn]}
+ accessibilityRole="button"
+ accessibilityLabel="Next image"
+ accessibilityHint="">
+ {imgs[index].alt ? (
+
+ {
+ setAltExpanded(!isAltExpanded)
+ }}>
+
+ {imgs[index].alt}
+
+
+
+ ) : null}
@@ -154,4 +210,9 @@ const styles = StyleSheet.create({
right: 30,
top: '50%',
},
+ footer: {
+ paddingHorizontal: 32,
+ paddingVertical: 24,
+ backgroundColor: colors.black,
+ },
})
diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx
new file mode 100644
index 0000000000..0e13ca3335
--- /dev/null
+++ b/src/view/com/lists/ListCard.tsx
@@ -0,0 +1,155 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api'
+import {Link} from '../util/Link'
+import {Text} from '../util/text/Text'
+import {RichText as RichTextCom} from '../util/text/RichText'
+import {UserAvatar} from '../util/UserAvatar'
+import {s} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useStores} from 'state/index'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+
+export const ListCard = ({
+ testID,
+ list,
+ noBg,
+ noBorder,
+ renderButton,
+}: {
+ testID?: string
+ list: AppBskyGraphDefs.ListView
+ noBg?: boolean
+ noBorder?: boolean
+ renderButton?: () => JSX.Element
+}) => {
+ const pal = usePalette('default')
+ const store = useStores()
+
+ const rkey = React.useMemo(() => {
+ try {
+ const urip = new AtUri(list.uri)
+ return urip.rkey
+ } catch {
+ return ''
+ }
+ }, [list])
+
+ const descriptionRichText = React.useMemo(() => {
+ if (list.description) {
+ return new RichText({
+ text: list.description,
+ facets: list.descriptionFacets,
+ })
+ }
+ return undefined
+ }, [list])
+
+ return (
+
+
+
+
+
+
+
+ {sanitizeDisplayName(list.name)}
+
+
+ {list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'} by{' '}
+ {list.creator.did === store.me.did
+ ? 'you'
+ : `@${list.creator.handle}`}
+
+ {!!list.viewer?.muted && (
+
+
+
+ Subscribed
+
+
+
+ )}
+
+ {renderButton ? (
+ {renderButton()}
+ ) : undefined}
+
+ {descriptionRichText ? (
+
+
+
+ ) : undefined}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ outer: {
+ borderTopWidth: 1,
+ paddingHorizontal: 6,
+ },
+ outerNoBorder: {
+ borderTopWidth: 0,
+ },
+ layout: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ layoutAvi: {
+ width: 54,
+ paddingLeft: 4,
+ paddingTop: 8,
+ paddingBottom: 10,
+ },
+ avi: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ resizeMode: 'cover',
+ },
+ layoutContent: {
+ flex: 1,
+ paddingRight: 10,
+ paddingTop: 10,
+ paddingBottom: 10,
+ },
+ layoutButton: {
+ paddingRight: 10,
+ },
+ details: {
+ paddingLeft: 54,
+ paddingRight: 10,
+ paddingBottom: 10,
+ },
+ pill: {
+ borderRadius: 4,
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ },
+ btn: {
+ paddingVertical: 7,
+ borderRadius: 50,
+ marginLeft: 6,
+ paddingHorizontal: 14,
+ },
+})
diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx
new file mode 100644
index 0000000000..914f446a14
--- /dev/null
+++ b/src/view/com/lists/ListItems.tsx
@@ -0,0 +1,387 @@
+import React, {MutableRefObject} from 'react'
+import {
+ ActivityIndicator,
+ RefreshControl,
+ StyleProp,
+ StyleSheet,
+ View,
+ ViewStyle,
+} from 'react-native'
+import {AppBskyActorDefs, AppBskyGraphDefs, RichText} from '@atproto/api'
+import {observer} from 'mobx-react-lite'
+import {FlatList} from '../util/Views'
+import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
+import {ProfileCard} from '../profile/ProfileCard'
+import {Button} from '../util/forms/Button'
+import {Text} from '../util/text/Text'
+import {RichText as RichTextCom} from '../util/text/RichText'
+import {UserAvatar} from '../util/UserAvatar'
+import {TextLink} from '../util/Link'
+import {ListModel} from 'state/models/content/list'
+import {useAnalytics} from 'lib/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useStores} from 'state/index'
+import {s} from 'lib/styles'
+import {isDesktopWeb} from 'platform/detection'
+
+const LOADING_ITEM = {_reactKey: '__loading__'}
+const HEADER_ITEM = {_reactKey: '__header__'}
+const EMPTY_ITEM = {_reactKey: '__empty__'}
+const ERROR_ITEM = {_reactKey: '__error__'}
+const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
+
+export const ListItems = observer(
+ ({
+ list,
+ style,
+ scrollElRef,
+ onPressTryAgain,
+ onToggleSubscribed,
+ onPressEditList,
+ onPressDeleteList,
+ renderEmptyState,
+ testID,
+ headerOffset = 0,
+ }: {
+ list: ListModel
+ style?: StyleProp
+ scrollElRef?: MutableRefObject | null>
+ onPressTryAgain?: () => void
+ onToggleSubscribed?: () => void
+ onPressEditList?: () => void
+ onPressDeleteList?: () => void
+ renderEmptyState?: () => JSX.Element
+ testID?: string
+ headerOffset?: number
+ }) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const {track} = useAnalytics()
+ const [isRefreshing, setIsRefreshing] = React.useState(false)
+
+ const data = React.useMemo(() => {
+ let items: any[] = [HEADER_ITEM]
+ if (list.hasLoaded) {
+ if (list.hasError) {
+ items = items.concat([ERROR_ITEM])
+ }
+ if (list.isEmpty) {
+ items = items.concat([EMPTY_ITEM])
+ } else {
+ items = items.concat(list.items)
+ }
+ if (list.loadMoreError) {
+ items = items.concat([LOAD_MORE_ERROR_ITEM])
+ }
+ } else if (list.isLoading) {
+ items = items.concat([LOADING_ITEM])
+ }
+ return items
+ }, [
+ list.hasError,
+ list.hasLoaded,
+ list.isLoading,
+ list.isEmpty,
+ list.items,
+ list.loadMoreError,
+ ])
+
+ // events
+ // =
+
+ const onRefresh = React.useCallback(async () => {
+ track('Lists:onRefresh')
+ setIsRefreshing(true)
+ try {
+ await list.refresh()
+ } catch (err) {
+ list.rootStore.log.error('Failed to refresh lists', err)
+ }
+ setIsRefreshing(false)
+ }, [list, track, setIsRefreshing])
+
+ const onEndReached = React.useCallback(async () => {
+ track('Lists:onEndReached')
+ try {
+ await list.loadMore()
+ } catch (err) {
+ list.rootStore.log.error('Failed to load more lists', err)
+ }
+ }, [list, track])
+
+ const onPressRetryLoadMore = React.useCallback(() => {
+ list.retryLoadMore()
+ }, [list])
+
+ const onPressEditMembership = React.useCallback(
+ (profile: AppBskyActorDefs.ProfileViewBasic) => {
+ store.shell.openModal({
+ name: 'list-add-remove-user',
+ subject: profile.did,
+ displayName: profile.displayName || profile.handle,
+ onUpdate() {
+ list.refresh()
+ },
+ })
+ },
+ [store, list],
+ )
+
+ // rendering
+ // =
+
+ const renderMemberButton = React.useCallback(
+ (profile: AppBskyActorDefs.ProfileViewBasic) => {
+ if (!list.isOwner) {
+ return null
+ }
+ return (
+ onPressEditMembership(profile)}
+ />
+ )
+ },
+ [list, onPressEditMembership],
+ )
+
+ const renderItem = React.useCallback(
+ ({item}: {item: any}) => {
+ if (item === EMPTY_ITEM) {
+ if (renderEmptyState) {
+ return renderEmptyState()
+ }
+ return
+ } else if (item === HEADER_ITEM) {
+ return list.list ? (
+
+ ) : null
+ } else if (item === ERROR_ITEM) {
+ return (
+
+ )
+ } else if (item === LOAD_MORE_ERROR_ITEM) {
+ return (
+
+ )
+ } else if (item === LOADING_ITEM) {
+ return
+ }
+ return (
+
+ )
+ },
+ [
+ list,
+ onPressTryAgain,
+ onPressRetryLoadMore,
+ renderMemberButton,
+ onPressEditList,
+ onPressDeleteList,
+ onToggleSubscribed,
+ renderEmptyState,
+ ],
+ )
+
+ const Footer = React.useCallback(
+ () =>
+ list.isLoading ? (
+
+
+
+ ) : (
+
+ ),
+ [list],
+ )
+
+ return (
+
+ {data.length > 0 && (
+ item._reactKey}
+ renderItem={renderItem}
+ ListFooterComponent={Footer}
+ refreshControl={
+
+ }
+ contentContainerStyle={s.contentContainer}
+ style={{paddingTop: headerOffset}}
+ onEndReached={onEndReached}
+ onEndReachedThreshold={0.6}
+ removeClippedSubviews={true}
+ contentOffset={{x: 0, y: headerOffset * -1}}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
+ />
+ )}
+
+ )
+ },
+)
+
+const ListHeader = observer(
+ ({
+ list,
+ isOwner,
+ onToggleSubscribed,
+ onPressEditList,
+ onPressDeleteList,
+ }: {
+ list: AppBskyGraphDefs.ListView
+ isOwner: boolean
+ onToggleSubscribed?: () => void
+ onPressEditList?: () => void
+ onPressDeleteList?: () => void
+ }) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const descriptionRT = React.useMemo(
+ () =>
+ list?.description &&
+ new RichText({text: list.description, facets: list.descriptionFacets}),
+ [list],
+ )
+ return (
+ <>
+
+
+
+ {list.name}
+
+ {list && (
+
+ {list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list '}
+ by{' '}
+ {list.creator.did === store.me.did ? (
+ 'you'
+ ) : (
+
+ )}
+
+ )}
+ {descriptionRT && (
+
+ )}
+ {isDesktopWeb && (
+
+ {list.viewer?.muted ? (
+
+ ) : (
+
+ )}
+ {isOwner && (
+
+ )}
+ {isOwner && (
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+ Muted users
+
+
+
+ >
+ )
+ },
+)
+
+const styles = StyleSheet.create({
+ header: {
+ flexDirection: 'row',
+ gap: 12,
+ paddingHorizontal: 16,
+ paddingTop: 12,
+ paddingBottom: 16,
+ borderTopWidth: 1,
+ },
+ headerDescription: {
+ marginTop: 8,
+ },
+ headerBtns: {
+ flexDirection: 'row',
+ gap: 8,
+ marginTop: 12,
+ },
+ fakeSelector: {
+ flexDirection: 'row',
+ paddingHorizontal: isDesktopWeb ? 16 : 6,
+ },
+ fakeSelectorItem: {
+ paddingHorizontal: 12,
+ paddingBottom: 8,
+ borderBottomWidth: 3,
+ },
+ feedFooter: {paddingTop: 20},
+})
diff --git a/src/view/com/lists/ListsList.tsx b/src/view/com/lists/ListsList.tsx
new file mode 100644
index 0000000000..88b71acc0d
--- /dev/null
+++ b/src/view/com/lists/ListsList.tsx
@@ -0,0 +1,240 @@
+import React, {MutableRefObject} from 'react'
+import {
+ ActivityIndicator,
+ RefreshControl,
+ StyleProp,
+ StyleSheet,
+ View,
+ ViewStyle,
+} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
+import {FlatList} from '../util/Views'
+import {ListCard} from './ListCard'
+import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
+import {Button} from '../util/forms/Button'
+import {Text} from '../util/text/Text'
+import {ListsListModel} from 'state/models/lists/lists-list'
+import {useAnalytics} from 'lib/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+
+const LOADING_ITEM = {_reactKey: '__loading__'}
+const CREATENEW_ITEM = {_reactKey: '__loading__'}
+const EMPTY_ITEM = {_reactKey: '__empty__'}
+const ERROR_ITEM = {_reactKey: '__error__'}
+const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
+
+export const ListsList = observer(
+ ({
+ listsList,
+ showAddBtns,
+ style,
+ scrollElRef,
+ onPressTryAgain,
+ onPressCreateNew,
+ renderItem,
+ renderEmptyState,
+ testID,
+ headerOffset = 0,
+ }: {
+ listsList: ListsListModel
+ showAddBtns?: boolean
+ style?: StyleProp
+ scrollElRef?: MutableRefObject | null>
+ onPressCreateNew: () => void
+ onPressTryAgain?: () => void
+ renderItem?: (list: GraphDefs.ListView) => JSX.Element
+ renderEmptyState?: () => JSX.Element
+ testID?: string
+ headerOffset?: number
+ }) => {
+ const pal = usePalette('default')
+ const {track} = useAnalytics()
+ const [isRefreshing, setIsRefreshing] = React.useState(false)
+
+ const data = React.useMemo(() => {
+ let items: any[] = []
+ if (listsList.hasLoaded) {
+ if (listsList.hasError) {
+ items = items.concat([ERROR_ITEM])
+ }
+ if (listsList.isEmpty) {
+ items = items.concat([EMPTY_ITEM])
+ } else {
+ if (showAddBtns) {
+ items = items.concat([CREATENEW_ITEM])
+ }
+ items = items.concat(listsList.lists)
+ }
+ if (listsList.loadMoreError) {
+ items = items.concat([LOAD_MORE_ERROR_ITEM])
+ }
+ } else if (listsList.isLoading) {
+ items = items.concat([LOADING_ITEM])
+ }
+ return items
+ }, [
+ listsList.hasError,
+ listsList.hasLoaded,
+ listsList.isLoading,
+ listsList.isEmpty,
+ listsList.lists,
+ listsList.loadMoreError,
+ showAddBtns,
+ ])
+
+ // events
+ // =
+
+ const onRefresh = React.useCallback(async () => {
+ track('Lists:onRefresh')
+ setIsRefreshing(true)
+ try {
+ await listsList.refresh()
+ } catch (err) {
+ listsList.rootStore.log.error('Failed to refresh lists', err)
+ }
+ setIsRefreshing(false)
+ }, [listsList, track, setIsRefreshing])
+
+ const onEndReached = React.useCallback(async () => {
+ track('Lists:onEndReached')
+ try {
+ await listsList.loadMore()
+ } catch (err) {
+ listsList.rootStore.log.error('Failed to load more lists', err)
+ }
+ }, [listsList, track])
+
+ const onPressRetryLoadMore = React.useCallback(() => {
+ listsList.retryLoadMore()
+ }, [listsList])
+
+ // rendering
+ // =
+
+ const renderItemInner = React.useCallback(
+ ({item}: {item: any}) => {
+ if (item === EMPTY_ITEM) {
+ if (renderEmptyState) {
+ return renderEmptyState()
+ }
+ return
+ } else if (item === CREATENEW_ITEM) {
+ return
+ } else if (item === ERROR_ITEM) {
+ return (
+
+ )
+ } else if (item === LOAD_MORE_ERROR_ITEM) {
+ return (
+
+ )
+ } else if (item === LOADING_ITEM) {
+ return
+ }
+ return renderItem ? (
+ renderItem(item)
+ ) : (
+
+ )
+ },
+ [
+ listsList,
+ onPressTryAgain,
+ onPressRetryLoadMore,
+ onPressCreateNew,
+ renderItem,
+ renderEmptyState,
+ ],
+ )
+
+ const Footer = React.useCallback(
+ () =>
+ listsList.isLoading ? (
+
+
+
+ ) : (
+
+ ),
+ [listsList],
+ )
+
+ return (
+
+ {data.length > 0 && (
+ item._reactKey}
+ renderItem={renderItemInner}
+ ListFooterComponent={Footer}
+ refreshControl={
+
+ }
+ contentContainerStyle={s.contentContainer}
+ style={{paddingTop: headerOffset}}
+ onEndReached={onEndReached}
+ onEndReachedThreshold={0.6}
+ removeClippedSubviews={true}
+ contentOffset={{x: 0, y: headerOffset * -1}}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
+ />
+ )}
+
+ )
+ },
+)
+
+function CreateNewItem({onPress}: {onPress: () => void}) {
+ const pal = usePalette('default')
+
+ return (
+
+
+
+
+ New Mute List
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ createNewContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 18,
+ paddingTop: 18,
+ paddingBottom: 16,
+ },
+ createNewButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ feedFooter: {paddingTop: 20},
+})
diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx
new file mode 100644
index 0000000000..6117924ae1
--- /dev/null
+++ b/src/view/com/modals/AddAppPasswords.tsx
@@ -0,0 +1,260 @@
+import React, {useState} from 'react'
+import {StyleSheet, TextInput, View, TouchableOpacity} from 'react-native'
+import {Text} from '../util/text/Text'
+import {Button} from '../util/forms/Button'
+import {s} from 'lib/styles'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb} from 'platform/detection'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import Clipboard from '@react-native-clipboard/clipboard'
+import * as Toast from '../util/Toast'
+
+export const snapPoints = ['70%']
+
+const shadesOfBlue: string[] = [
+ 'AliceBlue',
+ 'Aqua',
+ 'Aquamarine',
+ 'Azure',
+ 'BabyBlue',
+ 'Blue',
+ 'BlueViolet',
+ 'CadetBlue',
+ 'CornflowerBlue',
+ 'Cyan',
+ 'DarkBlue',
+ 'DarkCyan',
+ 'DarkSlateBlue',
+ 'DeepSkyBlue',
+ 'DodgerBlue',
+ 'ElectricBlue',
+ 'LightBlue',
+ 'LightCyan',
+ 'LightSkyBlue',
+ 'LightSteelBlue',
+ 'MediumAquaMarine',
+ 'MediumBlue',
+ 'MediumSlateBlue',
+ 'MidnightBlue',
+ 'Navy',
+ 'PowderBlue',
+ 'RoyalBlue',
+ 'SkyBlue',
+ 'SlateBlue',
+ 'SteelBlue',
+ 'Teal',
+ 'Turquoise',
+]
+
+export function Component({}: {}) {
+ const pal = usePalette('default')
+ const store = useStores()
+ const [name, setName] = useState(
+ shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
+ )
+ const [appPassword, setAppPassword] = useState()
+ const [wasCopied, setWasCopied] = useState(false)
+
+ const onCopy = React.useCallback(() => {
+ if (appPassword) {
+ Clipboard.setString(appPassword)
+ Toast.show('Copied to clipboard')
+ setWasCopied(true)
+ }
+ }, [appPassword])
+
+ const onDone = React.useCallback(() => {
+ store.shell.closeModal()
+ }, [store])
+
+ const createAppPassword = async () => {
+ // if name is all whitespace, we don't allow it
+ if (!name || !name.trim()) {
+ Toast.show(
+ 'Please enter a name for your app password. All spaces is not allowed.',
+ )
+ return
+ }
+ // if name is too short (under 4 chars), we don't allow it
+ if (name.length < 4) {
+ Toast.show('App Password names must be at least 4 characters long.')
+ return
+ }
+
+ try {
+ const newPassword = await store.me.createAppPassword(name)
+ if (newPassword) {
+ setAppPassword(newPassword.password)
+ } else {
+ Toast.show('Failed to create app password.')
+ // TODO: better error handling (?)
+ }
+ } catch (e) {
+ Toast.show('Failed to create app password.')
+ store.log.error('Failed to create app password', {e})
+ }
+ }
+
+ const _onChangeText = (text: string) => {
+ // sanitize input
+ // we only all alphanumeric characters, spaces, dashes, and underscores
+ // if the user enters anything else, we ignore it and shake the input container
+ // also, it cannot start with a space
+ if (text.match(/^[a-zA-Z0-9-_ ]*$/)) {
+ setName(text)
+ } else {
+ Toast.show(
+ 'App Password names can only contain letters, numbers, spaces, dashes, and underscores.',
+ )
+ }
+ }
+
+ return (
+
+
+ {!appPassword ? (
+
+ Please enter a unique name for this App Password or use our randomly
+ generated one.
+
+ ) : (
+
+
+ Here is your app password.
+ {' '}
+ Use this to sign into the other app along with your handle.
+
+ )}
+ {!appPassword ? (
+
+
+
+ ) : (
+
+
+ {appPassword}
+
+ {wasCopied ? (
+ Copied
+ ) : (
+
+ )}
+
+ )}
+
+ {appPassword ? (
+
+ For security reasons, you won't be able to view this again. If you
+ lose this password, you'll need to generate a new one.
+
+ ) : (
+
+ Can only contain letters, numbers, spaces, dashes, and underscores.
+ Must be at least 4 characters long, but no more than 32 characters
+ long.
+
+ )}
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingBottom: isDesktopWeb ? 0 : 50,
+ paddingHorizontal: 16,
+ },
+ textInputWrapper: {
+ borderRadius: 8,
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 16,
+ marginBottom: 8,
+ },
+ input: {
+ flex: 1,
+ width: '100%',
+ paddingVertical: 10,
+ paddingHorizontal: 8,
+ marginTop: 6,
+ fontSize: 17,
+ letterSpacing: 0.25,
+ fontWeight: '400',
+ borderRadius: 10,
+ },
+ passwordContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ alignItems: 'center',
+ borderRadius: 10,
+ marginTop: 16,
+ marginBottom: 12,
+ },
+ btnContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginTop: 12,
+ },
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 32,
+ paddingHorizontal: 60,
+ paddingVertical: 14,
+ },
+ btnLabel: {
+ fontSize: 18,
+ },
+ groupContent: {
+ borderTopWidth: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+})
diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx
new file mode 100644
index 0000000000..07270d5574
--- /dev/null
+++ b/src/view/com/modals/AltImage.tsx
@@ -0,0 +1,120 @@
+import React, {useCallback, useState} from 'react'
+import {StyleSheet, TextInput, TouchableOpacity, View} from 'react-native'
+import {usePalette} from 'lib/hooks/usePalette'
+import {gradients, s} from 'lib/styles'
+import {enforceLen} from 'lib/strings/helpers'
+import {MAX_ALT_TEXT} from 'lib/constants'
+import {useTheme} from 'lib/ThemeContext'
+import {Text} from '../util/text/Text'
+import LinearGradient from 'react-native-linear-gradient'
+import {useStores} from 'state/index'
+import {isDesktopWeb} from 'platform/detection'
+import {ImageModel} from 'state/models/media/image'
+
+export const snapPoints = ['fullscreen']
+
+interface Props {
+ image: ImageModel
+}
+
+export function Component({image}: Props) {
+ const pal = usePalette('default')
+ const store = useStores()
+ const theme = useTheme()
+ const [altText, setAltText] = useState(image.altText)
+
+ const onPressSave = useCallback(() => {
+ image.setAltText(altText)
+ store.shell.closeModal()
+ }, [store, image, altText])
+
+ const onPressCancel = () => {
+ store.shell.closeModal()
+ }
+
+ return (
+
+ Add alt text
+ setAltText(enforceLen(text, MAX_ALT_TEXT))}
+ accessibilityLabel="Image alt text"
+ accessibilityHint="Sets image alt text for screenreaders"
+ accessibilityLabelledBy="imageAltText"
+ />
+
+
+
+
+ Save
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ gap: 18,
+ paddingVertical: isDesktopWeb ? 0 : 18,
+ paddingHorizontal: isDesktopWeb ? 0 : 12,
+ height: '100%',
+ width: '100%',
+ },
+ title: {
+ textAlign: 'center',
+ fontWeight: 'bold',
+ fontSize: 24,
+ },
+ textArea: {
+ borderWidth: 1,
+ borderRadius: 6,
+ paddingTop: 10,
+ paddingHorizontal: 12,
+ fontSize: 16,
+ height: 100,
+ textAlignVertical: 'top',
+ },
+ button: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ borderRadius: 32,
+ padding: 10,
+ },
+ buttonControls: {
+ gap: 8,
+ },
+})
diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx
index a0c74b0dcd..961efc08c6 100644
--- a/src/view/com/modals/ChangeHandle.tsx
+++ b/src/view/com/modals/ChangeHandle.tsx
@@ -133,14 +133,22 @@ export function Component({onChanged}: {onChanged: () => void}) {
-
+
Cancel
-
- Change my handle
+
+ Change Handle
{isProcessing ? (
@@ -148,13 +156,20 @@ export function Component({onChanged}: {onChanged: () => void}) {
) : error && !serviceDescription ? (
+ onPress={onPressRetryConnect}
+ accessibilityRole="button"
+ accessibilityLabel="Retry change handle"
+ accessibilityHint={`Retries handle change to ${handle}`}>
Retry
) : canSave ? (
-
+
Save
@@ -238,13 +253,16 @@ function ProvidedHandleForm({
@@ -253,7 +271,11 @@ function ProvidedHandleForm({
@{createFullHandle(handle, userDomain)}
-
+
I have my own domain
@@ -338,7 +360,7 @@ function CustomHandleForm({
// =
return (
<>
-
+
Enter the domain you want to use
@@ -349,13 +371,16 @@ function CustomHandleForm({
@@ -421,7 +446,10 @@ function CustomHandleForm({
)}
-
+
Nevermind, create a handle for me
diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx
index 63877fe5dd..11e1a63348 100644
--- a/src/view/com/modals/Confirm.tsx
+++ b/src/view/com/modals/Confirm.tsx
@@ -11,17 +11,20 @@ import {s, colors} from 'lib/styles'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {cleanError} from 'lib/strings/errors'
import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb} from 'platform/detection'
-export const snapPoints = [300]
+export const snapPoints = ['50%']
export function Component({
title,
message,
onPressConfirm,
+ onPressCancel,
}: {
title: string
message: string | (() => JSX.Element)
onPressConfirm: () => void | Promise
+ onPressCancel?: () => void | Promise
}) {
const pal = usePalette('default')
const store = useStores()
@@ -65,10 +68,26 @@ export function Component({
+ style={[styles.btn]}
+ accessibilityRole="button"
+ accessibilityLabel="Confirm"
+ accessibilityHint="">
Confirm
)}
+ {onPressCancel === undefined ? null : (
+
+
+ Cancel
+
+
+ )}
)
}
@@ -77,7 +96,7 @@ const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
- paddingBottom: 60,
+ paddingBottom: isDesktopWeb ? 0 : 60,
},
title: {
textAlign: 'center',
@@ -98,4 +117,12 @@ const styles = StyleSheet.create({
marginHorizontal: 44,
backgroundColor: colors.blue3,
},
+ btnCancel: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 32,
+ padding: 14,
+ marginHorizontal: 20,
+ },
})
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
index 735de85a7b..5215c9cb4f 100644
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ b/src/view/com/modals/ContentFilteringSettings.tsx
@@ -7,34 +7,94 @@ import {useStores} from 'state/index'
import {LabelPreference} from 'state/models/ui/preferences'
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 {usePalette} from 'lib/hooks/usePalette'
import {CONFIGURABLE_LABEL_GROUPS} from 'lib/labeling/const'
-import {isDesktopWeb} from 'platform/detection'
+import {isDesktopWeb, isIOS} from 'platform/detection'
+import * as Toast from '../util/Toast'
export const snapPoints = ['90%']
-export function Component({}: {}) {
+export const Component = observer(({}: {}) => {
const store = useStores()
const pal = usePalette('default')
+
+ React.useEffect(() => {
+ 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])
return (
-
- Content Moderation
+
+ Content Filtering
-
-
-
-
+
+ {isIOS ? (
+
+ Adult content can only be enabled via the Web at{' '}
+
+ .
+
+ ) : (
+
+ )}
+
+
+
+
+
-
+
)
-}
+})
+// TODO: Refactor this component to pass labels down to each tab
const ContentLabelPref = observer(
- ({group}: {group: keyof typeof CONFIGURABLE_LABEL_GROUPS}) => {
+ ({
+ group,
+ disabled,
+ }: {
+ group: keyof typeof CONFIGURABLE_LABEL_GROUPS
+ disabled?: boolean
+ }) => {
const store = useStores()
const pal = usePalette('default')
+
+ const onChange = React.useCallback(
+ async (v: LabelPreference) => {
+ try {
+ await store.preferences.setContentLabelPref(group, v)
+ } 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, group],
+ )
+
return (
@@ -64,22 +146,29 @@ const ContentLabelPref = observer(
)}
- store.preferences.setContentLabelPref(group, v)}
- />
+ {disabled ? (
+
+ Hide
+
+ ) : (
+
+ )}
)
},
)
-function SelectGroup({
- current,
- onChange,
-}: {
+interface SelectGroupProps {
current: LabelPreference
onChange: (v: LabelPreference) => void
-}) {
+ group: keyof typeof CONFIGURABLE_LABEL_GROUPS
+}
+
+function SelectGroup({current, onChange, group}: SelectGroupProps) {
return (
)
}
+interface SelectableBtnProps {
+ current: string
+ value: LabelPreference
+ label: string
+ left?: boolean
+ right?: boolean
+ onChange: (v: LabelPreference) => void
+ group: keyof typeof CONFIGURABLE_LABEL_GROUPS
+}
+
function SelectableBtn({
current,
value,
@@ -113,14 +215,8 @@ function SelectableBtn({
left,
right,
onChange,
-}: {
- current: string
- value: LabelPreference
- label: string
- left?: boolean
- right?: boolean
- onChange: (v: LabelPreference) => void
-}) {
+ group,
+}: SelectableBtnProps) {
const pal = usePalette('default')
const palPrimary = usePalette('inverted')
return (
@@ -132,7 +228,10 @@ function SelectableBtn({
pal.border,
current === value ? palPrimary.view : pal.view,
]}
- onPress={() => onChange(value)}>
+ onPress={() => onChange(value)}
+ accessibilityRole="button"
+ accessibilityLabel={value}
+ accessibilityHint={`Set ${value} for ${group} content moderation policy`}>
{label}
@@ -209,4 +308,7 @@ const styles = StyleSheet.create({
padding: 14,
backgroundColor: colors.gray1,
},
+ toggleBtn: {
+ paddingHorizontal: 0,
+ },
})
diff --git a/src/view/com/modals/ContentLanguagesSettings.tsx b/src/view/com/modals/ContentLanguagesSettings.tsx
new file mode 100644
index 0000000000..700f1cbcb3
--- /dev/null
+++ b/src/view/com/modals/ContentLanguagesSettings.tsx
@@ -0,0 +1,143 @@
+import React from 'react'
+import {StyleSheet, Pressable, View} from 'react-native'
+import LinearGradient from 'react-native-linear-gradient'
+import {observer} from 'mobx-react-lite'
+import {ScrollView} from './util'
+import {useStores} from 'state/index'
+import {ToggleButton} from '../util/forms/ToggleButton'
+import {s, colors, gradients} from 'lib/styles'
+import {Text} from '../util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb} from 'platform/detection'
+import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../locale/languages'
+
+export const snapPoints = ['100%']
+
+export function Component({}: {}) {
+ const store = useStores()
+ const pal = usePalette('default')
+ const onPressDone = React.useCallback(() => {
+ store.shell.closeModal()
+ }, [store])
+
+ const languages = React.useMemo(() => {
+ const langs = LANGUAGES.filter(
+ lang =>
+ !!lang.code2.trim() &&
+ LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
+ )
+ // sort so that selected languages are on top, then alphabetically
+ langs.sort((a, b) => {
+ const hasA = store.preferences.hasContentLanguage(a.code2)
+ const hasB = store.preferences.hasContentLanguage(b.code2)
+ if (hasA === hasB) return a.name.localeCompare(b.name)
+ if (hasA) return -1
+ return 1
+ })
+ return langs
+ }, [store])
+
+ return (
+
+ Content Languages
+
+ Which languages would you like to see in the your feed? (Leave them all
+ unchecked to see any language.)
+
+
+ {languages.map(lang => (
+
+ ))}
+
+
+
+
+
+ Done
+
+
+
+
+ )
+}
+
+const LanguageToggle = observer(
+ ({code2, name}: {code2: string; name: string}) => {
+ const store = useStores()
+ const pal = usePalette('default')
+
+ const onPress = React.useCallback(() => {
+ store.preferences.toggleContentLanguage(code2)
+ }, [store, code2])
+
+ return (
+
+ )
+ },
+)
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingTop: 20,
+ },
+ title: {
+ textAlign: 'center',
+ fontWeight: 'bold',
+ fontSize: 24,
+ marginBottom: 12,
+ },
+ description: {
+ textAlign: 'center',
+ paddingHorizontal: 16,
+ marginBottom: 10,
+ },
+ scrollContainer: {
+ flex: 1,
+ paddingHorizontal: 10,
+ },
+ bottomSpacer: {
+ height: isDesktopWeb ? 0 : 60,
+ },
+ btnContainer: {
+ paddingTop: 10,
+ paddingHorizontal: 10,
+ paddingBottom: isDesktopWeb ? 0 : 40,
+ borderTopWidth: isDesktopWeb ? 0 : 1,
+ },
+
+ languageToggle: {
+ borderTopWidth: 1,
+ borderRadius: 0,
+ paddingHorizontal: 0,
+ paddingVertical: 12,
+ },
+
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ borderRadius: 32,
+ padding: 14,
+ backgroundColor: colors.gray1,
+ },
+})
diff --git a/src/view/com/modals/CreateOrEditMuteList.tsx b/src/view/com/modals/CreateOrEditMuteList.tsx
new file mode 100644
index 0000000000..7984ea64c6
--- /dev/null
+++ b/src/view/com/modals/CreateOrEditMuteList.tsx
@@ -0,0 +1,280 @@
+import React, {useState, useCallback} from 'react'
+import * as Toast from '../util/Toast'
+import {
+ ActivityIndicator,
+ KeyboardAvoidingView,
+ ScrollView,
+ StyleSheet,
+ TextInput,
+ TouchableOpacity,
+ View,
+} from 'react-native'
+import LinearGradient from 'react-native-linear-gradient'
+import {Image as RNImage} from 'react-native-image-crop-picker'
+import {Text} from '../util/text/Text'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {useStores} from 'state/index'
+import {ListModel} from 'state/models/content/list'
+import {s, colors, gradients} from 'lib/styles'
+import {enforceLen} from 'lib/strings/helpers'
+import {compressIfNeeded} from 'lib/media/manip'
+import {UserAvatar} from '../util/UserAvatar'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
+import {useAnalytics} from 'lib/analytics'
+import {cleanError, isNetworkError} from 'lib/strings/errors'
+import {isDesktopWeb} from 'platform/detection'
+
+const MAX_NAME = 64 // todo
+const MAX_DESCRIPTION = 300 // todo
+
+export const snapPoints = ['fullscreen']
+
+export function Component({
+ onSave,
+ list,
+}: {
+ onSave?: (uri: string) => void
+ list?: ListModel
+}) {
+ const store = useStores()
+ const [error, setError] = useState('')
+ const pal = usePalette('default')
+ const theme = useTheme()
+ const {track} = useAnalytics()
+
+ const [isProcessing, setProcessing] = useState(false)
+ const [name, setName] = useState(list?.list?.name || '')
+ const [description, setDescription] = useState(
+ list?.list?.description || '',
+ )
+ const [avatar, setAvatar] = useState(list?.list?.avatar)
+ const [newAvatar, setNewAvatar] = useState()
+
+ const onPressCancel = useCallback(() => {
+ store.shell.closeModal()
+ }, [store])
+
+ const onSelectNewAvatar = useCallback(
+ async (img: RNImage | null) => {
+ if (!img) {
+ setNewAvatar(null)
+ setAvatar(undefined)
+ return
+ }
+ track('CreateMuteList:AvatarSelected')
+ try {
+ const finalImg = await compressIfNeeded(img, 1000000)
+ setNewAvatar(finalImg)
+ setAvatar(finalImg.path)
+ } catch (e: any) {
+ setError(cleanError(e))
+ }
+ },
+ [track, setNewAvatar, setAvatar, setError],
+ )
+
+ const onPressSave = useCallback(async () => {
+ track('CreateMuteList:Save')
+ const nameTrimmed = name.trim()
+ if (!nameTrimmed) {
+ setError('Name is required')
+ return
+ }
+ setProcessing(true)
+ if (error) {
+ setError('')
+ }
+ try {
+ if (list) {
+ await list.updateMetadata({
+ name: nameTrimmed,
+ description: description.trim(),
+ avatar: newAvatar,
+ })
+ Toast.show('Mute list updated')
+ onSave?.(list.uri)
+ } else {
+ const res = await ListModel.createModList(store, {
+ name,
+ description,
+ avatar: newAvatar,
+ })
+ Toast.show('Mute list created')
+ onSave?.(res.uri)
+ }
+ store.shell.closeModal()
+ } catch (e: any) {
+ if (isNetworkError(e)) {
+ setError(
+ 'Failed to create the mute list. Check your internet connection and try again.',
+ )
+ } else {
+ setError(cleanError(e))
+ }
+ }
+ setProcessing(false)
+ }, [
+ track,
+ setProcessing,
+ setError,
+ error,
+ onSave,
+ store,
+ name,
+ description,
+ newAvatar,
+ list,
+ ])
+
+ return (
+
+
+
+ {list ? 'Edit Mute List' : 'New Mute List'}
+
+ {error !== '' && (
+
+
+
+ )}
+ List Avatar
+
+
+
+
+
+
+ List Name
+
+ setName(enforceLen(v, MAX_NAME))}
+ accessible={true}
+ accessibilityLabel="Name"
+ accessibilityHint=""
+ accessibilityLabelledBy="list-name"
+ />
+
+
+
+ Description
+
+ setDescription(enforceLen(v, MAX_DESCRIPTION))}
+ accessible={true}
+ accessibilityLabel="Description"
+ accessibilityHint=""
+ accessibilityLabelledBy="list-description"
+ />
+
+ {isProcessing ? (
+
+
+
+ ) : (
+
+
+ Save
+
+
+ )}
+
+
+ Cancel
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ paddingHorizontal: isDesktopWeb ? 0 : 16,
+ },
+ title: {
+ textAlign: 'center',
+ fontWeight: 'bold',
+ fontSize: 24,
+ marginBottom: 18,
+ },
+ label: {
+ fontWeight: 'bold',
+ paddingHorizontal: 4,
+ paddingBottom: 4,
+ marginTop: 20,
+ },
+ form: {
+ paddingHorizontal: 6,
+ },
+ textInput: {
+ borderWidth: 1,
+ borderRadius: 6,
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ fontSize: 16,
+ },
+ textArea: {
+ borderWidth: 1,
+ borderRadius: 6,
+ paddingHorizontal: 12,
+ paddingTop: 10,
+ fontSize: 16,
+ height: 100,
+ textAlignVertical: 'top',
+ },
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ borderRadius: 32,
+ padding: 10,
+ marginBottom: 10,
+ },
+ avi: {
+ width: 84,
+ height: 84,
+ borderWidth: 2,
+ borderRadius: 42,
+ marginTop: 4,
+ },
+ errorContainer: {marginTop: 20},
+})
diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx
index 353122163a..b4933a1f22 100644
--- a/src/view/com/modals/DeleteAccount.tsx
+++ b/src/view/com/modals/DeleteAccount.tsx
@@ -16,6 +16,7 @@ import {useTheme} from 'lib/ThemeContext'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {cleanError} from 'lib/strings/errors'
import {resetToTab} from '../../../Navigation'
+import {isDesktopWeb} from 'platform/detection'
export const snapPoints = ['60%']
@@ -42,11 +43,13 @@ export function Component({}: {}) {
const onPressConfirmDelete = async () => {
setError('')
setIsProcessing(true)
+ const token = confirmCode.replace(/\s/g, '')
+
try {
await store.agent.com.atproto.server.deleteAccount({
did: store.me.did,
password,
- token: confirmCode,
+ token,
})
Toast.show('Your account has been deleted')
resetToTab('HomeTab')
@@ -61,17 +64,36 @@ export function Component({}: {}) {
store.shell.closeModal()
}
return (
-
+
-
- Delete account
-
+
+
+ Delete Account
+
+
+
+ {' "'}
+
+
+ {store.me.handle}
+
+
+ {'"'}
+
+
+
{!isEmailSent ? (
<>
For security reasons, we'll need to send a confirmation code to
- your email.
+ your email address.
{error ? (
@@ -86,7 +108,10 @@ export function Component({}: {}) {
<>
+ onPress={onPressSendEmail}
+ accessibilityRole="button"
+ accessibilityLabel="Send email"
+ accessibilityHint="Sends email with confirmation code for account deletion">
- Send email
+ Send Email
+ onPress={onCancel}
+ accessibilityRole="button"
+ accessibilityLabel="Cancel account deletion"
+ accessibilityHint=""
+ onAccessibilityEscape={onCancel}>
Cancel
@@ -112,7 +141,11 @@ export function Component({}: {}) {
>
) : (
<>
-
+ {/* TODO: Update this label to be more concise */}
+
Check your inbox for an email with the confirmation code to enter
below:
@@ -123,8 +156,11 @@ export function Component({}: {}) {
keyboardAppearance={theme.colorScheme}
value={confirmCode}
onChangeText={setConfirmCode}
+ accessibilityLabelledBy="confirmationCode"
+ accessibilityLabel="Confirmation code"
+ accessibilityHint="Input confirmation code for account deletion"
/>
-
+
Please enter your password as well:
{error ? (
@@ -149,14 +188,21 @@ export function Component({}: {}) {
<>
+ onPress={onPressConfirmDelete}
+ accessibilityRole="button"
+ accessibilityLabel="Confirm delete account"
+ accessibilityHint="">
Delete my account
+ onPress={onCancel}
+ accessibilityRole="button"
+ accessibilityLabel="Cancel account deletion"
+ accessibilityHint="Exits account deletion process"
+ onAccessibilityEscape={onCancel}>
Cancel
@@ -177,10 +223,25 @@ const styles = StyleSheet.create({
innerContainer: {
paddingBottom: 20,
},
- title: {
- textAlign: 'center',
+ titleContainer: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ flexWrap: 'wrap',
marginTop: 12,
marginBottom: 12,
+ marginLeft: 20,
+ marginRight: 20,
+ },
+ titleMobile: {
+ textAlign: 'center',
+ },
+ titleDesktop: {
+ textAlign: 'center',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ maxWidth: '400px',
},
description: {
textAlign: 'center',
diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx
new file mode 100644
index 0000000000..09ae019436
--- /dev/null
+++ b/src/view/com/modals/EditImage.tsx
@@ -0,0 +1,378 @@
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
+import {Pressable, StyleSheet, View} from 'react-native'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWindowDimensions} from 'react-native'
+import {gradients, s} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {Text} from '../util/text/Text'
+import LinearGradient from 'react-native-linear-gradient'
+import {useStores} from 'state/index'
+import ImageEditor, {Position} from 'react-avatar-editor'
+import {TextInput} from './util'
+import {enforceLen} from 'lib/strings/helpers'
+import {MAX_ALT_TEXT} from 'lib/constants'
+import {GalleryModel} from 'state/models/media/gallery'
+import {ImageModel} from 'state/models/media/image'
+import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
+import {Slider} from '@miblanchard/react-native-slider'
+import {MaterialIcons} from '@expo/vector-icons'
+import {observer} from 'mobx-react-lite'
+import {getKeys} from 'lib/type-assertions'
+import {isDesktopWeb} from 'platform/detection'
+
+export const snapPoints = ['80%']
+
+const RATIOS = {
+ '4:3': {
+ Icon: RectWideIcon,
+ },
+ '1:1': {
+ Icon: SquareIcon,
+ },
+ '3:4': {
+ Icon: RectTallIcon,
+ },
+ None: {
+ label: 'None',
+ Icon: MaterialIcons,
+ name: 'do-not-disturb-alt',
+ },
+} as const
+
+type AspectRatio = keyof typeof RATIOS
+
+interface Props {
+ image: ImageModel
+ gallery: GalleryModel
+}
+
+export const Component = observer(function ({image, gallery}: Props) {
+ const pal = usePalette('default')
+ const theme = useTheme()
+ const store = useStores()
+ const windowDimensions = useWindowDimensions()
+
+ const {
+ aspectRatio,
+ // rotate = 0
+ } = image.attributes
+
+ const editorRef = useRef(null)
+ const [scale, setScale] = useState(image.attributes.scale ?? 1)
+ const [position, setPosition] = useState(
+ image.attributes.position,
+ )
+ const [altText, setAltText] = useState('')
+
+ const onFlipHorizontal = useCallback(() => {
+ image.flipHorizontal()
+ }, [image])
+
+ const onFlipVertical = useCallback(() => {
+ image.flipVertical()
+ }, [image])
+
+ // const onSetRotate = useCallback(
+ // (direction: 'left' | 'right') => {
+ // const rotation = (rotate + 90 * (direction === 'left' ? -1 : 1)) % 360
+ // image.setRotate(rotation)
+ // },
+ // [rotate, image],
+ // )
+
+ const onSetRatio = useCallback(
+ (ratio: AspectRatio) => {
+ image.setRatio(ratio)
+ },
+ [image],
+ )
+
+ const adjustments = useMemo(
+ () => [
+ // {
+ // name: 'rotate-left' as const,
+ // label: 'Rotate left',
+ // onPress: () => {
+ // onSetRotate('left')
+ // },
+ // },
+ // {
+ // name: 'rotate-right' as const,
+ // label: 'Rotate right',
+ // onPress: () => {
+ // onSetRotate('right')
+ // },
+ // },
+ {
+ name: 'flip' as const,
+ label: 'Flip horizontal',
+ onPress: onFlipHorizontal,
+ },
+ {
+ name: 'flip' as const,
+ label: 'Flip vertically',
+ onPress: onFlipVertical,
+ },
+ ],
+ [onFlipHorizontal, onFlipVertical],
+ )
+
+ useEffect(() => {
+ image.prev = image.cropped
+ image.prevAttributes = image.attributes
+ image.resetCropped()
+ }, [image])
+
+ const onCloseModal = useCallback(() => {
+ store.shell.closeModal()
+ }, [store.shell])
+
+ const onPressCancel = useCallback(async () => {
+ await gallery.previous(image)
+ onCloseModal()
+ }, [onCloseModal, gallery, image])
+
+ const onPressSave = useCallback(async () => {
+ image.setAltText(altText)
+
+ const crop = editorRef.current?.getCroppingRect()
+
+ await image.manipulate({
+ ...(crop !== undefined
+ ? {
+ crop: {
+ originX: crop.x,
+ originY: crop.y,
+ width: crop.width,
+ height: crop.height,
+ },
+ ...(scale !== 1 ? {scale} : {}),
+ ...(position !== undefined ? {position} : {}),
+ }
+ : {}),
+ })
+
+ image.prev = image.cropped
+ image.prevAttributes = image.attributes
+ onCloseModal()
+ }, [altText, image, position, scale, onCloseModal])
+
+ const getLabelIconSize = useCallback((as: AspectRatio) => {
+ switch (as) {
+ case 'None':
+ return 22
+ case '1:1':
+ return 32
+ default:
+ return 26
+ }
+ }, [])
+
+ if (image.cropped === undefined) {
+ return null
+ }
+
+ const computedWidth =
+ windowDimensions.width > 500 ? 410 : windowDimensions.width - 80
+ const sideLength = isDesktopWeb ? 300 : computedWidth
+
+ const dimensions = image.getResizedDimensions(aspectRatio, sideLength)
+ const imgContainerStyles = {width: sideLength, height: sideLength}
+
+ const imgControlStyles = {
+ alignItems: 'center' as const,
+ flexDirection: isDesktopWeb ? ('row' as const) : ('column' as const),
+ gap: isDesktopWeb ? 5 : 0,
+ }
+
+ return (
+
+ Edit image
+
+
+
+
+
+
+ setScale(Array.isArray(v) ? v[0] : v)
+ }
+ minimumValue={1}
+ maximumValue={3}
+ />
+
+
+ {isDesktopWeb ? (
+
+ Ratios
+
+ ) : null}
+
+ {getKeys(RATIOS).map(ratio => {
+ const {Icon, ...props} = RATIOS[ratio]
+ const labelIconSize = getLabelIconSize(ratio)
+ const isSelected = aspectRatio === ratio
+
+ return (
+ {
+ onSetRatio(ratio)
+ }}
+ accessibilityLabel={ratio}
+ accessibilityHint="">
+
+
+
+ {ratio}
+
+
+ )
+ })}
+
+ {isDesktopWeb ? (
+
+ Transformations
+
+ ) : null}
+
+ {adjustments.map(({label, name, onPress}) => (
+
+
+
+ ))}
+
+
+
+
+
+ Accessibility
+
+ setAltText(enforceLen(text, MAX_ALT_TEXT))}
+ accessibilityLabel="Alt text"
+ accessibilityHint=""
+ accessibilityLabelledBy="alt-text"
+ />
+
+
+
+
+ Cancel
+
+
+
+
+
+ Done
+
+
+
+
+
+ )
+})
+
+const styles = StyleSheet.create({
+ container: {
+ gap: 18,
+ paddingHorizontal: isDesktopWeb ? undefined : 16,
+ height: '100%',
+ width: '100%',
+ },
+ subsection: {marginTop: 12},
+ gap18: {gap: 18},
+ title: {
+ fontWeight: 'bold',
+ fontSize: 24,
+ },
+ btns: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ btn: {
+ borderRadius: 4,
+ paddingVertical: 8,
+ paddingHorizontal: 24,
+ },
+ imgControl: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: 40,
+ },
+ imgEditor: {
+ maxWidth: '100%',
+ },
+ imgContainer: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderWidth: 1,
+ borderStyle: 'solid',
+ marginBottom: 4,
+ },
+ flipVertical: {
+ transform: [{rotate: '90deg'}],
+ },
+ flipBtn: {
+ paddingHorizontal: 4,
+ paddingVertical: 8,
+ },
+ textArea: {
+ borderWidth: 1,
+ borderRadius: 6,
+ paddingTop: 10,
+ paddingHorizontal: 12,
+ fontSize: 16,
+ height: 100,
+ textAlignVertical: 'top',
+ maxHeight: isDesktopWeb ? undefined : 50,
+ },
+ bottomSection: {
+ borderTopWidth: 1,
+ paddingTop: 18,
+ },
+})
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index 8b1a737835..3db8d82d80 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -1,13 +1,15 @@
-import React, {useState} from 'react'
+import React, {useState, useCallback} from 'react'
import * as Toast from '../util/Toast'
import {
ActivityIndicator,
+ KeyboardAvoidingView,
+ ScrollView,
StyleSheet,
+ TextInput,
TouchableOpacity,
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
-import {ScrollView, TextInput} from './util'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
@@ -24,7 +26,7 @@ import {useTheme} from 'lib/ThemeContext'
import {useAnalytics} from 'lib/analytics/analytics'
import {cleanError, isNetworkError} from 'lib/strings/errors'
-export const snapPoints = ['80%']
+export const snapPoints = ['fullscreen']
export function Component({
profileView,
@@ -61,38 +63,45 @@ export function Component({
const onPressCancel = () => {
store.shell.closeModal()
}
- const onSelectNewAvatar = async (img: RNImage | null) => {
- track('EditProfile:AvatarSelected')
- try {
- // if img is null, user selected "remove avatar"
- if (!img) {
+ const onSelectNewAvatar = useCallback(
+ async (img: RNImage | null) => {
+ if (img === null) {
setNewUserAvatar(null)
setUserAvatar(null)
return
}
- const finalImg = await compressIfNeeded(img, 1000000)
- setNewUserAvatar(finalImg)
- setUserAvatar(finalImg.path)
- } catch (e: any) {
- setError(cleanError(e))
- }
- }
- const onSelectNewBanner = async (img: RNImage | null) => {
- if (!img) {
- setNewUserBanner(null)
- setUserBanner(null)
- return
- }
- track('EditProfile:BannerSelected')
- try {
- const finalImg = await compressIfNeeded(img, 1000000)
- setNewUserBanner(finalImg)
- setUserBanner(finalImg.path)
- } catch (e: any) {
- setError(cleanError(e))
- }
- }
- const onPressSave = async () => {
+ track('EditProfile:AvatarSelected')
+ try {
+ const finalImg = await compressIfNeeded(img, 1000000)
+ setNewUserAvatar(finalImg)
+ setUserAvatar(finalImg.path)
+ } catch (e: any) {
+ setError(cleanError(e))
+ }
+ },
+ [track, setNewUserAvatar, setUserAvatar, setError],
+ )
+
+ const onSelectNewBanner = useCallback(
+ async (img: RNImage | null) => {
+ if (!img) {
+ setNewUserBanner(null)
+ setUserBanner(null)
+ return
+ }
+ track('EditProfile:BannerSelected')
+ try {
+ const finalImg = await compressIfNeeded(img, 1000000)
+ setNewUserBanner(finalImg)
+ setUserBanner(finalImg.path)
+ } catch (e: any) {
+ setError(cleanError(e))
+ }
+ },
+ [track, setNewUserBanner, setUserBanner, setError],
+ )
+
+ const onPressSave = useCallback(async () => {
track('EditProfile:Save')
setProcessing(true)
if (error) {
@@ -120,11 +129,23 @@ export function Component({
}
}
setProcessing(false)
- }
+ }, [
+ track,
+ setProcessing,
+ setError,
+ error,
+ profileView,
+ onUpdate,
+ store,
+ displayName,
+ description,
+ newUserAvatar,
+ newUserBanner,
+ ])
return (
-
-
+
+
Edit my profile
)}
-
- Display Name
- setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))}
- />
-
-
- Description
- setDescription(enforceLen(v, MAX_DESCRIPTION))}
- />
-
- {isProcessing ? (
-
-
+
+
+ Display Name
+
+ setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
+ }
+ accessible={true}
+ accessibilityLabel="Display name"
+ accessibilityHint="Edit your display name"
+ />
- ) : (
+
+ Description
+ setDescription(enforceLen(v, MAX_DESCRIPTION))}
+ accessible={true}
+ accessibilityLabel="Description"
+ accessibilityHint="Edit your profile description"
+ />
+
+ {isProcessing ? (
+
+
+
+ ) : (
+
+
+ Save Changes
+
+
+ )}
-
- Save Changes
-
+ testID="editProfileCancelBtn"
+ style={s.mt5}
+ onPress={onPressCancel}
+ accessibilityRole="button"
+ accessibilityLabel="Cancel profile editing"
+ accessibilityHint=""
+ onAccessibilityEscape={onPressCancel}>
+
+ Cancel
+
- )}
-
-
- Cancel
-
-
+
-
+
)
}
const styles = StyleSheet.create({
- inner: {
- padding: 14,
- },
title: {
textAlign: 'center',
fontWeight: 'bold',
@@ -215,6 +250,9 @@ const styles = StyleSheet.create({
paddingBottom: 4,
marginTop: 20,
},
+ form: {
+ paddingHorizontal: 14,
+ },
textInput: {
borderWidth: 1,
borderRadius: 6,
@@ -243,7 +281,7 @@ const styles = StyleSheet.create({
avi: {
position: 'absolute',
top: 80,
- left: 10,
+ left: 24,
width: 84,
height: 84,
borderWidth: 2,
diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx
index 5e31e16a8b..b3fe9dd3ff 100644
--- a/src/view/com/modals/InviteCodes.tsx
+++ b/src/view/com/modals/InviteCodes.tsx
@@ -1,5 +1,6 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -56,7 +57,7 @@ export function Component({}: {}) {
code works once!
- ( We'll send you more periodically. )
+ (You'll receive one invite code every two weeks.)
{store.me.invites.map((invite, i) => (
@@ -82,46 +83,50 @@ export function Component({}: {}) {
)
}
-function InviteCode({
- testID,
- code,
- used,
-}: {
- testID: string
- code: string
- used?: boolean
-}) {
- const pal = usePalette('default')
- const [wasCopied, setWasCopied] = React.useState(false)
+const InviteCode = observer(
+ ({testID, code, used}: {testID: string; code: string; used?: boolean}) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const {invitesAvailable} = store.me
- const onPress = React.useCallback(() => {
- Clipboard.setString(code)
- Toast.show('Copied to clipboard')
- setWasCopied(true)
- }, [code])
+ const onPress = React.useCallback(() => {
+ Clipboard.setString(code)
+ Toast.show('Copied to clipboard')
+ store.invitedUsers.setInviteCopied(code)
+ }, [store, code])
- return (
-
-
- {code}
-
- {wasCopied ? (
- Copied
- ) : !used ? (
-
- ) : undefined}
-
- )
-}
+ return (
+
+
+ {code}
+
+
+ {!used && store.invitedUsers.isInviteCopied(code) && (
+ Copied
+ )}
+ {!used && (
+
+ )}
+
+ )
+ },
+)
const styles = StyleSheet.create({
container: {
@@ -163,11 +168,13 @@ const styles = StyleSheet.create({
inviteCode: {
flexDirection: 'row',
alignItems: 'center',
- justifyContent: 'space-between',
borderBottomWidth: 1,
paddingHorizontal: 20,
paddingVertical: 14,
},
+ codeCopied: {
+ marginRight: 8,
+ },
strikeThrough: {
textDecorationLine: 'line-through',
textDecorationStyle: 'solid',
diff --git a/src/view/com/modals/ListAddRemoveUser.tsx b/src/view/com/modals/ListAddRemoveUser.tsx
new file mode 100644
index 0000000000..c2d63ef6e7
--- /dev/null
+++ b/src/view/com/modals/ListAddRemoveUser.tsx
@@ -0,0 +1,253 @@
+import React, {useCallback} from 'react'
+import {observer} from 'mobx-react-lite'
+import {Pressable, StyleSheet, View} from 'react-native'
+import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {Text} from '../util/text/Text'
+import {UserAvatar} from '../util/UserAvatar'
+import {ListsList} from '../lists/ListsList'
+import {ListsListModel} from 'state/models/lists/lists-list'
+import {ListMembershipModel} from 'state/models/content/list-membership'
+import {EmptyStateWithButton} from '../util/EmptyStateWithButton'
+import {Button} from '../util/forms/Button'
+import * as Toast from '../util/Toast'
+import {useStores} from 'state/index'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {s} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb, isAndroid} from 'platform/detection'
+
+export const snapPoints = ['fullscreen']
+
+export const Component = observer(
+ ({
+ subject,
+ displayName,
+ onUpdate,
+ }: {
+ subject: string
+ displayName: string
+ onUpdate?: () => void
+ }) => {
+ const store = useStores()
+ const pal = usePalette('default')
+ const palPrimary = usePalette('primary')
+ const palInverted = usePalette('inverted')
+ const [selected, setSelected] = React.useState([])
+
+ const listsList: ListsListModel = React.useMemo(
+ () => new ListsListModel(store, store.me.did),
+ [store],
+ )
+ const memberships: ListMembershipModel = React.useMemo(
+ () => new ListMembershipModel(store, subject),
+ [store, subject],
+ )
+ React.useEffect(() => {
+ listsList.refresh()
+ memberships.fetch().then(
+ () => {
+ setSelected(memberships.memberships.map(m => m.value.list))
+ },
+ err => {
+ store.log.error('Failed to fetch memberships', {err})
+ },
+ )
+ }, [memberships, listsList, store, setSelected])
+
+ const onPressCancel = useCallback(() => {
+ store.shell.closeModal()
+ }, [store])
+
+ const onPressSave = useCallback(async () => {
+ try {
+ await memberships.updateTo(selected)
+ } catch (err) {
+ store.log.error('Failed to update memberships', {err})
+ return
+ }
+ Toast.show('Lists updated')
+ onUpdate?.()
+ store.shell.closeModal()
+ }, [store, selected, memberships, onUpdate])
+
+ const onPressNewMuteList = useCallback(() => {
+ store.shell.openModal({
+ name: 'create-or-edit-mute-list',
+ onSave: (_uri: string) => {
+ listsList.refresh()
+ },
+ })
+ }, [store, listsList])
+
+ const onToggleSelected = useCallback(
+ (uri: string) => {
+ if (selected.includes(uri)) {
+ setSelected(selected.filter(uri2 => uri2 !== uri))
+ } else {
+ setSelected([...selected, uri])
+ }
+ },
+ [selected, setSelected],
+ )
+
+ const renderItem = useCallback(
+ (list: GraphDefs.ListView) => {
+ const isSelected = selected.includes(list.uri)
+ return (
+ onToggleSelected(list.uri)}>
+
+
+
+
+
+ {sanitizeDisplayName(list.name)}
+
+
+ {list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'}{' '}
+ by{' '}
+ {list.creator.did === store.me.did
+ ? 'you'
+ : `@${list.creator.handle}`}
+
+
+
+ {isSelected && (
+
+ )}
+
+
+ )
+ },
+ [pal, palPrimary, palInverted, onToggleSelected, selected, store.me.did],
+ )
+
+ const renderEmptyState = React.useCallback(() => {
+ return (
+
+ )
+ }, [onPressNewMuteList])
+
+ return (
+
+ Add {displayName} to Lists
+
+
+
+
+
+
+ )
+ },
+)
+
+const styles = StyleSheet.create({
+ container: {
+ paddingHorizontal: isDesktopWeb ? 0 : 16,
+ },
+ title: {
+ textAlign: 'center',
+ fontWeight: 'bold',
+ fontSize: 24,
+ marginBottom: 10,
+ },
+ list: {
+ flex: 1,
+ borderTopWidth: 1,
+ },
+ btns: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 10,
+ paddingTop: 10,
+ paddingBottom: isAndroid ? 10 : 0,
+ borderTopWidth: 1,
+ },
+ footerBtn: {
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ },
+
+ listItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderTopWidth: 1,
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ },
+ listItemAvi: {
+ width: 54,
+ paddingLeft: 4,
+ paddingTop: 8,
+ paddingBottom: 10,
+ },
+ listItemContent: {
+ flex: 1,
+ paddingRight: 10,
+ paddingTop: 10,
+ paddingBottom: 10,
+ },
+ checkbox: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderWidth: 1,
+ width: 24,
+ height: 24,
+ borderRadius: 6,
+ marginRight: 8,
+ },
+})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 3f10ec8365..864dcc847e 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -1,5 +1,6 @@
import React, {useRef, useEffect} from 'react'
-import {StyleSheet, View} from 'react-native'
+import {StyleSheet} from 'react-native'
+import {SafeAreaView} from 'react-native-safe-area-context'
import {observer} from 'mobx-react-lite'
import BottomSheet from '@gorhom/bottom-sheet'
import {useStores} from 'state/index'
@@ -9,14 +10,20 @@ import {usePalette} from 'lib/hooks/usePalette'
import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile'
import * as ServerInputModal from './ServerInput'
-import * as ReportPostModal from './ReportPost'
+import * as ReportPostModal from './report/ReportPost'
import * as RepostModal from './Repost'
-import * as ReportAccountModal from './ReportAccount'
+import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
+import * as ListAddRemoveUserModal from './ListAddRemoveUser'
+import * as AltImageModal from './AltImage'
+import * as EditImageModal from './AltImage'
+import * as ReportAccountModal from './report/ReportAccount'
import * as DeleteAccountModal from './DeleteAccount'
import * as ChangeHandleModal from './ChangeHandle'
import * as WaitlistModal from './Waitlist'
import * as InviteCodesModal from './InviteCodes'
+import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
+import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
const DEFAULT_SNAPPOINTS = ['90%']
@@ -62,12 +69,24 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'report-account') {
snapPoints = ReportAccountModal.snapPoints
element =
+ } else if (activeModal?.name === 'create-or-edit-mute-list') {
+ snapPoints = CreateOrEditMuteListModal.snapPoints
+ element =
+ } else if (activeModal?.name === 'list-add-remove-user') {
+ snapPoints = ListAddRemoveUserModal.snapPoints
+ element =
} else if (activeModal?.name === 'delete-account') {
snapPoints = DeleteAccountModal.snapPoints
element =
} else if (activeModal?.name === 'repost') {
snapPoints = RepostModal.snapPoints
element =
+ } else if (activeModal?.name === 'alt-text-image') {
+ snapPoints = AltImageModal.snapPoints
+ element =
+ } else if (activeModal?.name === 'edit-image') {
+ snapPoints = AltImageModal.snapPoints
+ element =
} else if (activeModal?.name === 'change-handle') {
snapPoints = ChangeHandleModal.snapPoints
element =
@@ -77,11 +96,25 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'invite-codes') {
snapPoints = InviteCodesModal.snapPoints
element =
+ } else if (activeModal?.name === 'add-app-password') {
+ snapPoints = AddAppPassword.snapPoints
+ element =
} else if (activeModal?.name === 'content-filtering-settings') {
snapPoints = ContentFilteringSettingsModal.snapPoints
element =
+ } else if (activeModal?.name === 'content-languages-settings') {
+ snapPoints = ContentLanguagesSettingsModal.snapPoints
+ element =
} else {
- return
+ return null
+ }
+
+ if (snapPoints[0] === 'fullscreen') {
+ return (
+
+ {element}
+
+ )
}
return (
@@ -90,7 +123,8 @@ export const ModalsContainer = observer(function ModalsContainer() {
snapPoints={snapPoints}
index={store.shell.isModalActive ? 0 : -1}
enablePanDownToClose
- keyboardBehavior="fillParent"
+ android_keyboardInputMode="adjustResize"
+ keyboardBlurBehavior="restore"
backdropComponent={
store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined
}
@@ -107,4 +141,11 @@ const styles = StyleSheet.create({
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
},
+ fullscreenContainer: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+ },
})
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 25fed69a49..27b2641ba5 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -9,15 +9,21 @@ import {isMobileWeb} from 'platform/detection'
import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile'
import * as ServerInputModal from './ServerInput'
-import * as ReportPostModal from './ReportPost'
-import * as ReportAccountModal from './ReportAccount'
+import * as ReportPostModal from './report/ReportPost'
+import * as ReportAccountModal from './report/ReportAccount'
+import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
+import * as ListAddRemoveUserModal from './ListAddRemoveUser'
import * as DeleteAccountModal from './DeleteAccount'
import * as RepostModal from './Repost'
import * as CropImageModal from './crop-image/CropImage.web'
+import * as AltTextImageModal from './AltImage'
+import * as EditImageModal from './EditImage'
import * as ChangeHandleModal from './ChangeHandle'
import * as WaitlistModal from './Waitlist'
import * as InviteCodesModal from './InviteCodes'
+import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
+import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
export const ModalsContainer = observer(function ModalsContainer() {
const store = useStores()
@@ -44,12 +50,13 @@ function Modal({modal}: {modal: ModalIface}) {
}
const onPressMask = () => {
- if (modal.name === 'crop-image') {
+ if (modal.name === 'crop-image' || modal.name === 'edit-image') {
return // dont close on mask presses during crop
}
store.shell.closeModal()
}
const onInnerPress = () => {
+ // TODO: can we use prevent default?
// do nothing, we just want to stop it from bubbling
}
@@ -64,6 +71,10 @@ function Modal({modal}: {modal: ModalIface}) {
element =
} else if (modal.name === 'report-account') {
element =
+ } else if (modal.name === 'create-or-edit-mute-list') {
+ element =
+ } else if (modal.name === 'list-add-remove-user') {
+ element =
} else if (modal.name === 'crop-image') {
element =
} else if (modal.name === 'delete-account') {
@@ -76,21 +87,32 @@ function Modal({modal}: {modal: ModalIface}) {
element =
} else if (modal.name === 'invite-codes') {
element =
+ } else if (modal.name === 'add-app-password') {
+ element =
} else if (modal.name === 'content-filtering-settings') {
element =
+ } else if (modal.name === 'content-languages-settings') {
+ element =
+ } else if (modal.name === 'alt-text-image') {
+ element =
+ } else if (modal.name === 'edit-image') {
+ element =
} else {
return null
}
return (
+ // eslint-disable-next-line
+ {/* eslint-disable-next-line */}
{element}
@@ -118,6 +140,7 @@ const styles = StyleSheet.create({
paddingVertical: 20,
paddingHorizontal: 24,
borderRadius: 8,
+ borderWidth: 1,
},
containerMobile: {
borderRadius: 0,
diff --git a/src/view/com/modals/ReportAccount.tsx b/src/view/com/modals/ReportAccount.tsx
deleted file mode 100644
index 601bccbd13..0000000000
--- a/src/view/com/modals/ReportAccount.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import React, {useState} from 'react'
-import {
- ActivityIndicator,
- StyleSheet,
- TouchableOpacity,
- View,
-} from 'react-native'
-import {ComAtprotoModerationDefs} from '@atproto/api'
-import LinearGradient from 'react-native-linear-gradient'
-import {useStores} from 'state/index'
-import {s, colors, gradients} from 'lib/styles'
-import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup'
-import {Text} from '../util/text/Text'
-import * as Toast from '../util/Toast'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {cleanError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-
-const ITEMS: RadioGroupItem[] = [
- {key: 'spam', label: 'Spam or excessive repeat posts'},
- {key: 'abuse', label: 'Abusive, rude, or hateful'},
- {key: 'illegal', label: 'Posts illegal content'},
-]
-
-export const snapPoints = ['50%']
-
-export function Component({did}: {did: string}) {
- const store = useStores()
- const pal = usePalette('default')
- const [isProcessing, setIsProcessing] = useState(false)
- const [error, setError] = useState('')
- const [issue, setIssue] = useState('')
- const onSelectIssue = (v: string) => setIssue(v)
- const onPress = async () => {
- setError('')
- if (!issue) {
- return
- }
- setIsProcessing(true)
- try {
- // NOTE: we should update the lexicon of reasontype to include more options -prf
- let reasonType = ComAtprotoModerationDefs.REASONOTHER
- if (issue === 'spam') {
- reasonType = ComAtprotoModerationDefs.REASONSPAM
- }
- const reason = ITEMS.find(item => item.key === issue)?.label || ''
- await store.agent.com.atproto.moderation.createReport({
- reasonType,
- reason,
- subject: {
- $type: 'com.atproto.admin.defs#repoRef',
- did,
- },
- })
- Toast.show("Thank you for your report! We'll look into it promptly.")
- store.shell.closeModal()
- return
- } catch (e: any) {
- setError(cleanError(e))
- setIsProcessing(false)
- }
- }
- return (
-
- Report account
-
- What is the issue with this account?
-
-
- {error ? (
-
-
-
- ) : undefined}
- {isProcessing ? (
-
-
-
- ) : issue ? (
-
-
- Send Report
-
-
- ) : undefined}
-
- )
-}
-
-const styles = StyleSheet.create({
- title: {
- textAlign: 'center',
- fontWeight: 'bold',
- fontSize: 24,
- marginBottom: 12,
- },
- description: {
- textAlign: 'center',
- fontSize: 17,
- paddingHorizontal: 22,
- marginBottom: 10,
- },
- btn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- width: '100%',
- borderRadius: 32,
- padding: 14,
- backgroundColor: colors.gray1,
- },
-})
diff --git a/src/view/com/modals/ReportPost.tsx b/src/view/com/modals/ReportPost.tsx
deleted file mode 100644
index 01a132af09..0000000000
--- a/src/view/com/modals/ReportPost.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import React, {useState} from 'react'
-import {
- ActivityIndicator,
- StyleSheet,
- TouchableOpacity,
- View,
-} from 'react-native'
-import {ComAtprotoModerationDefs} from '@atproto/api'
-import LinearGradient from 'react-native-linear-gradient'
-import {useStores} from 'state/index'
-import {s, colors, gradients} from 'lib/styles'
-import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup'
-import {Text} from '../util/text/Text'
-import * as Toast from '../util/Toast'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {cleanError} from 'lib/strings/errors'
-import {usePalette} from 'lib/hooks/usePalette'
-
-const ITEMS: RadioGroupItem[] = [
- {key: 'spam', label: 'Spam or excessive repeat posts'},
- {key: 'abuse', label: 'Abusive, rude, or hateful'},
- {key: 'copyright', label: 'Contains copyrighted material'},
- {key: 'illegal', label: 'Contains illegal content'},
-]
-
-export const snapPoints = ['50%']
-
-export function Component({
- postUri,
- postCid,
-}: {
- postUri: string
- postCid: string
-}) {
- const store = useStores()
- const pal = usePalette('default')
- const [isProcessing, setIsProcessing] = useState(false)
- const [error, setError] = useState('')
- const [issue, setIssue] = useState('')
- const onSelectIssue = (v: string) => setIssue(v)
- const onPress = async () => {
- setError('')
- if (!issue) {
- return
- }
- setIsProcessing(true)
- try {
- // NOTE: we should update the lexicon of reasontype to include more options -prf
- let reasonType = ComAtprotoModerationDefs.REASONOTHER
- if (issue === 'spam') {
- reasonType = ComAtprotoModerationDefs.REASONSPAM
- }
- const reason = ITEMS.find(item => item.key === issue)?.label || ''
- await store.agent.createModerationReport({
- reasonType,
- reason,
- subject: {
- $type: 'com.atproto.repo.strongRef',
- uri: postUri,
- cid: postCid,
- },
- })
- Toast.show("Thank you for your report! We'll look into it promptly.")
- store.shell.closeModal()
- return
- } catch (e: any) {
- setError(cleanError(e))
- setIsProcessing(false)
- }
- }
- return (
-
- Report post
-
- What is the issue with this post?
-
-
- {error ? (
-
-
-
- ) : undefined}
- {isProcessing ? (
-
-
-
- ) : issue ? (
-
-
- Send Report
-
-
- ) : undefined}
-
- )
-}
-
-const styles = StyleSheet.create({
- title: {
- textAlign: 'center',
- fontWeight: 'bold',
- fontSize: 24,
- marginBottom: 12,
- },
- description: {
- textAlign: 'center',
- fontSize: 17,
- paddingHorizontal: 22,
- marginBottom: 10,
- },
- btn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- width: '100%',
- borderRadius: 32,
- padding: 14,
- backgroundColor: colors.gray1,
- },
-})
diff --git a/src/view/com/modals/Repost.tsx b/src/view/com/modals/Repost.tsx
index d5ed66b703..b1862ecbd9 100644
--- a/src/view/com/modals/Repost.tsx
+++ b/src/view/com/modals/Repost.tsx
@@ -18,6 +18,7 @@ export function Component({
onRepost: () => void
onQuote: () => void
isReposted: boolean
+ // TODO: Add author into component
}) {
const store = useStores()
const pal = usePalette('default')
@@ -31,7 +32,10 @@ export function Component({
+ onPress={onRepost}
+ accessibilityRole="button"
+ accessibilityLabel={isReposted ? 'Undo repost' : 'Repost'}
+ accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}>
{!isReposted ? 'Repost' : 'Undo repost'}
@@ -40,14 +44,23 @@ export function Component({
+ onPress={onQuote}
+ accessibilityRole="button"
+ accessibilityLabel="Quote post"
+ accessibilityHint="">
Quote Post
-
+
void}) {
doSelect(LOCAL_DEV_SERVICE)}>
+ onPress={() => doSelect(LOCAL_DEV_SERVICE)}
+ accessibilityRole="button">
Local dev server
void}) {
doSelect(STAGING_SERVICE)}>
+ onPress={() => doSelect(STAGING_SERVICE)}
+ accessibilityRole="button">
Staging
void}) {
) : undefined}
doSelect(PROD_SERVICE)}>
+ onPress={() => doSelect(PROD_SERVICE)}
+ accessibilityRole="button"
+ accessibilityLabel="Select Bluesky Social"
+ accessibilityHint="Sets Bluesky Social as your service provider">
Bluesky.Social
void}) {
keyboardAppearance={theme.colorScheme}
value={customUrl}
onChangeText={setCustomUrl}
+ accessibilityLabel="Custom domain"
+ // TODO: Simplify this wording further to be understandable by everyone
+ accessibilityHint="Use your domain as your Bluesky client service provider"
/>
doSelect(customUrl)}>
+ onPress={() => doSelect(customUrl)}
+ accessibilityRole="button"
+ accessibilityLabel={`Confirm service. ${
+ customUrl === ''
+ ? 'Button disabled. Input custom domain to proceed.'
+ : ''
+ }`}
+ accessibilityHint=""
+ // TODO - accessibility: Need to inform state change on failure
+ disabled={customUrl === ''}>
{error ? (
@@ -99,7 +102,10 @@ export function Component({}: {}) {
) : (
<>
-
+
-
+
Cancel
diff --git a/src/view/com/modals/crop-image/CropImage.tsx b/src/view/com/modals/crop-image/CropImage.tsx
deleted file mode 100644
index 9ac3f277fe..0000000000
--- a/src/view/com/modals/crop-image/CropImage.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * NOTE
- * This modal is used only in the web build
- * Native uses a third-party library
- */
-
-export const snapPoints = ['0%']
-
-export function Component() {
- return null
-}
diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx
index 8a9b4bf623..c5959cf4c1 100644
--- a/src/view/com/modals/crop-image/CropImage.web.tsx
+++ b/src/view/com/modals/crop-image/CropImage.web.tsx
@@ -4,12 +4,13 @@ import ImageEditor from 'react-avatar-editor'
import {Slider} from '@miblanchard/react-native-slider'
import LinearGradient from 'react-native-linear-gradient'
import {Text} from 'view/com/util/text/Text'
-import {Dimensions, Image} from 'lib/media/types'
+import {Dimensions} from 'lib/media/types'
import {getDataUriSize} from 'lib/media/util'
import {s, gradients} from 'lib/styles'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
+import {Image as RNImage} from 'react-native-image-crop-picker'
enum AspectRatio {
Square = 'square',
@@ -30,7 +31,7 @@ export function Component({
onSelect,
}: {
uri: string
- onSelect: (img?: Image) => void
+ onSelect: (img?: RNImage) => void
}) {
const store = useStores()
const pal = usePalette('default')
@@ -92,19 +93,31 @@ export function Component({
maximumValue={3}
containerStyle={styles.slider}
/>
-
+
-
+
-
+
-
+
Cancel
-
+
void
+ goBack: () => void
+ submitReport: () => void
+ isProcessing: boolean
+}) {
+ const pal = usePalette('default')
+
+ return (
+
+
+
+ Back
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ detailsContainer: {
+ marginTop: isDesktopWeb ? 0 : 12,
+ },
+ backBtn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ detailsInputContainer: {
+ borderRadius: 8,
+ },
+ detailsInput: {
+ paddingHorizontal: 12,
+ paddingTop: 12,
+ paddingBottom: 12,
+ borderRadius: 8,
+ minHeight: 100,
+ fontSize: 16,
+ },
+ detailsInputBottomBar: {
+ alignSelf: 'flex-end',
+ },
+ charCounter: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingRight: 10,
+ paddingBottom: 8,
+ },
+})
diff --git a/src/view/com/modals/report/ReportAccount.tsx b/src/view/com/modals/report/ReportAccount.tsx
new file mode 100644
index 0000000000..237f2dc5dc
--- /dev/null
+++ b/src/view/com/modals/report/ReportAccount.tsx
@@ -0,0 +1,180 @@
+import React, {useState, useMemo} from 'react'
+import {TouchableOpacity, StyleSheet, View} from 'react-native'
+import {ComAtprotoModerationDefs} from '@atproto/api'
+import {useStores} from 'state/index'
+import {s} from 'lib/styles'
+import {RadioGroup, RadioGroupItem} from '../../util/forms/RadioGroup'
+import {Text} from '../../util/text/Text'
+import * as Toast from '../../util/Toast'
+import {ErrorMessage} from '../../util/error/ErrorMessage'
+import {cleanError} from 'lib/strings/errors'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb} from 'platform/detection'
+import {SendReportButton} from './SendReportButton'
+import {InputIssueDetails} from './InputIssueDetails'
+
+export const snapPoints = [400]
+
+export function Component({did}: {did: string}) {
+ const store = useStores()
+ const pal = usePalette('default')
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [error, setError] = useState()
+ const [issue, setIssue] = useState()
+ const onSelectIssue = (v: string) => setIssue(v)
+ const [details, setDetails] = useState()
+ const [showDetailsInput, setShowDetailsInput] = useState(false)
+
+ const onPress = async () => {
+ setError('')
+ if (!issue) {
+ return
+ }
+ setIsProcessing(true)
+ try {
+ await store.agent.com.atproto.moderation.createReport({
+ reasonType: issue,
+ subject: {
+ $type: 'com.atproto.admin.defs#repoRef',
+ did,
+ },
+ reason: details,
+ })
+ Toast.show("Thank you for your report! We'll look into it promptly.")
+ store.shell.closeModal()
+ return
+ } catch (e: any) {
+ setError(cleanError(e))
+ setIsProcessing(false)
+ }
+ }
+ const goBack = () => {
+ setShowDetailsInput(false)
+ }
+ const goToDetails = () => {
+ setShowDetailsInput(true)
+ }
+
+ return (
+
+ {showDetailsInput ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+
+const SelectIssue = ({
+ onPress,
+ onSelectIssue,
+ error,
+ isProcessing,
+ goToDetails,
+}: {
+ onPress: () => void
+ onSelectIssue: (v: string) => void
+ error: string | undefined
+ isProcessing: boolean
+ goToDetails: () => void
+}) => {
+ const pal = usePalette('default')
+ const ITEMS: RadioGroupItem[] = useMemo(
+ () => [
+ {
+ key: ComAtprotoModerationDefs.REASONMISLEADING,
+ label: (
+
+
+ Misleading Account
+
+
+ Impersonation or false claims about identity or affiliation
+
+
+ ),
+ },
+ {
+ key: ComAtprotoModerationDefs.REASONSPAM,
+ label: (
+
+
+ Frequently Posts Unwanted Content
+
+
+ Spam; excessive mentions or replies
+
+
+ ),
+ },
+ ],
+ [pal],
+ )
+ return (
+ <>
+
+ Report Account
+
+
+ What is the issue with this account?
+
+
+
+ For other issues, please report specific posts.
+
+ {error ? (
+
+
+
+ ) : undefined}
+
+
+ Add details to report
+
+ >
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingHorizontal: isDesktopWeb ? 0 : 10,
+ },
+ title: {
+ textAlign: 'center',
+ fontWeight: 'bold',
+ marginBottom: 12,
+ },
+ description: {
+ textAlign: 'center',
+ paddingHorizontal: 22,
+ marginBottom: 10,
+ },
+ addDetailsBtn: {
+ padding: 14,
+ alignSelf: 'center',
+ },
+})
diff --git a/src/view/com/modals/report/ReportPost.tsx b/src/view/com/modals/report/ReportPost.tsx
new file mode 100644
index 0000000000..fe2a5bca4d
--- /dev/null
+++ b/src/view/com/modals/report/ReportPost.tsx
@@ -0,0 +1,251 @@
+import React, {useState, useMemo} from 'react'
+import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {ComAtprotoModerationDefs} from '@atproto/api'
+import {useStores} from 'state/index'
+import {s} from 'lib/styles'
+import {RadioGroup, RadioGroupItem} from '../../util/forms/RadioGroup'
+import {Text} from '../../util/text/Text'
+import * as Toast from '../../util/Toast'
+import {ErrorMessage} from '../../util/error/ErrorMessage'
+import {cleanError} from 'lib/strings/errors'
+import {usePalette} from 'lib/hooks/usePalette'
+import {SendReportButton} from './SendReportButton'
+import {InputIssueDetails} from './InputIssueDetails'
+
+const DMCA_LINK = 'https://bsky.app/support/copyright'
+
+export const snapPoints = [575]
+
+export function Component({
+ postUri,
+ postCid,
+}: {
+ postUri: string
+ postCid: string
+}) {
+ const store = useStores()
+ const pal = usePalette('default')
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [showTextInput, setShowTextInput] = useState(false)
+ const [error, setError] = useState()
+ const [issue, setIssue] = useState()
+ const [details, setDetails] = useState()
+
+ const submitReport = async () => {
+ setError('')
+ if (!issue) {
+ return
+ }
+ setIsProcessing(true)
+ try {
+ if (issue === '__copyright__') {
+ Linking.openURL(DMCA_LINK)
+ return
+ }
+ await store.agent.createModerationReport({
+ reasonType: issue,
+ subject: {
+ $type: 'com.atproto.repo.strongRef',
+ uri: postUri,
+ cid: postCid,
+ },
+ reason: details,
+ })
+ Toast.show("Thank you for your report! We'll look into it promptly.")
+
+ store.shell.closeModal()
+ return
+ } catch (e: any) {
+ setError(cleanError(e))
+ setIsProcessing(false)
+ }
+ }
+
+ const goBack = () => {
+ setShowTextInput(false)
+ }
+
+ return (
+
+ {showTextInput ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+
+const SelectIssue = ({
+ error,
+ setShowTextInput,
+ issue,
+ setIssue,
+ submitReport,
+ isProcessing,
+}: {
+ error: string | undefined
+ setShowTextInput: (v: boolean) => void
+ issue: string | undefined
+ setIssue: (v: string) => void
+ submitReport: () => void
+ isProcessing: boolean
+}) => {
+ const pal = usePalette('default')
+ const ITEMS: RadioGroupItem[] = useMemo(
+ () => [
+ {
+ key: ComAtprotoModerationDefs.REASONSPAM,
+ label: (
+
+
+ Spam
+
+ Excessive mentions or replies
+
+ ),
+ },
+ {
+ key: ComAtprotoModerationDefs.REASONSEXUAL,
+ label: (
+
+
+ Unwanted Sexual Content
+
+
+ Nudity or pornography not labeled as such
+
+
+ ),
+ },
+ {
+ key: '__copyright__',
+ label: (
+
+
+ Copyright Violation
+
+ Contains copyrighted material
+
+ ),
+ },
+ {
+ key: ComAtprotoModerationDefs.REASONRUDE,
+ label: (
+
+
+ Anti-Social Behavior
+
+
+ Harassment, trolling, or intolerance
+
+
+ ),
+ },
+ {
+ key: ComAtprotoModerationDefs.REASONVIOLATION,
+ label: (
+
+
+ Illegal and Urgent
+
+
+ Glaring violations of law or terms of service
+
+
+ ),
+ },
+ {
+ key: ComAtprotoModerationDefs.REASONOTHER,
+ label: (
+
+
+ Other
+
+
+ An issue not included in these options
+
+
+ ),
+ },
+ ],
+ [pal],
+ )
+
+ const onSelectIssue = (v: string) => setIssue(v)
+ const goToDetails = () => {
+ if (issue === '__copyright__') {
+ Linking.openURL(DMCA_LINK)
+ return
+ }
+ setShowTextInput(true)
+ }
+
+ return (
+ <>
+ Report post
+
+ What is the issue with this post?
+
+
+ {error ? (
+
+
+
+ ) : undefined}
+ {issue ? (
+ <>
+
+
+ Add details to report
+
+ >
+ ) : undefined}
+ >
+ )
+}
+
+const styles = StyleSheet.create({
+ title: {
+ textAlign: 'center',
+ fontWeight: 'bold',
+ fontSize: 24,
+ marginBottom: 12,
+ },
+ description: {
+ textAlign: 'center',
+ fontSize: 17,
+ paddingHorizontal: 22,
+ marginBottom: 10,
+ },
+ addDetailsBtn: {
+ padding: 14,
+ alignSelf: 'center',
+ },
+})
diff --git a/src/view/com/modals/report/SendReportButton.tsx b/src/view/com/modals/report/SendReportButton.tsx
new file mode 100644
index 0000000000..82fb65f20c
--- /dev/null
+++ b/src/view/com/modals/report/SendReportButton.tsx
@@ -0,0 +1,57 @@
+import React from 'react'
+import LinearGradient from 'react-native-linear-gradient'
+import {
+ ActivityIndicator,
+ StyleSheet,
+ TouchableOpacity,
+ View,
+} from 'react-native'
+import {Text} from '../../util/text/Text'
+import {s, gradients, colors} from 'lib/styles'
+
+export function SendReportButton({
+ onPress,
+ isProcessing,
+}: {
+ onPress: () => void
+ isProcessing: boolean
+}) {
+ // loading state
+ // =
+ if (isProcessing) {
+ return (
+
+
+
+ )
+ }
+ return (
+
+
+ Send Report
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ borderRadius: 32,
+ padding: 14,
+ backgroundColor: colors.gray1,
+ },
+})
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index 33bde1955c..d457d71362 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -135,8 +135,9 @@ export const Feed = observer(function Feed({
/>
)}
- {data.length && (
+ {data.length ? (
item._reactKey}
@@ -153,9 +154,10 @@ export const Feed = observer(function Feed({
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
onScroll={onScroll}
+ scrollEventThrottle={100}
contentContainerStyle={s.contentContainer}
/>
- )}
+ ) : null}
)
})
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 34df2a8edb..7994c53abc 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {useMemo, useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite'
import {
Animated,
@@ -8,7 +8,7 @@ import {
View,
} from 'react-native'
import {AppBskyEmbedImages} from '@atproto/api'
-import {AtUri, ComAtprotoLabelDefs} from '@atproto/api'
+import {AtUri} from '@atproto/api'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -26,8 +26,15 @@ import {UserAvatar} from '../util/UserAvatar'
import {ImageHorzList} from '../util/images/ImageHorzList'
import {Post} from '../post/Post'
import {Link, TextLink} from '../util/Link'
+import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {
+ getProfileViewBasicLabelInfo,
+ getProfileModeration,
+} from 'lib/labeling/helpers'
+import {ProfileModeration} from 'lib/labeling/types'
+import {formatCount} from '../util/numeric/format'
const MAX_AUTHORS = 5
@@ -38,17 +45,18 @@ interface Author {
handle: string
displayName?: string
avatar?: string
- labels?: ComAtprotoLabelDefs.Label[]
+ moderation: ProfileModeration
}
-export const FeedItem = observer(function FeedItem({
+export const FeedItem = observer(function ({
item,
}: {
item: NotificationsFeedItemModel
}) {
+ const store = useStores()
const pal = usePalette('default')
- const [isAuthorsExpanded, setAuthorsExpanded] = React.useState(false)
- const itemHref = React.useMemo(() => {
+ const [isAuthorsExpanded, setAuthorsExpanded] = useState(false)
+ const itemHref = useMemo(() => {
if (item.isLike || item.isRepost) {
const urip = new AtUri(item.subjectUri)
return `/profile/${urip.host}/post/${urip.rkey}`
@@ -60,7 +68,7 @@ export const FeedItem = observer(function FeedItem({
}
return ''
}, [item])
- const itemTitle = React.useMemo(() => {
+ const itemTitle = useMemo(() => {
if (item.isLike || item.isRepost) {
return 'Post'
} else if (item.isFollow) {
@@ -74,6 +82,33 @@ export const FeedItem = observer(function FeedItem({
setAuthorsExpanded(!isAuthorsExpanded)
}
+ const authors: Author[] = useMemo(() => {
+ return [
+ {
+ href: `/profile/${item.author.handle}`,
+ handle: item.author.handle,
+ displayName: item.author.displayName,
+ avatar: item.author.avatar,
+ moderation: getProfileModeration(
+ store,
+ getProfileViewBasicLabelInfo(item.author),
+ ),
+ },
+ ...(item.additional?.map(({author}) => {
+ return {
+ href: `/profile/${author.handle}`,
+ handle: author.handle,
+ displayName: author.displayName,
+ avatar: author.avatar,
+ moderation: getProfileModeration(
+ store,
+ getProfileViewBasicLabelInfo(author),
+ ),
+ }
+ }) || []),
+ ]
+ }, [store, item.additional, item.author])
+
if (item.additionalPost?.notFound) {
// don't render anything if the target post was deleted or unfindable
return
@@ -85,7 +120,12 @@ export const FeedItem = observer(function FeedItem({
return
}
return (
-
+
>
+ return null
}
- const authors: Author[] = [
- {
- href: `/profile/${item.author.handle}`,
- handle: item.author.handle,
- displayName: item.author.displayName,
- avatar: item.author.avatar,
- labels: item.author.labels,
- },
- ...(item.additional?.map(
- ({author: {avatar, labels, handle, displayName}}) => {
- return {
- href: `/profile/${handle}`,
- handle,
- displayName,
- avatar,
- labels,
- }
- },
- ) || []),
- ]
-
return (
+ // eslint-disable-next-line react-native-a11y/no-nested-touchables
+ noFeedback
+ accessible={(item.isLike && authors.length === 1) || item.isRepost}>
+ {/* TODO: Prevent conditional rendering and move toward composable
+ notifications for clearer accessibility labeling */}
{icon === 'HeartIconSolid' ? (
) : (
@@ -174,17 +198,18 @@ export const FeedItem = observer(function FeedItem({
1 ? onToggleAuthorsExpanded : () => {}}>
+ onPress={authors.length > 1 ? onToggleAuthorsExpanded : undefined}
+ accessible={false}>
-
+
{authors.length > 1 ? (
<>
- and
-
- {authors.length - 1} {pluralize(authors.length - 1, 'other')}
+ and
+
+ {formatCount(authors.length - 1)}{' '}
+ {pluralize(authors.length - 1, 'other')}
>
) : undefined}
- {action}
-
- {ago(item.indexedAt)}
-
-
+ {action}
+ {ago(item.indexedAt)}
+
{item.isLike || item.isRepost || item.isQuote ? (
@@ -227,7 +251,10 @@ function CondensedAuthorsList({
+ onPress={onToggleAuthorsExpanded}
+ accessibilityRole="button"
+ accessibilityLabel="Hide user list"
+ accessibilityHint="Collapses list of users for a given notification">
)
}
return (
-
- {authors.slice(0, MAX_AUTHORS).map(author => (
-
-
-
- ))}
- {authors.length > MAX_AUTHORS ? (
-
- +{authors.length - MAX_AUTHORS}
-
- ) : undefined}
-
-
+
+
+ {authors.slice(0, MAX_AUTHORS).map(author => (
+
+
+
+ ))}
+ {authors.length > MAX_AUTHORS ? (
+
+ +{authors.length - MAX_AUTHORS}
+
+ ) : undefined}
+
+
+
)
}
@@ -296,13 +328,14 @@ function ExpandedAuthorsList({
const heightStyle = {
height: Animated.multiply(heightInterp, targetHeight),
}
- React.useEffect(() => {
+ useEffect(() => {
Animated.timing(heightInterp, {
toValue: visible ? 1 : 0,
duration: 200,
useNativeDriver: false,
}).start()
}, [heightInterp, visible])
+
return (
@@ -364,10 +397,7 @@ function AdditionalPostText({
<>
{text?.length > 0 && {text} }
{images && images?.length > 0 && (
- img.thumb)}
- style={styles.additionalPostImages}
- />
+
)}
>
)
@@ -410,9 +440,6 @@ const styles = StyleSheet.create({
paddingTop: 6,
paddingBottom: 2,
},
- metaItem: {
- paddingRight: 3,
- },
postText: {
paddingBottom: 5,
color: colors.black,
diff --git a/src/view/com/pager/DraggableScrollView.tsx b/src/view/com/pager/DraggableScrollView.tsx
new file mode 100644
index 0000000000..4b7396eaa9
--- /dev/null
+++ b/src/view/com/pager/DraggableScrollView.tsx
@@ -0,0 +1,15 @@
+import {useDraggableScroll} from 'lib/hooks/useDraggableScrollView'
+import React, {ComponentProps} from 'react'
+import {ScrollView} from 'react-native'
+
+export const DraggableScrollView = React.forwardRef<
+ ScrollView,
+ ComponentProps
+>(function DraggableScrollView(props, ref) {
+ const {refs} = useDraggableScroll({
+ outerRef: ref,
+ cursor: 'grab', // optional, default
+ })
+
+ return
+})
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx
index d80b140ce2..0df915950e 100644
--- a/src/view/com/pager/FeedsTabBar.web.tsx
+++ b/src/view/com/pager/FeedsTabBar.web.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {useMemo} from 'react'
import {Animated, StyleSheet} from 'react-native'
import {observer} from 'mobx-react-lite'
import {TabBar} from 'view/com/pager/TabBar'
@@ -27,6 +27,10 @@ const FeedsTabBarDesktop = observer(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) => {
const store = useStores()
+ const items = useMemo(
+ () => ['Following', ...store.me.savedFeeds.pinnedFeedNames],
+ [store.me.savedFeeds.pinnedFeedNames],
+ )
const pal = usePalette('default')
const interp = useAnimatedValue(0)
@@ -44,13 +48,14 @@ const FeedsTabBarDesktop = observer(
{translateY: Animated.multiply(interp, -100)},
],
}
+
return (
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
@@ -63,11 +68,10 @@ const styles = StyleSheet.create({
position: 'absolute',
zIndex: 1,
left: '50%',
- width: 640,
+ width: 598,
top: 0,
flexDirection: 'row',
alignItems: 'center',
- paddingHorizontal: 18,
},
tabBarAvi: {
marginTop: 1,
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index 76e0a6fc6b..6211735679 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -1,12 +1,17 @@
-import React from 'react'
-import {Animated, StyleSheet, TouchableOpacity} from 'react-native'
+import React, {useMemo} from 'react'
+import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {TabBar} from 'view/com/pager/TabBar'
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
-import {UserAvatar} from '../util/UserAvatar'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
+import {Link} from '../util/Link'
+import {Text} from '../util/text/Text'
+import {CogIcon} from 'lib/icons'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {s} from 'lib/styles'
export const FeedsTabBar = observer(
(
@@ -28,22 +33,53 @@ export const FeedsTabBar = observer(
transform: [{translateY: Animated.multiply(interp, -100)}],
}
+ const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
+
const onPressAvi = React.useCallback(() => {
store.shell.openDrawer()
}, [store])
+ const items = useMemo(
+ () => ['Following', ...store.me.savedFeeds.pinnedFeedNames],
+ [store.me.savedFeeds.pinnedFeedNames],
+ )
+
return (
-
-
-
-
+
+
+
+
+
+
+
+
+ {store.session.isSandbox ? 'SANDBOX' : 'Bluesky'}
+
+
+
+
+
+
+
@@ -58,12 +94,20 @@ const styles = StyleSheet.create({
left: 0,
right: 0,
top: 0,
+ flexDirection: 'column',
+ alignItems: 'center',
+ borderBottomWidth: 1,
+ },
+ topBar: {
flexDirection: 'row',
+ justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 18,
+ paddingTop: 8,
+ paddingBottom: 2,
+ width: '100%',
},
- tabBarAvi: {
- marginTop: 1,
- marginRight: 18,
+ title: {
+ fontSize: 21,
},
})
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index 34747db6dd..e2c8bf6d2c 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -1,16 +1,17 @@
-import React from 'react'
+import React, {forwardRef} from 'react'
import {Animated, View} from 'react-native'
import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {s} from 'lib/styles'
export type PageSelectedEvent = PagerViewOnPageSelectedEvent
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
+export interface PagerRef {
+ setPage: (index: number) => void
+}
+
export interface RenderTabBarFnProps {
selectedPage: number
- position: Animated.Value
- offset: Animated.Value
onSelect?: (index: number) => void
}
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
@@ -22,68 +23,60 @@ interface Props {
onPageSelected?: (index: number) => void
testID?: string
}
-export const Pager = ({
- children,
- tabBarPosition = 'top',
- initialPage = 0,
- renderTabBar,
- onPageSelected,
- testID,
-}: React.PropsWithChildren) => {
- const [selectedPage, setSelectedPage] = React.useState(0)
- const position = useAnimatedValue(0)
- const offset = useAnimatedValue(0)
- const pagerView = React.useRef()
+export const Pager = forwardRef>(
+ (
+ {
+ children,
+ tabBarPosition = 'top',
+ initialPage = 0,
+ renderTabBar,
+ onPageSelected,
+ testID,
+ }: React.PropsWithChildren,
+ ref,
+ ) => {
+ const [selectedPage, setSelectedPage] = React.useState(0)
+ const pagerView = React.useRef()
- const onPageSelectedInner = React.useCallback(
- (e: PageSelectedEvent) => {
- setSelectedPage(e.nativeEvent.position)
- onPageSelected?.(e.nativeEvent.position)
- },
- [setSelectedPage, onPageSelected],
- )
+ React.useImperativeHandle(ref, () => ({
+ setPage: (index: number) => pagerView.current?.setPage(index),
+ }))
- const onTabBarSelect = React.useCallback(
- (index: number) => {
- pagerView.current?.setPage(index)
- },
- [pagerView],
- )
+ const onPageSelectedInner = React.useCallback(
+ (e: PageSelectedEvent) => {
+ setSelectedPage(e.nativeEvent.position)
+ onPageSelected?.(e.nativeEvent.position)
+ },
+ [setSelectedPage, onPageSelected],
+ )
- return (
-
- {tabBarPosition === 'top' &&
- renderTabBar({
- selectedPage,
- position,
- offset,
- onSelect: onTabBarSelect,
- })}
-
- {children}
-
- {tabBarPosition === 'bottom' &&
- renderTabBar({
- selectedPage,
- position,
- offset,
- onSelect: onTabBarSelect,
- })}
-
- )
-}
+ const onTabBarSelect = React.useCallback(
+ (index: number) => {
+ pagerView.current?.setPage(index)
+ },
+ [pagerView],
+ )
+
+ return (
+
+ {tabBarPosition === 'top' &&
+ renderTabBar({
+ selectedPage,
+ onSelect: onTabBarSelect,
+ })}
+
+ {children}
+
+ {tabBarPosition === 'bottom' &&
+ renderTabBar({
+ selectedPage,
+ onSelect: onTabBarSelect,
+ })}
+
+ )
+ },
+)
diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx
index 107497f6fb..7be2b11ec1 100644
--- a/src/view/com/pager/Pager.web.tsx
+++ b/src/view/com/pager/Pager.web.tsx
@@ -1,12 +1,9 @@
import React from 'react'
-import {Animated, View} from 'react-native'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {View} from 'react-native'
import {s} from 'lib/styles'
export interface RenderTabBarFnProps {
selectedPage: number
- position: Animated.Value
- offset: Animated.Value
onSelect?: (index: number) => void
}
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
@@ -17,53 +14,51 @@ interface Props {
renderTabBar: RenderTabBarFn
onPageSelected?: (index: number) => void
}
-export const Pager = ({
- children,
- tabBarPosition = 'top',
- initialPage = 0,
- renderTabBar,
- onPageSelected,
-}: React.PropsWithChildren) => {
- const [selectedPage, setSelectedPage] = React.useState(initialPage)
- const position = useAnimatedValue(0)
- const offset = useAnimatedValue(0)
+export const Pager = React.forwardRef(
+ (
+ {
+ children,
+ tabBarPosition = 'top',
+ initialPage = 0,
+ renderTabBar,
+ onPageSelected,
+ }: React.PropsWithChildren,
+ ref,
+ ) => {
+ const [selectedPage, setSelectedPage] = React.useState(initialPage)
- const onTabBarSelect = React.useCallback(
- (index: number) => {
- setSelectedPage(index)
- onPageSelected?.(index)
- Animated.timing(position, {
- toValue: index,
- duration: 200,
- useNativeDriver: true,
- }).start()
- },
- [setSelectedPage, onPageSelected, position],
- )
+ React.useImperativeHandle(ref, () => ({
+ setPage: (index: number) => setSelectedPage(index),
+ }))
- return (
-
- {tabBarPosition === 'top' &&
- renderTabBar({
- selectedPage,
- position,
- offset,
- onSelect: onTabBarSelect,
- })}
- {React.Children.map(children, (child, i) => (
-
- {child}
-
- ))}
- {tabBarPosition === 'bottom' &&
- renderTabBar({
- selectedPage,
- position,
- offset,
- onSelect: onTabBarSelect,
- })}
-
- )
-}
+ const onTabBarSelect = React.useCallback(
+ (index: number) => {
+ setSelectedPage(index)
+ onPageSelected?.(index)
+ },
+ [setSelectedPage, onPageSelected],
+ )
+
+ return (
+
+ {tabBarPosition === 'top' &&
+ renderTabBar({
+ selectedPage,
+ onSelect: onTabBarSelect,
+ })}
+ {React.Children.map(children, (child, i) => (
+
+ {child}
+
+ ))}
+ {tabBarPosition === 'bottom' &&
+ renderTabBar({
+ selectedPage,
+ onSelect: onTabBarSelect,
+ })}
+
+ )
+ },
+)
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index 628128e8f1..5afbb49f5e 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -1,22 +1,15 @@
-import React, {createRef, useState, useMemo, useRef} from 'react'
-import {Animated, StyleSheet, View} from 'react-native'
+import React, {useRef, useMemo, useEffect, useState, useCallback} from 'react'
+import {StyleSheet, View, ScrollView, LayoutChangeEvent} from 'react-native'
import {Text} from '../util/text/Text'
import {PressableWithHover} from '../util/PressableWithHover'
import {usePalette} from 'lib/hooks/usePalette'
-import {isDesktopWeb} from 'platform/detection'
-
-interface Layout {
- x: number
- width: number
-}
+import {isDesktopWeb, isMobileWeb} from 'platform/detection'
+import {DraggableScrollView} from './DraggableScrollView'
export interface TabBarProps {
testID?: string
selectedPage: number
items: string[]
- position: Animated.Value
- offset: Animated.Value
- indicatorPosition?: 'top' | 'bottom'
indicatorColor?: string
onSelect?: (index: number) => void
onPressSelected?: () => void
@@ -26,102 +19,74 @@ export function TabBar({
testID,
selectedPage,
items,
- position,
- offset,
- indicatorPosition = 'bottom',
indicatorColor,
onSelect,
onPressSelected,
}: TabBarProps) {
const pal = usePalette('default')
- const [itemLayouts, setItemLayouts] = useState(
- items.map(() => ({x: 0, width: 0})),
+ const scrollElRef = useRef(null)
+ const [itemXs, setItemXs] = useState([])
+ const indicatorStyle = useMemo(
+ () => ({borderBottomColor: indicatorColor || pal.colors.link}),
+ [indicatorColor, pal],
)
- const itemRefs = useMemo(
- () => Array.from({length: items.length}).map(() => createRef()),
- [items.length],
- )
- const panX = Animated.add(position, offset)
- const containerRef = useRef(null)
- const indicatorStyle = {
- backgroundColor: indicatorColor || pal.colors.link,
- bottom:
- indicatorPosition === 'bottom' ? (isDesktopWeb ? 0 : -1) : undefined,
- top: indicatorPosition === 'top' ? (isDesktopWeb ? 0 : -1) : undefined,
- transform: [
- {
- translateX: panX.interpolate({
- inputRange: items.map((_item, i) => i),
- outputRange: itemLayouts.map(l => l.x + l.width / 2),
- }),
- },
- {
- scaleX: panX.interpolate({
- inputRange: items.map((_item, i) => i),
- outputRange: itemLayouts.map(l => l.width),
- }),
- },
- ],
- }
-
- const onLayout = () => {
- const promises = []
- for (let i = 0; i < items.length; i++) {
- promises.push(
- new Promise(resolve => {
- if (!containerRef.current || !itemRefs[i].current) {
- return resolve({x: 0, width: 0})
- }
-
- itemRefs[i].current?.measureLayout(
- containerRef.current,
- (x: number, _y: number, width: number) => {
- resolve({x, width})
- },
- )
- }),
- )
- }
- Promise.all(promises).then((layouts: Layout[]) => {
- setItemLayouts(layouts)
+ // scrolls to the selected item when the page changes
+ useEffect(() => {
+ scrollElRef.current?.scrollTo({
+ x: itemXs[selectedPage] || 0,
})
- }
+ }, [scrollElRef, itemXs, selectedPage])
- const onPressItem = (index: number) => {
- onSelect?.(index)
- if (index === selectedPage) {
- onPressSelected?.()
- }
- }
+ const onPressItem = useCallback(
+ (index: number) => {
+ onSelect?.(index)
+ if (index === selectedPage) {
+ onPressSelected?.()
+ }
+ },
+ [onSelect, selectedPage, onPressSelected],
+ )
+
+ // calculates the x position of each item on mount and on layout change
+ const onItemLayout = React.useCallback(
+ (e: LayoutChangeEvent, index: number) => {
+ const x = e.nativeEvent.layout.x
+ setItemXs(prev => {
+ const Xs = [...prev]
+ Xs[index] = x
+ return Xs
+ })
+ },
+ [],
+ )
return (
-
-
- {items.map((item, i) => {
- const selected = i === selectedPage
- return (
- onPressItem(i)}>
-
- {item}
-
-
- )
- })}
+
+
+ {items.map((item, i) => {
+ const selected = i === selectedPage
+ return (
+ onItemLayout(e, i)}
+ style={[styles.item, selected && indicatorStyle]}
+ hoverStyle={pal.viewLight}
+ onPress={() => onPressItem(i)}>
+
+ {item}
+
+
+ )
+ })}
+
)
}
@@ -130,45 +95,39 @@ const styles = isDesktopWeb
? StyleSheet.create({
outer: {
flexDirection: 'row',
- paddingHorizontal: 18,
+ width: 598,
},
- itemTop: {
- paddingTop: 16,
- paddingBottom: 14,
- paddingHorizontal: 12,
+ contentContainer: {
+ columnGap: 8,
+ marginLeft: 14,
+ paddingRight: 14,
+ backgroundColor: 'transparent',
},
- itemBottom: {
+ item: {
paddingTop: 14,
- paddingBottom: 16,
- paddingHorizontal: 12,
- },
- indicator: {
- position: 'absolute',
- left: 0,
- width: 1,
- height: 3,
- zIndex: 1,
+ paddingBottom: 12,
+ paddingHorizontal: 10,
+ borderBottomWidth: 3,
+ borderBottomColor: 'transparent',
},
})
: StyleSheet.create({
outer: {
+ flex: 1,
flexDirection: 'row',
- paddingHorizontal: 14,
+ backgroundColor: 'transparent',
},
- itemTop: {
+ contentContainer: {
+ columnGap: isMobileWeb ? 0 : 20,
+ marginLeft: isMobileWeb ? 0 : 18,
+ paddingRight: isMobileWeb ? 0 : 36,
+ backgroundColor: 'transparent',
+ },
+ item: {
paddingTop: 10,
paddingBottom: 10,
- marginRight: 24,
- },
- itemBottom: {
- paddingTop: 8,
- paddingBottom: 12,
- marginRight: 24,
- },
- indicator: {
- position: 'absolute',
- left: 0,
- width: 1,
- height: 3,
+ paddingHorizontal: isMobileWeb ? 8 : 0,
+ borderBottomWidth: 3,
+ borderBottomColor: 'transparent',
},
})
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index 3ab9a279b1..80dd59072b 100644
--- a/src/view/com/post-thread/PostLikedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -47,15 +47,7 @@ export const PostLikedBy = observer(function ({uri}: {uri: string}) {
// loaded
// =
const renderItem = ({item}: {item: LikeItem}) => (
-
+
)
return (
)}
extraData={view.isLoading}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
/>
)
})
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 9874460e99..31fa0cf7fe 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -58,15 +58,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
// loaded
// =
const renderItem = ({item}: {item: RepostedByItem}) => (
-
+
)
return (
)}
extraData={view.isLoading}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
/>
)
})
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index a1e25a6adf..610b96507a 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -7,6 +7,7 @@ import {
TouchableOpacity,
View,
} from 'react-native'
+import {AppBskyFeedDefs} from '@atproto/api'
import {CenteredView, FlatList} from '../util/Views'
import {
PostThreadModel,
@@ -23,15 +24,23 @@ import {Text} from '../util/text/Text'
import {s} from 'lib/styles'
import {isDesktopWeb, isMobileWeb} from 'platform/detection'
import {usePalette} from 'lib/hooks/usePalette'
+import {useSetTitle} from 'lib/hooks/useSetTitle'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
const REPLY_PROMPT = {_reactKey: '__reply__', _isHighlightedPost: false}
+const DELETED = {_reactKey: '__deleted__', _isHighlightedPost: false}
+const BLOCKED = {_reactKey: '__blocked__', _isHighlightedPost: false}
const BOTTOM_COMPONENT = {
_reactKey: '__bottom_component__',
_isHighlightedPost: false,
}
-type YieldedItem = PostThreadItemModel | typeof REPLY_PROMPT
+type YieldedItem =
+ | PostThreadItemModel
+ | typeof REPLY_PROMPT
+ | typeof DELETED
+ | typeof BLOCKED
export const PostThread = observer(function PostThread({
uri,
@@ -52,6 +61,13 @@ export const PostThread = observer(function PostThread({
}
return []
}, [view.thread])
+ useSetTitle(
+ view.thread?.postRecord &&
+ `${sanitizeDisplayName(
+ view.thread.post.author.displayName ||
+ `@${view.thread.post.author.handle}`,
+ )}: "${view.thread?.postRecord?.text}"`,
+ )
// events
// =
@@ -103,6 +119,22 @@ export const PostThread = observer(function PostThread({
({item}: {item: YieldedItem}) => {
if (item === REPLY_PROMPT) {
return
+ } else if (item === DELETED) {
+ return (
+
+
+ Deleted post.
+
+
+ )
+ } else if (item === BLOCKED) {
+ return (
+
+
+ Blocked post.
+
+
+ )
} else if (item === BOTTOM_COMPONENT) {
// HACK
// due to some complexities with how flatlist works, this is the easiest way
@@ -130,10 +162,16 @@ export const PostThread = observer(function PostThread({
// loading
// =
- if ((view.isLoading && !view.isRefreshing) || view.params.uri !== uri) {
+ if (
+ !view.hasLoaded ||
+ (view.isLoading && !view.isRefreshing) ||
+ view.params.uri !== uri
+ ) {
return (
-
+
+
+
)
}
@@ -151,7 +189,11 @@ export const PostThread = observer(function PostThread({
The post may have been deleted.
-
+
)
}
+ if (view.isBlocked) {
+ return (
+
+
+
+ Post hidden
+
+
+ You have blocked the author or you have been blocked by the author.
+
+
+
+
+ Back
+
+
+
+
+ )
+ }
// loaded
// =
@@ -202,8 +272,10 @@ function* flattenThread(
isAscending = false,
): Generator {
if (post.parent) {
- if ('notFound' in post.parent && post.parent.notFound) {
- // TODO render not found
+ if (AppBskyFeedDefs.isNotFoundPost(post.parent)) {
+ yield DELETED
+ } else if (AppBskyFeedDefs.isBlockedPost(post.parent)) {
+ yield BLOCKED
} else {
yield* flattenThread(post.parent as PostThreadItemModel, true)
}
@@ -214,8 +286,8 @@ function* flattenThread(
}
if (post.replies?.length) {
for (const reply of post.replies) {
- if ('notFound' in reply && reply.notFound) {
- // TODO render not found
+ if (AppBskyFeedDefs.isNotFoundPost(reply)) {
+ yield DELETED
} else {
yield* flattenThread(reply as PostThreadItemModel)
}
@@ -232,6 +304,11 @@ const styles = StyleSheet.create({
paddingVertical: 14,
borderRadius: 6,
},
+ missingItem: {
+ borderTop: 1,
+ paddingHorizontal: 18,
+ paddingVertical: 18,
+ },
bottomBorder: {
borderBottomWidth: 1,
},
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 6e8758f7e2..8ccdfdb06e 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {useCallback, useMemo} from 'react'
import {observer} from 'mobx-react-lite'
import {Linking, StyleSheet, View} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
@@ -15,17 +15,20 @@ import {PostDropdownBtn} from '../util/forms/DropdownButton'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles'
-import {ago} from 'lib/strings/time'
+import {ago, niceDate} from 'lib/strings/time'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {pluralize} from 'lib/strings/helpers'
import {useStores} from 'state/index'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds'
-import {PostCtrls} from '../util/PostCtrls'
+import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostHider} from '../util/moderation/PostHider'
import {ContentHider} from '../util/moderation/ContentHider'
+import {ImageHider} from '../util/moderation/ImageHider'
+import {PostSandboxWarning} from '../util/PostSandboxWarning'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
+import {formatCount} from '../util/numeric/format'
const PARENT_REPLY_LINE_LENGTH = 8
@@ -77,25 +80,49 @@ export const PostThreadItem = observer(function PostThreadItem({
onPost: onPostReply,
})
}, [store, item, record, onPostReply])
+
const onPressToggleRepost = React.useCallback(() => {
return item
.toggleRepost()
.catch(e => store.log.error('Failed to toggle repost', e))
}, [item, store])
+
const onPressToggleLike = React.useCallback(() => {
return item
.toggleLike()
.catch(e => store.log.error('Failed to toggle like', e))
}, [item, store])
+
const onCopyPostText = React.useCallback(() => {
Clipboard.setString(record?.text || '')
Toast.show('Copied to clipboard')
}, [record])
+
+ const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
+
const onOpenTranslate = React.useCallback(() => {
Linking.openURL(
- encodeURI(`https://translate.google.com/#auto|en|${record?.text || ''}`),
+ encodeURI(
+ `https://translate.google.com/?sl=auto&tl=${primaryLanguage}&text=${
+ record?.text || ''
+ }`,
+ ),
)
- }, [record])
+ }, [record, primaryLanguage])
+
+ const onToggleThreadMute = React.useCallback(async () => {
+ try {
+ await item.toggleThreadMute()
+ if (item.isThreadMuted) {
+ Toast.show('You will no longer received notifications for this thread')
+ } else {
+ Toast.show('You will now receive notifications for this thread')
+ }
+ } catch (e) {
+ store.log.error('Failed to toggle thread mute', e)
+ }
+ }, [item, store])
+
const onDeletePost = React.useCallback(() => {
item.delete().then(
() => {
@@ -109,6 +136,40 @@ export const PostThreadItem = observer(function PostThreadItem({
)
}, [item, store])
+ const accessibilityActions = useMemo(
+ () => [
+ {
+ name: 'reply',
+ label: 'Reply',
+ },
+ {
+ name: 'repost',
+ label: item.post.viewer?.repost ? 'Undo repost' : 'Repost',
+ },
+ {name: 'like', label: item.post.viewer?.like ? 'Unlike' : 'Like'},
+ ],
+ [item.post.viewer?.like, item.post.viewer?.repost],
+ )
+
+ const onAccessibilityAction = useCallback(
+ event => {
+ switch (event.nativeEvent.actionName) {
+ case 'like':
+ onPressToggleLike()
+ break
+ case 'reply':
+ onPressReply()
+ break
+ case 'repost':
+ onPressToggleRepost()
+ break
+ default:
+ break
+ }
+ },
+ [onPressReply, onPressToggleLike, onPressToggleRepost],
+ )
+
if (!record) {
return
}
@@ -127,21 +188,25 @@ export const PostThreadItem = observer(function PostThreadItem({
if (item._isHighlightedPost) {
return (
-
+ style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
+ moderation={item.moderation.thread}
+ accessibilityActions={accessibilityActions}
+ onAccessibilityAction={onAccessibilityAction}>
+
-
+
@@ -169,19 +234,21 @@ export const PostThreadItem = observer(function PostThreadItem({
@@ -198,9 +265,7 @@ export const PostThreadItem = observer(function PostThreadItem({
-
+
{item.richText?.text ? (
) : undefined}
-
+
+
+
- {item._isHighlightedPost && hasEngagement ? (
+
+ {niceDate(item.post.indexedAt)}
+
+ {hasEngagement ? (
{item.post.repostCount ? (
- {item.post.repostCount}
+ {formatCount(item.post.repostCount)}
{' '}
{pluralize(item.post.repostCount, 'repost')}
@@ -240,7 +310,7 @@ export const PostThreadItem = observer(function PostThreadItem({
title={likesTitle}>
- {item.post.likeCount}
+ {formatCount(item.post.likeCount)}
{' '}
{pluralize(item.post.likeCount, 'like')}
@@ -269,16 +339,18 @@ export const PostThreadItem = observer(function PostThreadItem({
isAuthor={item.post.author.did === store.me.did}
isReposted={!!item.post.viewer?.repost}
isLiked={!!item.post.viewer?.like}
+ isThreadMuted={item.isThreadMuted}
onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost}
onPressToggleLike={onPressToggleLike}
onCopyPostText={onCopyPostText}
onOpenTranslate={onOpenTranslate}
+ onToggleThreadMute={onToggleThreadMute}
onDeletePost={onDeletePost}
/>
-
+
)
} else {
return (
@@ -286,9 +358,15 @@ export const PostThreadItem = observer(function PostThreadItem({
+ style={[
+ styles.outer,
+ pal.border,
+ pal.view,
+ item._showParentReplyLine && styles.noTopBorder,
+ ]}
+ moderation={item.moderation.thread}
+ accessibilityActions={accessibilityActions}
+ onAccessibilityAction={onAccessibilityAction}>
{item._showParentReplyLine && (
)}
+
@@ -325,7 +404,7 @@ export const PostThreadItem = observer(function PostThreadItem({
did={item.post.author.did}
/>
{item.richText?.text ? (
@@ -337,7 +416,9 @@ export const PostThreadItem = observer(function PostThreadItem({
/>
) : undefined}
-
+
+
+
@@ -400,6 +483,9 @@ const styles = StyleSheet.create({
paddingLeft: 6,
paddingRight: 6,
},
+ noTopBorder: {
+ borderTopWidth: 0,
+ },
parentReplyLine: {
position: 'absolute',
left: 44,
@@ -418,10 +504,10 @@ const styles = StyleSheet.create({
flexDirection: 'row',
},
layoutAvi: {
- width: 70,
paddingLeft: 10,
paddingTop: 10,
paddingBottom: 10,
+ marginRight: 10,
},
layoutContent: {
flex: 1,
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 60d46f5cc5..d37c43a3d4 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -1,4 +1,4 @@
-import React, {useState, useEffect} from 'react'
+import React, {useCallback, useEffect, useMemo, useState} from 'react'
import {
ActivityIndicator,
Linking,
@@ -20,9 +20,10 @@ import {Link} from '../util/Link'
import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds'
-import {PostCtrls} from '../util/PostCtrls'
+import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostHider} from '../util/moderation/PostHider'
import {ContentHider} from '../util/moderation/ContentHider'
+import {ImageHider} from '../util/moderation/ImageHider'
import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText'
import * as Toast from '../util/Toast'
@@ -166,13 +167,32 @@ const PostLoaded = observer(
Toast.show('Copied to clipboard')
}, [record])
+ const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
+
const onOpenTranslate = React.useCallback(() => {
Linking.openURL(
encodeURI(
- `https://translate.google.com/#auto|en|${record?.text || ''}`,
+ `https://translate.google.com/?sl=auto&tl=${primaryLanguage}&text=${
+ record?.text || ''
+ }`,
),
)
- }, [record])
+ }, [record, primaryLanguage])
+
+ const onToggleThreadMute = React.useCallback(async () => {
+ try {
+ await item.toggleThreadMute()
+ if (item.isThreadMuted) {
+ Toast.show(
+ 'You will no longer received notifications for this thread',
+ )
+ } else {
+ Toast.show('You will now receive notifications for this thread')
+ }
+ } catch (e) {
+ store.log.error('Failed to toggle thread mute', e)
+ }
+ }, [item, store])
const onDeletePost = React.useCallback(() => {
item.delete().then(
@@ -187,12 +207,47 @@ const PostLoaded = observer(
)
}, [item, setDeleted, store])
+ const accessibilityActions = useMemo(
+ () => [
+ {
+ name: 'reply',
+ label: 'Reply',
+ },
+ {
+ name: 'repost',
+ label: item.post.viewer?.repost ? 'Undo repost' : 'Repost',
+ },
+ {name: 'like', label: item.post.viewer?.like ? 'Unlike' : 'Like'},
+ ],
+ [item.post.viewer?.like, item.post.viewer?.repost],
+ )
+
+ const onAccessibilityAction = useCallback(
+ event => {
+ switch (event.nativeEvent.actionName) {
+ case 'like':
+ onPressToggleLike()
+ break
+ case 'reply':
+ onPressReply()
+ break
+ case 'repost':
+ onPressToggleRepost()
+ break
+ default:
+ break
+ }
+ },
+ [onPressReply, onPressToggleLike, onPressToggleRepost],
+ )
+
return (
+ moderation={item.moderation.list}
+ accessibilityActions={accessibilityActions}
+ onAccessibilityAction={onAccessibilityAction}>
{showReplyLine && }
@@ -200,7 +255,7 @@ const PostLoaded = observer(
@@ -220,30 +275,37 @@ const PostLoaded = observer(
size={9}
style={[pal.textLight, s.mr5]}
/>
-
- Reply to
-
-
+ style={[pal.textLight, s.mr2]}
+ lineHeight={1.2}
+ numberOfLines={1}>
+ Reply to{' '}
+
+
)}
{item.richText?.text ? (
) : undefined}
-
+
+
+
diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx
deleted file mode 100644
index 1a56a5dbf0..0000000000
--- a/src/view/com/post/PostText.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, {useState, useEffect} from 'react'
-import {observer} from 'mobx-react-lite'
-import {StyleProp, StyleSheet, TextStyle, View} from 'react-native'
-import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {Text} from '../util/text/Text'
-import {PostModel} from 'state/models/content/post'
-import {useStores} from 'state/index'
-
-export const PostText = observer(function PostText({
- uri,
- style,
-}: {
- uri: string
- style?: StyleProp
-}) {
- const store = useStores()
- const [model, setModel] = useState()
-
- useEffect(() => {
- if (model?.uri === uri) {
- return // no change needed? or trigger refresh?
- }
- const newModel = new PostModel(store, uri)
- setModel(newModel)
- newModel.setup().catch(err => store.log.error('Failed to fetch post', err))
- }, [uri, model?.uri, store])
-
- // loading
- // =
- if (!model || model.isLoading || model.uri !== uri) {
- return (
-
-
-
-
-
- )
- }
-
- // error
- // =
- if (model.hasError) {
- return (
-
-
-
- )
- }
-
- // loaded
- // =
- return (
-
- {model.text}
-
- )
-})
-
-const styles = StyleSheet.create({
- mt6: {marginTop: 6},
-})
diff --git a/src/view/com/posts/CustomFeedEmptyState.tsx b/src/view/com/posts/CustomFeedEmptyState.tsx
new file mode 100644
index 0000000000..e83a94f03c
--- /dev/null
+++ b/src/view/com/posts/CustomFeedEmptyState.tsx
@@ -0,0 +1,86 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {useNavigation} from '@react-navigation/native'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {Text} from '../util/text/Text'
+import {Button} from '../util/forms/Button'
+import {MagnifyingGlassIcon} from 'lib/icons'
+import {NavigationProp} from 'lib/routes/types'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+import {isWeb} from 'platform/detection'
+
+export function CustomFeedEmptyState() {
+ const pal = usePalette('default')
+ const palInverted = usePalette('inverted')
+ const navigation = useNavigation()
+
+ const onPressFindAccounts = React.useCallback(() => {
+ if (isWeb) {
+ navigation.navigate('Search', {})
+ } else {
+ navigation.navigate('SearchTab')
+ navigation.popToTop()
+ }
+ }, [navigation])
+
+ return (
+
+
+
+
+
+ This feed is empty! You may need to follow more users or tune your
+ language settings.
+
+
+
+ Find accounts to follow
+
+
+
+
+ )
+}
+const styles = StyleSheet.create({
+ emptyContainer: {
+ height: '100%',
+ paddingVertical: 40,
+ paddingHorizontal: 30,
+ },
+ emptyIconContainer: {
+ marginBottom: 16,
+ },
+ emptyIcon: {
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ },
+ emptyBtn: {
+ marginVertical: 20,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 18,
+ paddingHorizontal: 24,
+ borderRadius: 30,
+ },
+
+ feedsTip: {
+ position: 'absolute',
+ left: 22,
+ },
+ feedsTipArrow: {
+ marginLeft: 32,
+ marginTop: 8,
+ },
+})
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 23944a08ef..921f231907 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -18,6 +18,7 @@ import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {s} from 'lib/styles'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -31,9 +32,12 @@ export const Feed = observer(function Feed({
scrollElRef,
onPressTryAgain,
onScroll,
+ scrollEventThrottle,
renderEmptyState,
testID,
headerOffset = 0,
+ ListHeaderComponent,
+ extraData,
}: {
feed: PostsFeedModel
style?: StyleProp
@@ -41,11 +45,15 @@ export const Feed = observer(function Feed({
scrollElRef?: MutableRefObject | null>
onPressTryAgain?: () => void
onScroll?: OnScrollCb
+ scrollEventThrottle?: number
renderEmptyState?: () => JSX.Element
testID?: string
headerOffset?: number
+ ListHeaderComponent?: () => JSX.Element
+ extraData?: any
}) {
const pal = usePalette('default')
+ const theme = useTheme()
const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false)
@@ -163,6 +171,7 @@ export const Feed = observer(function Feed({
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListFooterComponent={FeedFooter}
+ ListHeaderComponent={ListHeaderComponent}
refreshControl={
)}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 983962f5ed..18c32b8997 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -1,4 +1,4 @@
-import React, {useMemo, useState} from 'react'
+import React, {useCallback, useMemo, useState} from 'react'
import {observer} from 'mobx-react-lite'
import {Linking, StyleSheet, View} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
@@ -7,16 +7,19 @@ import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
-import {PostsFeedItemModel} from 'state/models/feeds/posts'
+import {PostsFeedItemModel} from 'state/models/feeds/post'
+import {ModerationBehaviorCode} from 'lib/labeling/types'
import {Link, DesktopWebTextLink} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
-import {PostCtrls} from '../util/PostCtrls'
+import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds'
import {PostHider} from '../util/moderation/PostHider'
import {ContentHider} from '../util/moderation/ContentHider'
+import {ImageHider} from '../util/moderation/ImageHider'
import {RichText} from '../util/text/RichText'
+import {PostSandboxWarning} from '../util/PostSandboxWarning'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles'
@@ -95,11 +98,31 @@ export const FeedItem = observer(function ({
Toast.show('Copied to clipboard')
}, [record])
+ const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
+
const onOpenTranslate = React.useCallback(() => {
Linking.openURL(
- encodeURI(`https://translate.google.com/#auto|en|${record?.text || ''}`),
+ encodeURI(
+ `https://translate.google.com/?sl=auto&tl=${primaryLanguage}&text=${
+ record?.text || ''
+ }`,
+ ),
)
- }, [record])
+ }, [record, primaryLanguage])
+
+ const onToggleThreadMute = React.useCallback(async () => {
+ track('FeedItem:ThreadMute')
+ try {
+ await item.toggleThreadMute()
+ if (item.isThreadMuted) {
+ Toast.show('You will no longer receive notifications for this thread')
+ } else {
+ Toast.show('You will now receive notifications for this thread')
+ }
+ } catch (e) {
+ store.log.error('Failed to toggle thread mute', e)
+ }
+ }, [track, item, store])
const onDeletePost = React.useCallback(() => {
track('FeedItem:PostDelete')
@@ -115,30 +138,71 @@ export const FeedItem = observer(function ({
)
}, [track, item, setDeleted, store])
- if (!record || deleted) {
- return
- }
-
const isSmallTop = isThreadChild
- const isNoTop = false //isChild && !item._isThreadChild
- const isMuted =
- item.post.author.viewer?.muted && ignoreMuteFor !== item.post.author.did
const outerStyles = [
styles.outer,
pal.view,
{borderColor: pal.colors.border},
isSmallTop ? styles.outerSmallTop : undefined,
- isNoTop ? styles.outerNoTop : undefined,
isThreadParent ? styles.outerNoBottom : undefined,
]
+ // moderation override
+ let moderation = item.moderation.list
+ if (
+ ignoreMuteFor === item.post.author.did &&
+ moderation.isMute &&
+ !moderation.noOverride
+ ) {
+ moderation = {behavior: ModerationBehaviorCode.Show}
+ }
+
+ const accessibilityActions = useMemo(
+ () => [
+ {
+ name: 'reply',
+ label: 'Reply',
+ },
+ {
+ name: 'repost',
+ label: item.post.viewer?.repost ? 'Undo repost' : 'Repost',
+ },
+ {name: 'like', label: item.post.viewer?.like ? 'Unlike' : 'Like'},
+ ],
+ [item.post.viewer?.like, item.post.viewer?.repost],
+ )
+
+ const onAccessibilityAction = useCallback(
+ event => {
+ switch (event.nativeEvent.actionName) {
+ case 'like':
+ onPressToggleLike()
+ break
+ case 'reply':
+ onPressReply()
+ break
+ case 'repost':
+ onPressToggleRepost()
+ break
+ default:
+ break
+ }
+ },
+ [onPressReply, onPressToggleLike, onPressToggleRepost],
+ )
+
+ if (!record || deleted) {
+ return
+ }
+
return (
+ moderation={moderation}
+ accessibilityActions={accessibilityActions}
+ onAccessibilityAction={onAccessibilityAction}>
{isThreadChild && (
)}
{item.reasonRepost && (
@@ -186,13 +246,14 @@ export const FeedItem = observer(function ({
)}
+
@@ -216,19 +277,23 @@ export const FeedItem = observer(function ({
s.mr5,
]}
/>
-
- Reply to
-
-
+ style={[pal.textLight, s.mr2]}
+ lineHeight={1.2}
+ numberOfLines={1}>
+ Reply to{' '}
+
+
)}
{item.richText?.text ? (
@@ -239,7 +304,9 @@ export const FeedItem = observer(function ({
/>
) : undefined}
-
+
+
+
@@ -280,10 +349,6 @@ const styles = StyleSheet.create({
paddingRight: 15,
paddingBottom: 8,
},
- outerNoTop: {
- borderTopWidth: 0,
- paddingTop: 0,
- },
outerSmallTop: {
borderTopWidth: 0,
},
@@ -304,7 +369,6 @@ const styles = StyleSheet.create({
bottom: 0,
borderLeftWidth: 2,
},
- bottomReplyLineNoTop: {top: 64},
includeReason: {
flexDirection: 'row',
paddingLeft: 50,
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index 651b69bff1..888466200a 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -1,12 +1,13 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
-import {PostsFeedSliceModel} from 'state/models/feeds/posts'
+import {PostsFeedSliceModel} from 'state/models/feeds/post'
import {AtUri} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import Svg, {Circle, Line} from 'react-native-svg'
import {FeedItem} from './FeedItem'
import {usePalette} from 'lib/hooks/usePalette'
+import {ModerationBehaviorCode} from 'lib/labeling/types'
export function FeedSlice({
slice,
@@ -17,6 +18,11 @@ export function FeedSlice({
showFollowBtn?: boolean
ignoreMuteFor?: string
}) {
+ if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) {
+ if (!ignoreMuteFor && !slice.moderation.list.noOverride) {
+ return null
+ }
+ }
if (slice.isThread && slice.items.length > 3) {
const last = slice.items.length - 1
return (
diff --git a/src/view/com/posts/FollowingEmptyState.tsx b/src/view/com/posts/FollowingEmptyState.tsx
index acd035f21d..d1843900b4 100644
--- a/src/view/com/posts/FollowingEmptyState.tsx
+++ b/src/view/com/posts/FollowingEmptyState.tsx
@@ -11,6 +11,7 @@ import {MagnifyingGlassIcon} from 'lib/icons'
import {NavigationProp} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
+import {isWeb} from 'platform/detection'
export function FollowingEmptyState() {
const pal = usePalette('default')
@@ -18,8 +19,12 @@ export function FollowingEmptyState() {
const navigation = useNavigation()
const onPressFindAccounts = React.useCallback(() => {
- navigation.navigate('SearchTab')
- navigation.popToTop()
+ if (isWeb) {
+ navigation.navigate('Search', {})
+ } else {
+ navigation.navigate('SearchTab')
+ navigation.popToTop()
+ }
}, [navigation])
return (
@@ -48,7 +53,6 @@ export function FollowingEmptyState() {
}
const styles = StyleSheet.create({
emptyContainer: {
- // flex: 1,
height: '100%',
paddingVertical: 40,
paddingHorizontal: 30,
diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx
new file mode 100644
index 0000000000..db353909cc
--- /dev/null
+++ b/src/view/com/posts/MultiFeed.tsx
@@ -0,0 +1,246 @@
+import React, {MutableRefObject} from 'react'
+import {observer} from 'mobx-react-lite'
+import {
+ ActivityIndicator,
+ RefreshControl,
+ StyleProp,
+ StyleSheet,
+ View,
+ ViewStyle,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {FlatList} from '../util/Views'
+import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {PostsMultiFeedModel, MultiFeedItem} from 'state/models/feeds/multi-feed'
+import {FeedSlice} from './FeedSlice'
+import {Text} from '../util/text/Text'
+import {Link} from '../util/Link'
+import {UserAvatar} from '../util/UserAvatar'
+import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {s} from 'lib/styles'
+import {useAnalytics} from 'lib/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
+import {isDesktopWeb} from 'platform/detection'
+import {CogIcon} from 'lib/icons'
+
+export const MultiFeed = observer(function Feed({
+ multifeed,
+ style,
+ showPostFollowBtn,
+ scrollElRef,
+ onScroll,
+ scrollEventThrottle,
+ testID,
+ headerOffset = 0,
+ extraData,
+}: {
+ multifeed: PostsMultiFeedModel
+ style?: StyleProp
+ showPostFollowBtn?: boolean
+ scrollElRef?: MutableRefObject | null>
+ onPressTryAgain?: () => void
+ onScroll?: OnScrollCb
+ scrollEventThrottle?: number
+ renderEmptyState?: () => JSX.Element
+ testID?: string
+ headerOffset?: number
+ extraData?: any
+}) {
+ const pal = usePalette('default')
+ const theme = useTheme()
+ const {track} = useAnalytics()
+ const [isRefreshing, setIsRefreshing] = React.useState(false)
+
+ // events
+ // =
+
+ const onRefresh = React.useCallback(async () => {
+ track('MultiFeed:onRefresh')
+ setIsRefreshing(true)
+ try {
+ await multifeed.refresh()
+ } catch (err) {
+ multifeed.rootStore.log.error('Failed to refresh posts feed', err)
+ }
+ setIsRefreshing(false)
+ }, [multifeed, track, setIsRefreshing])
+
+ const onEndReached = React.useCallback(async () => {
+ track('MultiFeed:onEndReached')
+ try {
+ await multifeed.loadMore()
+ } catch (err) {
+ multifeed.rootStore.log.error('Failed to load more posts', err)
+ }
+ }, [multifeed, track])
+
+ // rendering
+ // =
+
+ const renderItem = React.useCallback(
+ ({item}: {item: MultiFeedItem}) => {
+ if (item.type === 'header') {
+ if (isDesktopWeb) {
+ return (
+
+
+ My Feeds
+
+
+
+
+
+ )
+ }
+ return
+ } else if (item.type === 'feed-header') {
+ return (
+
+
+
+ {item.title}
+
+
+ )
+ } else if (item.type === 'feed-slice') {
+ return (
+
+ )
+ } else if (item.type === 'feed-loading') {
+ return
+ } else if (item.type === 'feed-error') {
+ return
+ } else if (item.type === 'feed-footer') {
+ return (
+
+
+ See more from {item.title}
+
+
+
+ )
+ } else if (item.type === 'footer') {
+ return (
+
+
+
+ Discover new feeds
+
+
+ )
+ }
+ return null
+ },
+ [showPostFollowBtn, pal],
+ )
+
+ const FeedFooter = React.useCallback(
+ () =>
+ multifeed.isLoading && !isRefreshing ? (
+
+
+
+ ) : (
+
+ ),
+ [multifeed.isLoading, isRefreshing, pal],
+ )
+
+ return (
+
+ {multifeed.items.length > 0 && (
+ item._reactKey}
+ renderItem={renderItem}
+ ListFooterComponent={FeedFooter}
+ refreshControl={
+
+ }
+ contentContainerStyle={s.contentContainer}
+ style={[{paddingTop: headerOffset}, pal.view, styles.container]}
+ onScroll={onScroll}
+ scrollEventThrottle={scrollEventThrottle}
+ indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
+ onEndReached={onEndReached}
+ onEndReachedThreshold={0.6}
+ removeClippedSubviews={true}
+ contentOffset={{x: 0, y: headerOffset * -1}}
+ extraData={extraData}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
+ />
+ )}
+
+ )
+})
+
+const styles = StyleSheet.create({
+ container: {
+ height: '100%',
+ },
+ header: {
+ borderTopWidth: 1,
+ marginBottom: 4,
+ },
+ headerDesktop: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderBottomWidth: 1,
+ marginBottom: 4,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
+ feedHeader: {
+ flexDirection: 'row',
+ gap: 8,
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingBottom: 8,
+ marginTop: 12,
+ },
+ feedHeaderTitle: {
+ fontWeight: 'bold',
+ },
+ feedFooter: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ marginBottom: 12,
+ borderTopWidth: 1,
+ borderBottomWidth: 1,
+ },
+ footerLink: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 8,
+ paddingHorizontal: 14,
+ paddingVertical: 12,
+ marginHorizontal: 8,
+ marginBottom: 8,
+ gap: 8,
+ },
+ loadMore: {
+ paddingTop: 10,
+ },
+})
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index 7715358cf2..fcb2225daa 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -33,7 +33,7 @@ export const FollowButton = observer(
store.me.follows.removeFollow(did)
onToggleFollow?.(false)
} catch (e: any) {
- store.log.error('Failed fo delete follow', e)
+ store.log.error('Failed to delete follow', e)
Toast.show('An issue occurred, please try again.')
}
} else if (updatedFollowState === FollowState.NotFollowing) {
@@ -42,7 +42,7 @@ export const FollowButton = observer(
store.me.follows.addFollow(did, res.uri)
onToggleFollow?.(true)
} catch (e: any) {
- store.log.error('Failed fo create follow', e)
+ store.log.error('Failed to create follow', e)
Toast.show('An issue occurred, please try again.')
}
}
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 07bf4e291c..50b9f199c3 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -1,7 +1,7 @@
-import React from 'react'
+import * as React from 'react'
import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
-import {AppBskyActorDefs, ComAtprotoLabelDefs} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
@@ -10,143 +10,167 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {FollowButton} from './FollowButton'
import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {
+ getProfileViewBasicLabelInfo,
+ getProfileModeration,
+} from 'lib/labeling/helpers'
+import {ModerationBehaviorCode} from 'lib/labeling/types'
-export function ProfileCard({
- testID,
- handle,
- displayName,
- avatar,
- description,
- labels,
- isFollowedBy,
- noBg,
- noBorder,
- followers,
- renderButton,
-}: {
- testID?: string
- handle: string
- displayName?: string
- avatar?: string
- description?: string
- labels: ComAtprotoLabelDefs.Label[] | undefined
- isFollowedBy?: boolean
- noBg?: boolean
- noBorder?: boolean
- followers?: AppBskyActorDefs.ProfileView[] | undefined
- renderButton?: () => JSX.Element
-}) {
- const pal = usePalette('default')
- return (
-
-
-
-
-
-
-
- {sanitizeDisplayName(displayName || handle)}
-
-
- @{handle}
-
- {isFollowedBy && (
-
-
-
- Follows You
-
+export const ProfileCard = observer(
+ ({
+ testID,
+ profile,
+ noBg,
+ noBorder,
+ followers,
+ overrideModeration,
+ renderButton,
+ }: {
+ testID?: string
+ profile: AppBskyActorDefs.ProfileViewBasic
+ noBg?: boolean
+ noBorder?: boolean
+ followers?: AppBskyActorDefs.ProfileView[] | undefined
+ overrideModeration?: boolean
+ renderButton?: (
+ profile: AppBskyActorDefs.ProfileViewBasic,
+ ) => React.ReactNode
+ }) => {
+ const store = useStores()
+ const pal = usePalette('default')
+
+ const moderation = getProfileModeration(
+ store,
+ getProfileViewBasicLabelInfo(profile),
+ )
+
+ if (
+ moderation.list.behavior === ModerationBehaviorCode.Hide &&
+ !overrideModeration
+ ) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+
+
+ {sanitizeDisplayName(profile.displayName || profile.handle)}
+
+
+ @{profile.handle}
+
+ {!!profile.viewer?.followedBy && (
+
+
+
+ Follows You
+
+
-
- )}
+ )}
+
+ {renderButton ? (
+ {renderButton(profile)}
+ ) : undefined}
- {renderButton ? (
- {renderButton()}
+ {profile.description ? (
+
+
+ {profile.description}
+
+
) : undefined}
-
- {description ? (
-
-
- {description}
-
-
- ) : undefined}
- {followers?.length ? (
-
-
- Followed by{' '}
- {followers.map(f => f.displayName || f.handle).join(', ')}
-
- {followers.slice(0, 3).map(f => (
-
-
-
-
+
+
+ )
+ },
+)
+
+const FollowersList = observer(
+ ({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => {
+ const store = useStores()
+ const pal = usePalette('default')
+ if (!followers?.length) {
+ return null
+ }
+
+ const followersWithMods = followers
+ .map(f => ({
+ f,
+ mod: getProfileModeration(store, getProfileViewBasicLabelInfo(f)),
+ }))
+ .filter(({mod}) => mod.list.behavior !== ModerationBehaviorCode.Hide)
+
+ return (
+
+
+ Followed by{' '}
+ {followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')}
+
+ {followersWithMods.slice(0, 3).map(({f, mod}) => (
+
+
+
- ))}
-
- ) : undefined}
-
- )
-}
+
+ ))}
+
+ )
+ },
+)
export const ProfileCardWithFollowBtn = observer(
({
- did,
- handle,
- displayName,
- avatar,
- description,
- labels,
- isFollowedBy,
+ profile,
noBg,
noBorder,
followers,
}: {
- did: string
- handle: string
- displayName?: string
- avatar?: string
- description?: string
- labels: ComAtprotoLabelDefs.Label[] | undefined
- isFollowedBy?: boolean
+ profile: AppBskyActorDefs.ProfileViewBasic
noBg?: boolean
noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined
}) => {
const store = useStores()
- const isMe = store.me.handle === handle
+ const isMe = store.me.handle === profile.handle
return (
}
+ renderButton={
+ isMe ? undefined : () =>
+ }
/>
)
},
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index db592075ac..aeb2fcba91 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -61,15 +61,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
// loaded
// =
const renderItem = ({item}: {item: FollowerItem}) => (
-
+
)
return (
)}
extraData={view.isLoading}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
/>
)
})
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 10da79c5e7..0632fac02b 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -58,15 +58,7 @@ export const ProfileFollows = observer(function ProfileFollows({
// loaded
// =
const renderItem = ({item}: {item: FollowItem}) => (
-
+
)
return (
)}
extraData={view.isLoading}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
/>
)
})
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 55f194d596..af13901d41 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -1,7 +1,6 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {
- Share,
StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback,
@@ -24,16 +23,19 @@ import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton'
import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text'
+import {TextLink} from '../util/Link'
import {RichText} from '../util/text/RichText'
import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner'
-import {ProfileHeaderLabels} from '../util/moderation/ProfileHeaderLabels'
+import {ProfileHeaderWarnings} from '../util/moderation/ProfileHeaderWarnings'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics'
import {NavigationProp} from 'lib/routes/types'
-import {isAndroid, isDesktopWeb, isIOS} from 'platform/detection'
+import {listUriToHref} from 'lib/strings/url-helpers'
+import {isDesktopWeb, isNative} from 'platform/detection'
import {FollowState} from 'state/models/cache/my-follows'
-import Clipboard from '@react-native-clipboard/clipboard'
+import {shareUrl} from 'lib/sharing'
+import {formatCount} from '../util/numeric/format'
const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
@@ -97,289 +99,428 @@ export const ProfileHeader = observer(
},
)
-const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
- view,
- onRefreshAll,
- hideBackButton = false,
-}: Props) {
- const pal = usePalette('default')
- const store = useStores()
- const navigation = useNavigation()
- const {track} = useAnalytics()
+const ProfileHeaderLoaded = observer(
+ ({view, onRefreshAll, hideBackButton = false}: Props) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const navigation = useNavigation()
+ const {track} = useAnalytics()
- const onPressBack = React.useCallback(() => {
- navigation.goBack()
- }, [navigation])
+ const onPressBack = React.useCallback(() => {
+ navigation.goBack()
+ }, [navigation])
- const onPressAvi = React.useCallback(() => {
- if (view.avatar) {
- store.shell.openLightbox(new ProfileImageLightbox(view))
- }
- }, [store, view])
+ const onPressAvi = React.useCallback(() => {
+ if (view.avatar) {
+ store.shell.openLightbox(new ProfileImageLightbox(view))
+ }
+ }, [store, view])
- const onPressToggleFollow = React.useCallback(() => {
- view?.toggleFollowing().then(
- () => {
- Toast.show(
- `${
- view.viewer.following ? 'Following' : 'No longer following'
- } ${sanitizeDisplayName(view.displayName || view.handle)}`,
- )
- },
- err => store.log.error('Failed to toggle follow', err),
+ const onPressToggleFollow = React.useCallback(() => {
+ view?.toggleFollowing().then(
+ () => {
+ Toast.show(
+ `${
+ view.viewer.following ? 'Following' : 'No longer following'
+ } ${sanitizeDisplayName(view.displayName || view.handle)}`,
+ )
+ },
+ err => store.log.error('Failed to toggle follow', err),
+ )
+ }, [view, store])
+
+ const onPressEditProfile = React.useCallback(() => {
+ track('ProfileHeader:EditProfileButtonClicked')
+ store.shell.openModal({
+ name: 'edit-profile',
+ profileView: view,
+ onUpdate: onRefreshAll,
+ })
+ }, [track, store, view, onRefreshAll])
+
+ const onPressFollowers = React.useCallback(() => {
+ track('ProfileHeader:FollowersButtonClicked')
+ navigation.push('ProfileFollowers', {name: view.handle})
+ }, [track, navigation, view])
+
+ const onPressFollows = React.useCallback(() => {
+ track('ProfileHeader:FollowsButtonClicked')
+ navigation.push('ProfileFollows', {name: view.handle})
+ }, [track, navigation, view])
+
+ const onPressShare = React.useCallback(() => {
+ track('ProfileHeader:ShareButtonClicked')
+ const url = toShareUrl(`/profile/${view.handle}`)
+ shareUrl(url)
+ }, [track, view])
+
+ const onPressAddRemoveLists = React.useCallback(() => {
+ track('ProfileHeader:AddToListsButtonClicked')
+ store.shell.openModal({
+ name: 'list-add-remove-user',
+ subject: view.did,
+ displayName: view.displayName || view.handle,
+ })
+ }, [track, view, store])
+
+ const onPressMuteAccount = React.useCallback(async () => {
+ track('ProfileHeader:MuteAccountButtonClicked')
+ try {
+ await view.muteAccount()
+ Toast.show('Account muted')
+ } catch (e: any) {
+ store.log.error('Failed to mute account', e)
+ Toast.show(`There was an issue! ${e.toString()}`)
+ }
+ }, [track, view, store])
+
+ const onPressUnmuteAccount = React.useCallback(async () => {
+ track('ProfileHeader:UnmuteAccountButtonClicked')
+ try {
+ await view.unmuteAccount()
+ Toast.show('Account unmuted')
+ } catch (e: any) {
+ store.log.error('Failed to unmute account', e)
+ Toast.show(`There was an issue! ${e.toString()}`)
+ }
+ }, [track, view, store])
+
+ const onPressBlockAccount = React.useCallback(async () => {
+ track('ProfileHeader:BlockAccountButtonClicked')
+ store.shell.openModal({
+ name: 'confirm',
+ title: 'Block Account',
+ message:
+ 'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.',
+ onPressConfirm: async () => {
+ try {
+ await view.blockAccount()
+ onRefreshAll()
+ Toast.show('Account blocked')
+ } catch (e: any) {
+ store.log.error('Failed to block account', e)
+ Toast.show(`There was an issue! ${e.toString()}`)
+ }
+ },
+ })
+ }, [track, view, store, onRefreshAll])
+
+ const onPressUnblockAccount = React.useCallback(async () => {
+ track('ProfileHeader:UnblockAccountButtonClicked')
+ store.shell.openModal({
+ name: 'confirm',
+ title: 'Unblock Account',
+ message:
+ 'The account will be able to interact with you after unblocking.',
+ onPressConfirm: async () => {
+ try {
+ await view.unblockAccount()
+ onRefreshAll()
+ Toast.show('Account unblocked')
+ } catch (e: any) {
+ store.log.error('Failed to unblock account', e)
+ Toast.show(`There was an issue! ${e.toString()}`)
+ }
+ },
+ })
+ }, [track, view, store, onRefreshAll])
+
+ const onPressReportAccount = React.useCallback(() => {
+ track('ProfileHeader:ReportAccountButtonClicked')
+ store.shell.openModal({
+ name: 'report-account',
+ did: view.did,
+ })
+ }, [track, store, view])
+
+ const isMe = React.useMemo(
+ () => store.me.did === view.did,
+ [store.me.did, view.did],
)
- }, [view, store])
+ const dropdownItems: DropdownItem[] = React.useMemo(() => {
+ let items: DropdownItem[] = [
+ {
+ testID: 'profileHeaderDropdownShareBtn',
+ label: 'Share',
+ onPress: onPressShare,
+ },
+ {
+ testID: 'profileHeaderDropdownListAddRemoveBtn',
+ label: 'Add to Lists',
+ onPress: onPressAddRemoveLists,
+ },
+ ]
+ if (!isMe) {
+ items.push({sep: true})
+ if (!view.viewer.blocking) {
+ items.push({
+ testID: 'profileHeaderDropdownMuteBtn',
+ label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
+ onPress: view.viewer.muted
+ ? onPressUnmuteAccount
+ : onPressMuteAccount,
+ })
+ }
+ items.push({
+ testID: 'profileHeaderDropdownBlockBtn',
+ label: view.viewer.blocking ? 'Unblock Account' : 'Block Account',
+ onPress: view.viewer.blocking
+ ? onPressUnblockAccount
+ : onPressBlockAccount,
+ })
+ items.push({
+ testID: 'profileHeaderDropdownReportBtn',
+ label: 'Report Account',
+ onPress: onPressReportAccount,
+ })
+ }
+ return items
+ }, [
+ isMe,
+ view.viewer.muted,
+ view.viewer.blocking,
+ onPressShare,
+ onPressUnmuteAccount,
+ onPressMuteAccount,
+ onPressUnblockAccount,
+ onPressBlockAccount,
+ onPressReportAccount,
+ onPressAddRemoveLists,
+ ])
- const onPressEditProfile = React.useCallback(() => {
- track('ProfileHeader:EditProfileButtonClicked')
- store.shell.openModal({
- name: 'edit-profile',
- profileView: view,
- onUpdate: onRefreshAll,
- })
- }, [track, store, view, onRefreshAll])
+ const blockHide = !isMe && (view.viewer.blocking || view.viewer.blockedBy)
+ const following = formatCount(view.followsCount)
+ const followers = formatCount(view.followersCount)
+ const pluralizedFollowers = pluralize(view.followersCount, 'follower')
- const onPressFollowers = React.useCallback(() => {
- track('ProfileHeader:FollowersButtonClicked')
- navigation.push('ProfileFollowers', {name: view.handle})
- }, [track, navigation, view])
-
- const onPressFollows = React.useCallback(() => {
- track('ProfileHeader:FollowsButtonClicked')
- navigation.push('ProfileFollows', {name: view.handle})
- }, [track, navigation, view])
-
- const onPressShare = React.useCallback(async () => {
- track('ProfileHeader:ShareButtonClicked')
- const url = toShareUrl(`/profile/${view.handle}`)
-
- if (isIOS || isAndroid) {
- Share.share({url})
- } else {
- // React Native Share is not supported by web. Web Share API
- // has increasing but not full support, so default to clipboard
- Clipboard.setString(url)
- Toast.show('Copied to clipboard')
- }
- }, [track, view])
-
- const onPressMuteAccount = React.useCallback(async () => {
- track('ProfileHeader:MuteAccountButtonClicked')
- try {
- await view.muteAccount()
- Toast.show('Account muted')
- } catch (e: any) {
- store.log.error('Failed to mute account', e)
- Toast.show(`There was an issue! ${e.toString()}`)
- }
- }, [track, view, store])
-
- const onPressUnmuteAccount = React.useCallback(async () => {
- track('ProfileHeader:UnmuteAccountButtonClicked')
- try {
- await view.unmuteAccount()
- Toast.show('Account unmuted')
- } catch (e: any) {
- store.log.error('Failed to unmute account', e)
- Toast.show(`There was an issue! ${e.toString()}`)
- }
- }, [track, view, store])
-
- const onPressReportAccount = React.useCallback(() => {
- track('ProfileHeader:ReportAccountButtonClicked')
- store.shell.openModal({
- name: 'report-account',
- did: view.did,
- })
- }, [track, store, view])
-
- const isMe = React.useMemo(
- () => store.me.did === view.did,
- [store.me.did, view.did],
- )
- const dropdownItems: DropdownItem[] = React.useMemo(() => {
- let items: DropdownItem[] = [
- {
- testID: 'profileHeaderDropdownSahreBtn',
- label: 'Share',
- onPress: onPressShare,
- },
- ]
- if (!isMe) {
- items.push({
- testID: 'profileHeaderDropdownMuteBtn',
- label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
- onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount,
- })
- items.push({
- testID: 'profileHeaderDropdownReportBtn',
- label: 'Report Account',
- onPress: onPressReportAccount,
- })
- }
- return items
- }, [
- isMe,
- view.viewer.muted,
- onPressShare,
- onPressUnmuteAccount,
- onPressMuteAccount,
- onPressReportAccount,
- ])
- return (
-
-
-
-
- {isMe ? (
-
-
- Edit Profile
-
-
- ) : (
+ return (
+
+
+
+
+ {isMe ? (
+
+
+ Edit Profile
+
+
+ ) : view.viewer.blocking ? (
+
+
+ Unblock
+
+
+ ) : !view.viewer.blockedBy ? (
+ <>
+ {store.me.follows.getFollowState(view.did) ===
+ FollowState.Following ? (
+
+
+
+ Following
+
+
+ ) : (
+
+
+
+ Follow
+
+
+ )}
+ >
+ ) : null}
+ {dropdownItems?.length ? (
+
+
+
+ ) : undefined}
+
+
+
+ {sanitizeDisplayName(view.displayName || view.handle)}
+
+
+
+ {view.viewer.followedBy && !blockHide ? (
+
+
+ Follows you
+
+
+ ) : undefined}
+ @{view.handle}
+
+ {!blockHide && (
<>
- {store.me.follows.getFollowState(view.did) ===
- FollowState.Following ? (
+
-
-
- Following
+ testID="profileHeaderFollowersButton"
+ style={[s.flexRow, s.mr10]}
+ onPress={onPressFollowers}
+ accessibilityRole="button"
+ accessibilityLabel={`${followers} ${pluralizedFollowers}`}
+ accessibilityHint={'Opens followers list'}>
+
+ {followers}
+
+
+ {pluralizedFollowers}
- ) : (
-
-
- Follow
+ testID="profileHeaderFollowsButton"
+ style={[s.flexRow, s.mr10]}
+ onPress={onPressFollows}
+ accessibilityRole="button"
+ accessibilityLabel={`${following} following`}
+ accessibilityHint={'Opens following list'}>
+
+ {following}
+
+
+ following
- )}
+
+ {formatCount(view.postsCount)}{' '}
+
+ {pluralize(view.postsCount, 'post')}
+
+
+
+ {view.descriptionRichText ? (
+
+ ) : undefined}
>
)}
- {dropdownItems?.length ? (
-
-
-
- ) : undefined}
+
+
+ {view.viewer.blocking ? (
+
+
+
+ Account blocked
+
+
+ ) : view.viewer.muted ? (
+
+
+
+ Account muted{' '}
+ {view.viewer.mutedByList && (
+
+ by{' '}
+
+
+ )}
+
+
+ ) : undefined}
+ {view.viewer.blockedBy && (
+
+
+
+ This account has blocked you
+
+
+ )}
+
-
-
- {sanitizeDisplayName(view.displayName || view.handle)}
-
-
-
- {view.viewer.followedBy ? (
-
-
- Follows you
-
+ {!isDesktopWeb && !hideBackButton && (
+
+
+
+
+
- ) : undefined}
- @{view.handle}
-
-
-
-
- {view.followersCount}
-
-
- {pluralize(view.followersCount, 'follower')}
-
-
-
-
- {view.followsCount}
-
-
- following
-
-
-
-
- {view.postsCount}
-
-
- {pluralize(view.postsCount, 'post')}
-
-
-
- {view.descriptionRichText ? (
-
- ) : undefined}
-
- {view.viewer.muted ? (
+
+ )}
+
-
+
-
- Account muted
-
-
- ) : undefined}
-
- {!isDesktopWeb && !hideBackButton && (
-
-
-
-
-
- )}
-
-
-
-
-
-
- )
-})
+
+ )
+ },
+)
const styles = StyleSheet.create({
banner: {
@@ -443,6 +584,15 @@ const styles = StyleSheet.create({
},
title: {lineHeight: 38},
+ // Word wrapping appears fine on
+ // mobile but overflows on desktop
+ handle: isNative
+ ? {}
+ : {
+ // @ts-ignore web only -prf
+ wordBreak: 'break-all',
+ },
+
handleLine: {
flexDirection: 'row',
marginBottom: 8,
@@ -469,6 +619,19 @@ const styles = StyleSheet.create({
paddingVertical: 2,
},
+ moderationLines: {
+ gap: 6,
+ },
+
+ moderationNotice: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderRadius: 8,
+ paddingHorizontal: 16,
+ paddingVertical: 14,
+ gap: 8,
+ },
+
br40: {borderRadius: 40},
br50: {borderRadius: 50},
})
diff --git a/src/view/com/search/HeaderWithInput.tsx b/src/view/com/search/HeaderWithInput.tsx
index 10a5bede71..c51d4f709e 100644
--- a/src/view/com/search/HeaderWithInput.tsx
+++ b/src/view/com/search/HeaderWithInput.tsx
@@ -4,7 +4,6 @@ import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
-import {UserAvatar} from 'view/com/util/UserAvatar'
import {Text} from 'view/com/util/text/Text'
import {MagnifyingGlassIcon} from 'lib/icons'
import {useTheme} from 'lib/ThemeContext'
@@ -54,8 +53,11 @@ export function HeaderWithInput({
testID="viewHeaderBackOrMenuBtn"
onPress={onPressMenu}
hitSlop={MENU_HITSLOP}
- style={styles.headerMenuBtn}>
-
+ style={styles.headerMenuBtn}
+ accessibilityRole="button"
+ accessibilityLabel="Menu"
+ accessibilityHint="Access navigation links and settings">
+
setIsInputFocused(false)}
onChangeText={onChangeQuery}
onSubmitEditing={onSubmitQuery}
+ autoFocus={true}
+ accessibilityRole="search"
+ accessibilityLabel="Search"
+ accessibilityHint=""
+ autoCorrect={false}
+ autoCapitalize="none"
/>
{query ? (
-
+
{query || isInputFocused ? (
-
+
Cancel
@@ -106,13 +120,18 @@ const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
+ justifyContent: 'center',
paddingHorizontal: 12,
paddingVertical: 4,
},
headerMenuBtn: {
- width: 40,
+ width: 30,
height: 30,
- marginLeft: 6,
+ borderRadius: 30,
+ marginRight: 6,
+ paddingBottom: 2,
+ alignItems: 'center',
+ justifyContent: 'center',
},
headerSearchContainer: {
flex: 1,
diff --git a/src/view/com/search/SearchResults.tsx b/src/view/com/search/SearchResults.tsx
index 5d6163d4b0..bf623d93e9 100644
--- a/src/view/com/search/SearchResults.tsx
+++ b/src/view/com/search/SearchResults.tsx
@@ -14,6 +14,7 @@ import {
import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
+import {isDesktopWeb} from 'platform/detection'
const SECTIONS = ['Posts', 'Users']
@@ -23,8 +24,13 @@ export const SearchResults = observer(({model}: {model: SearchUIModel}) => {
const renderTabBar = React.useCallback(
(props: RenderTabBarFnProps) => {
return (
-
-
+
+
)
},
@@ -33,8 +39,12 @@ export const SearchResults = observer(({model}: {model: SearchUIModel}) => {
return (
-
-
+
+
+
+
+
+
)
})
@@ -49,7 +59,7 @@ const PostResults = observer(({model}: {model: SearchUIModel}) => {
)
}
- if (model.postUris.length === 0) {
+ if (model.posts.length === 0) {
return (
@@ -60,9 +70,14 @@ const PostResults = observer(({model}: {model: SearchUIModel}) => {
}
return (
-
- {model.postUris.map(uri => (
-
+
+ {model.posts.map(post => (
+
))}
@@ -94,15 +109,7 @@ const Profiles = observer(({model}: {model: SearchUIModel}) => {
return (
{model.profiles.map(item => (
-
+
))}
@@ -114,9 +121,19 @@ const Profiles = observer(({model}: {model: SearchUIModel}) => {
const styles = StyleSheet.create({
tabBar: {
borderBottomWidth: 1,
+ position: 'absolute',
+ zIndex: 1,
+ left: 0,
+ right: 0,
+ top: 0,
+ flexDirection: 'column',
+ alignItems: 'center',
},
empty: {
paddingHorizontal: 14,
paddingVertical: 16,
},
+ results: {
+ paddingTop: isDesktopWeb ? 50 : 42,
+ },
})
diff --git a/src/view/com/search/Suggestions.tsx b/src/view/com/search/Suggestions.tsx
index aacab5c98f..c8941e24d3 100644
--- a/src/view/com/search/Suggestions.tsx
+++ b/src/view/com/search/Suggestions.tsx
@@ -60,7 +60,7 @@ export const Suggestions = observer(
{
_reactKey: '__popular_heading__',
type: 'heading',
- title: 'In your network',
+ title: 'In Your Network',
},
])
.concat(
@@ -77,7 +77,7 @@ export const Suggestions = observer(
{
_reactKey: '__suggested_heading__',
type: 'heading',
- title: 'Suggested follows',
+ title: 'Suggested Follows',
},
])
.concat(
@@ -144,18 +144,9 @@ export const Suggestions = observer(
)
@@ -191,19 +173,9 @@ export const Suggestions = observer(
)
diff --git a/src/view/com/util/Alert.tsx b/src/view/com/util/Alert.tsx
new file mode 100644
index 0000000000..e91640dcd0
--- /dev/null
+++ b/src/view/com/util/Alert.tsx
@@ -0,0 +1 @@
+export {Alert} from 'react-native'
diff --git a/src/view/com/util/Alert.web.tsx b/src/view/com/util/Alert.web.tsx
new file mode 100644
index 0000000000..94ccc7e43f
--- /dev/null
+++ b/src/view/com/util/Alert.web.tsx
@@ -0,0 +1,23 @@
+import {AlertButton, AlertStatic} from 'react-native'
+
+class WebAlert implements Pick {
+ public alert(title: string, message?: string, buttons?: AlertButton[]): void {
+ if (buttons === undefined || buttons.length === 0) {
+ window.alert([title, message].filter(Boolean).join('\n'))
+ return
+ }
+
+ const result = window.confirm([title, message].filter(Boolean).join('\n'))
+
+ if (result === true) {
+ const confirm = buttons.find(({style}) => style !== 'cancel')
+ confirm?.onPress?.()
+ return
+ }
+
+ const cancel = buttons.find(({style}) => style === 'cancel')
+ cancel?.onPress?.()
+ }
+}
+
+export const Alert = new WebAlert()
diff --git a/src/view/com/util/BlurView.web.tsx b/src/view/com/util/BlurView.web.tsx
index efcf40b9c1..d1fb4665fb 100644
--- a/src/view/com/util/BlurView.web.tsx
+++ b/src/view/com/util/BlurView.web.tsx
@@ -14,7 +14,9 @@ export const BlurView = ({
...props
}: React.PropsWithChildren) => {
// @ts-ignore using an RNW-specific attribute here -prf
- style = addStyle(style, {backdropFilter: `blur(${blurAmount || 10}px`})
+ let blur = `blur(${blurAmount || 10}px`
+ // @ts-ignore using an RNW-specific attribute here -prf
+ style = addStyle(style, {backdropFilter: blur, WebkitBackdropFilter: blur})
if (blurType === 'dark') {
style = addStyle(style, styles.dark)
} else {
diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx
index e175b33a53..91379f1c98 100644
--- a/src/view/com/util/BottomSheetCustomBackdrop.tsx
+++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx
@@ -1,5 +1,5 @@
import React, {useMemo} from 'react'
-import {GestureResponderEvent, TouchableWithoutFeedback} from 'react-native'
+import {TouchableWithoutFeedback} from 'react-native'
import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
import Animated, {
Extrapolate,
@@ -8,7 +8,7 @@ import Animated, {
} from 'react-native-reanimated'
export function createCustomBackdrop(
- onClose?: ((event: GestureResponderEvent) => void) | undefined,
+ onClose?: (() => void) | undefined,
): React.FC {
const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => {
// animated variables
@@ -27,7 +27,15 @@ export function createCustomBackdrop(
)
return (
-
+ {
+ if (onClose !== undefined) {
+ onClose()
+ }
+ }}>
)
diff --git a/src/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx
index 2b2c4e6578..a495fcd3f8 100644
--- a/src/view/com/util/EmptyState.tsx
+++ b/src/view/com/util/EmptyState.tsx
@@ -10,17 +10,19 @@ import {UserGroupIcon} from 'lib/icons'
import {usePalette} from 'lib/hooks/usePalette'
export function EmptyState({
+ testID,
icon,
message,
style,
}: {
+ testID?: string
icon: IconProp | 'user-group'
message: string
style?: StyleProp
}) {
const pal = usePalette('default')
return (
-
+
{icon === 'user-group' ? (
diff --git a/src/view/com/util/EmptyStateWithButton.tsx b/src/view/com/util/EmptyStateWithButton.tsx
new file mode 100644
index 0000000000..008ca2bdb6
--- /dev/null
+++ b/src/view/com/util/EmptyStateWithButton.tsx
@@ -0,0 +1,88 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {IconProp} from '@fortawesome/fontawesome-svg-core'
+import {Text} from './text/Text'
+import {Button} from './forms/Button'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+
+interface Props {
+ testID?: string
+ icon: IconProp
+ message: string
+ buttonLabel: string
+ onPress: () => void
+}
+
+export function EmptyStateWithButton(props: Props) {
+ const pal = usePalette('default')
+ const palInverted = usePalette('inverted')
+
+ return (
+
+
+
+
+
+ {props.message}
+
+
+
+
+
+ {props.buttonLabel}
+
+
+
+
+ )
+}
+const styles = StyleSheet.create({
+ container: {
+ height: '100%',
+ paddingVertical: 40,
+ paddingHorizontal: 30,
+ },
+ iconContainer: {
+ marginBottom: 16,
+ },
+ icon: {
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ },
+ btns: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+ btn: {
+ gap: 10,
+ marginVertical: 20,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 14,
+ paddingHorizontal: 24,
+ borderRadius: 30,
+ },
+ notice: {
+ borderRadius: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ marginHorizontal: 30,
+ },
+})
diff --git a/src/view/com/util/Html.tsx b/src/view/com/util/Html.tsx
index dbf24a83a9..6179a726e9 100644
--- a/src/view/com/util/Html.tsx
+++ b/src/view/com/util/Html.tsx
@@ -1,49 +1,53 @@
-import React from 'react'
+import * as React from 'react'
import {StyleSheet, View} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
import {Text} from './text/Text'
import {TextLink} from './Link'
import {isDesktopWeb} from 'platform/detection'
+import {
+ H1 as ExpoH1,
+ H2 as ExpoH2,
+ H3 as ExpoH3,
+ H4 as ExpoH4,
+} from '@expo/html-elements'
/**
* These utilities are used to define long documents in an html-like
* DSL. See for instance /locale/en/privacy-policy.tsx
*/
+interface IsChildProps {
+ isChild?: boolean
+}
+
+// type ReactNodeWithIsChildProp =
+// | React.ReactElement
+// | React.ReactElement[]
+// | React.ReactNode
+
export function H1({children}: React.PropsWithChildren<{}>) {
const pal = usePalette('default')
- return (
-
- {children}
-
- )
+ const typography = useTheme().typography['title-xl']
+ return {children}
}
export function H2({children}: React.PropsWithChildren<{}>) {
const pal = usePalette('default')
- return (
-
- {children}
-
- )
+ const typography = useTheme().typography['title-lg']
+ return {children}
}
export function H3({children}: React.PropsWithChildren<{}>) {
const pal = usePalette('default')
- return (
-
- {children}
-
- )
+ const typography = useTheme().typography.title
+ return {children}
}
export function H4({children}: React.PropsWithChildren<{}>) {
const pal = usePalette('default')
- return (
-
- {children}
-
- )
+ const typography = useTheme().typography['title-sm']
+ return {children}
}
export function P({children}: React.PropsWithChildren<{}>) {
@@ -55,10 +59,7 @@ export function P({children}: React.PropsWithChildren<{}>) {
)
}
-export function UL({
- children,
- isChild,
-}: React.PropsWithChildren<{isChild: boolean}>) {
+export function UL({children, isChild}: React.PropsWithChildren) {
return (
{markChildProps(children)}
@@ -66,10 +67,7 @@ export function UL({
)
}
-export function OL({
- children,
- isChild,
-}: React.PropsWithChildren<{isChild: boolean}>) {
+export function OL({children, isChild}: React.PropsWithChildren) {
return (
{markChildProps(children)}
@@ -122,10 +120,13 @@ export function EM({children}: React.PropsWithChildren<{}>) {
)
}
-function markChildProps(children) {
+function markChildProps(children: React.ReactNode) {
return React.Children.map(children, child => {
if (React.isValidElement(child)) {
- return React.cloneElement(child, {isChild: true})
+ return React.cloneElement(
+ child as React.ReactElement,
+ {isChild: true},
+ )
}
return child
})
@@ -143,9 +144,11 @@ const styles = StyleSheet.create({
letterSpacing: 0.8,
},
h3: {
+ marginTop: 0,
marginBottom: 10,
},
h4: {
+ marginTop: 0,
marginBottom: 10,
fontWeight: 'bold',
},
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index 5110acf486..f753f01cc3 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {ComponentProps, useMemo} from 'react'
import {observer} from 'mobx-react-lite'
import {
Linking,
@@ -21,7 +21,7 @@ import {TypographyVariant} from 'lib/ThemeContext'
import {NavigationProp} from 'lib/routes/types'
import {router} from '../../../routes'
import {useStores, RootStoreModel} from 'state/index'
-import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers'
+import {convertBskyAppUrlIfNeeded, isExternalUrl} from 'lib/strings/url-helpers'
import {isDesktopWeb} from 'platform/detection'
import {sanitizeUrl} from '@braintree/sanitize-url'
@@ -29,6 +29,17 @@ type Event =
| React.MouseEvent
| GestureResponderEvent
+interface Props extends ComponentProps {
+ testID?: string
+ style?: StyleProp
+ href?: string
+ title?: string
+ children?: React.ReactNode
+ noFeedback?: boolean
+ asAnchor?: boolean
+ anchorNoUnderline?: boolean
+}
+
export const Link = observer(function Link({
testID,
style,
@@ -37,15 +48,10 @@ export const Link = observer(function Link({
children,
noFeedback,
asAnchor,
-}: {
- testID?: string
- style?: StyleProp
- href?: string
- title?: string
- children?: React.ReactNode
- noFeedback?: boolean
- asAnchor?: boolean
-}) {
+ accessible,
+ anchorNoUnderline,
+ ...props
+}: Props) {
const store = useStores()
const navigation = useNavigation()
@@ -64,20 +70,34 @@ export const Link = observer(function Link({
testID={testID}
onPress={onPress}
// @ts-ignore web only -prf
- href={asAnchor ? sanitizeUrl(href) : undefined}>
+ href={asAnchor ? sanitizeUrl(href) : undefined}
+ accessible={accessible}
+ accessibilityRole="link"
+ {...props}>
{children ? children : {title || 'link'} }
)
}
+
+ if (anchorNoUnderline) {
+ // @ts-ignore web only -prf
+ props.dataSet = props.dataSet || {}
+ // @ts-ignore web only -prf
+ props.dataSet.noUnderline = 1
+ }
+
return (
+ href={asAnchor ? sanitizeUrl(href) : undefined}
+ {...props}>
{children ? children : {title || 'link'} }
)
@@ -112,6 +132,16 @@ export const TextLink = observer(function TextLink({
},
[store, navigation, href],
)
+ const hrefAttrs = useMemo(() => {
+ const isExternal = isExternalUrl(href)
+ if (isExternal) {
+ return {
+ target: '_blank',
+ // rel: 'noopener noreferrer',
+ }
+ }
+ return {}
+ }, [href])
return (
{text}
diff --git a/src/view/com/util/Picker.tsx b/src/view/com/util/Picker.tsx
deleted file mode 100644
index 9007cb1f05..0000000000
--- a/src/view/com/util/Picker.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-// TODO: replaceme with something in the design system
-
-import React, {useRef} from 'react'
-import {
- StyleProp,
- StyleSheet,
- TextStyle,
- TouchableOpacity,
- TouchableWithoutFeedback,
- View,
- ViewStyle,
-} from 'react-native'
-import {
- FontAwesomeIcon,
- FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import RootSiblings from 'react-native-root-siblings'
-import {Text} from './text/Text'
-import {colors} from 'lib/styles'
-
-interface PickerItem {
- value: string
- label: string
-}
-
-interface PickerOpts {
- style?: StyleProp
- labelStyle?: StyleProp
- iconStyle?: FontAwesomeIconStyle
- items: PickerItem[]
- value: string
- onChange: (value: string) => void
- enabled?: boolean
-}
-
-const MENU_WIDTH = 200
-
-export function Picker({
- style,
- labelStyle,
- iconStyle,
- items,
- value,
- onChange,
- enabled,
-}: PickerOpts) {
- const ref = useRef(null)
- const valueLabel = items.find(item => item.value === value)?.label || value
- const onPress = () => {
- if (!enabled) {
- return
- }
- ref.current?.measure(
- (
- _x: number,
- _y: number,
- width: number,
- height: number,
- pageX: number,
- pageY: number,
- ) => {
- createDropdownMenu(pageX, pageY + height, MENU_WIDTH, items, onChange)
- },
- )
- }
- return (
-
-
-
- {valueLabel}
-
-
-
-
- )
-}
-
-function createDropdownMenu(
- x: number,
- y: number,
- width: number,
- items: PickerItem[],
- onChange: (value: string) => void,
-): RootSiblings {
- const onPressItem = (index: number) => {
- sibling.destroy()
- onChange(items[index].value)
- }
- const onOuterPress = () => sibling.destroy()
- const sibling = new RootSiblings(
- (
- <>
-
-
-
-
- {items.map((item, index) => (
- onPressItem(index)}>
- {item.label}
-
- ))}
-
- >
- ),
- )
- return sibling
-}
-
-const styles = StyleSheet.create({
- outer: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- label: {
- marginRight: 5,
- },
- icon: {},
- bg: {
- position: 'absolute',
- top: 0,
- right: 0,
- bottom: 0,
- left: 0,
- backgroundColor: '#000',
- opacity: 0.1,
- },
- menu: {
- position: 'absolute',
- backgroundColor: '#fff',
- borderRadius: 14,
- opacity: 1,
- paddingVertical: 6,
- },
- menuItem: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 6,
- paddingLeft: 15,
- paddingRight: 30,
- },
- menuItemBorder: {
- borderTopWidth: 1,
- borderTopColor: colors.gray2,
- marginTop: 4,
- paddingTop: 12,
- },
- menuItemIcon: {
- marginLeft: 6,
- marginRight: 8,
- },
- menuItemLabel: {
- fontSize: 15,
- },
-})
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index d9dd11e05a..45651e4e5a 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -97,7 +97,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
)}
diff --git a/src/view/com/util/PostSandboxWarning.tsx b/src/view/com/util/PostSandboxWarning.tsx
new file mode 100644
index 0000000000..54495aa9bb
--- /dev/null
+++ b/src/view/com/util/PostSandboxWarning.tsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {Text} from './text/Text'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+
+export function PostSandboxWarning() {
+ const store = useStores()
+ const pal = usePalette('default')
+ if (store.session.isSandbox) {
+ return (
+
+
+ SANDBOX
+
+
+ )
+ }
+ return null
+}
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ top: 6,
+ right: 10,
+ },
+ text: {
+ fontWeight: 'bold',
+ opacity: 0.07,
+ },
+})
diff --git a/src/view/com/util/Selector.tsx b/src/view/com/util/Selector.tsx
index 872b781844..223a069c89 100644
--- a/src/view/com/util/Selector.tsx
+++ b/src/view/com/util/Selector.tsx
@@ -85,6 +85,8 @@ export function Selector({
onSelect?.(index)
}
+ const numItems = items.length
+
return (
{
const selected = i === selectedIndex
return (
- onPressItem(i)}>
+ onPressItem(i)}
+ accessibilityLabel={`Select ${item}`}
+ accessibilityHint={`Select option ${i} of ${numItems}`}>
+
+
+
+ )
+ }
+ if (type === 'list') {
+ // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
+ return (
+
+
+
+
+
+ )
+ }
return (
void
}) {
const store = useStores()
@@ -53,61 +103,87 @@ export function UserAvatar({
const {requestCameraAccessIfNeeded} = useCameraPermission()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
- const dropdownItems = [
- !isWeb && {
- testID: 'changeAvatarCameraBtn',
- label: 'Camera',
- icon: 'camera' as IconProp,
- onPress: async () => {
- if (!(await requestCameraAccessIfNeeded())) {
- return
- }
- onSelectNewAvatar?.(
- await openCamera(store, {
- width: 1000,
- height: 1000,
- cropperCircleOverlay: true,
- }),
- )
- },
- },
- {
- testID: 'changeAvatarLibraryBtn',
- label: 'Library',
- icon: 'image' as IconProp,
- onPress: async () => {
- if (!(await requestPhotoAccessIfNeeded())) {
- return
- }
- const items = await openPicker(store, {
- mediaType: 'photo',
- multiple: false,
- })
+ const aviStyle = useMemo(() => {
+ if (type === 'algo' || type === 'list') {
+ return {
+ width: size,
+ height: size,
+ borderRadius: 8,
+ }
+ }
+ return {
+ width: size,
+ height: size,
+ borderRadius: Math.floor(size / 2),
+ }
+ }, [type, size])
- onSelectNewAvatar?.(
- await openCropper(store, {
+ const dropdownItems = useMemo(
+ () => [
+ !isWeb && {
+ testID: 'changeAvatarCameraBtn',
+ label: 'Camera',
+ icon: 'camera' as IconProp,
+ onPress: async () => {
+ if (!(await requestCameraAccessIfNeeded())) {
+ return
+ }
+
+ onSelectNewAvatar?.(
+ await openCamera(store, {
+ width: 1000,
+ height: 1000,
+ cropperCircleOverlay: true,
+ }),
+ )
+ },
+ },
+ {
+ testID: 'changeAvatarLibraryBtn',
+ label: 'Library',
+ icon: 'image' as IconProp,
+ onPress: async () => {
+ if (!(await requestPhotoAccessIfNeeded())) {
+ return
+ }
+
+ const items = await openPicker({
+ aspect: [1, 1],
+ })
+ const item = items[0]
+
+ const croppedImage = await openCropper(store, {
mediaType: 'photo',
- path: items[0].path,
- width: 1000,
- height: 1000,
cropperCircleOverlay: true,
- }),
- )
- },
- },
- {
- testID: 'changeAvatarRemoveBtn',
- label: 'Remove',
- icon: ['far', 'trash-can'] as IconProp,
- onPress: async () => {
- onSelectNewAvatar?.(null)
- },
- },
- ]
+ height: item.height,
+ width: item.width,
+ path: item.path,
+ })
- const warning = React.useMemo(() => {
- if (!hasWarning) {
- return <>>
+ onSelectNewAvatar?.(croppedImage)
+ },
+ },
+ !!avatar && {
+ testID: 'changeAvatarRemoveBtn',
+ label: 'Remove',
+ icon: ['far', 'trash-can'] as IconProp,
+ onPress: async () => {
+ onSelectNewAvatar?.(null)
+ },
+ },
+ ],
+ [
+ avatar,
+ onSelectNewAvatar,
+ requestCameraAccessIfNeeded,
+ requestPhotoAccessIfNeeded,
+ store,
+ ],
+ )
+
+ const warning = useMemo(() => {
+ if (!moderation?.warn) {
+ return null
}
return (
@@ -118,7 +194,7 @@ export function UserAvatar({
/>
)
- }, [hasWarning, size, pal])
+ }, [moderation?.warn, size, pal])
// onSelectNewAvatar is only passed as prop on the EditProfile component
return onSelectNewAvatar ? (
@@ -133,15 +209,12 @@ export function UserAvatar({
{avatar ? (
) : (
-
+
)}
- ) : avatar ? (
+ ) : avatar &&
+ !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
{warning}
) : (
-
+
{warning}
)
@@ -181,11 +256,6 @@ const styles = StyleSheet.create({
justifyContent: 'center',
backgroundColor: colors.gray5,
},
- avatarImage: {
- width: 80,
- height: 80,
- borderRadius: 40,
- },
warningIconContainer: {
position: 'absolute',
right: 0,
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index e58fb0ef4e..cce0e839b1 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -5,7 +5,6 @@ import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {Image} from 'expo-image'
import {colors} from 'lib/styles'
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
-import {Image as TImage} from 'lib/media/types'
import {useStores} from 'state/index'
import {
usePhotoLibraryPermission,
@@ -13,14 +12,18 @@ import {
} from 'lib/hooks/usePermissions'
import {DropdownButton} from './forms/DropdownButton'
import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
+import {AvatarModeration} from 'lib/labeling/types'
+import {isWeb, isAndroid} from 'platform/detection'
+import {Image as RNImage} from 'react-native-image-crop-picker'
export function UserBanner({
banner,
+ moderation,
onSelectNewBanner,
}: {
banner?: string | null
- onSelectNewBanner?: (img: TImage | null) => void
+ moderation?: AvatarModeration
+ onSelectNewBanner?: (img: RNImage | null) => void
}) {
const store = useStores()
const pal = usePalette('default')
@@ -52,10 +55,8 @@ export function UserBanner({
if (!(await requestPhotoAccessIfNeeded())) {
return
}
- const items = await openPicker(store, {
- mediaType: 'photo',
- multiple: false,
- })
+ const items = await openPicker()
+
onSelectNewBanner?.(
await openCropper(store, {
mediaType: 'photo',
@@ -66,7 +67,7 @@ export function UserBanner({
)
},
},
- {
+ !!banner && {
testID: 'changeBannerRemoveBtn',
label: 'Remove',
icon: ['far', 'trash-can'] as IconProp,
@@ -91,6 +92,8 @@ export function UserBanner({
testID="userBannerImage"
style={styles.bannerImage}
source={{uri: banner}}
+ accessible={true}
+ accessibilityIgnoresInvertColors
/>
) : (
- ) : banner ? (
+ ) : banner &&
+ !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
) : (
)
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index 3ec475e935..f5a921ac04 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -3,7 +3,7 @@ import {observer} from 'mobx-react-lite'
import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
-import {UserAvatar} from './UserAvatar'
+import {CenteredView} from './Views'
import {Text} from './text/Text'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
@@ -18,10 +18,16 @@ export const ViewHeader = observer(function ({
title,
canGoBack,
hideOnScroll,
+ showOnDesktop,
+ showBorder,
+ renderButton,
}: {
title: string
canGoBack?: boolean
hideOnScroll?: boolean
+ showOnDesktop?: boolean
+ showBorder?: boolean
+ renderButton?: () => JSX.Element
}) {
const pal = usePalette('default')
const store = useStores()
@@ -42,19 +48,27 @@ export const ViewHeader = observer(function ({
}, [track, store])
if (isDesktopWeb) {
- return <>>
+ if (showOnDesktop) {
+ return
+ }
+ return null
} else {
if (typeof canGoBack === 'undefined') {
canGoBack = navigation.canGoBack()
}
return (
-
+
+ style={canGoBack ? styles.backBtn : styles.backBtnWide}
+ accessibilityRole="button"
+ accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
+ accessibilityHint={
+ canGoBack ? '' : 'Access navigation links and settings'
+ }>
{canGoBack ? (
) : (
-
+
)}
@@ -70,19 +88,45 @@ export const ViewHeader = observer(function ({
{title}
-
+ {renderButton ? (
+ renderButton()
+ ) : (
+
+ )}
)
}
})
+function DesktopWebHeader({
+ title,
+ renderButton,
+}: {
+ title: string
+ renderButton?: () => JSX.Element
+}) {
+ const pal = usePalette('default')
+ return (
+
+
+
+ {title}
+
+
+ {renderButton?.()}
+
+ )
+}
+
const Container = observer(
({
children,
hideOnScroll,
+ showBorder,
}: {
children: React.ReactNode
hideOnScroll: boolean
+ showBorder?: boolean
}) => {
const store = useStores()
const pal = usePalette('default')
@@ -110,11 +154,28 @@ const Container = observer(
}
if (!hideOnScroll) {
- return {children}
+ return (
+
+ {children}
+
+ )
}
return (
+ style={[
+ styles.header,
+ pal.view,
+ pal.border,
+ styles.headerFloating,
+ transform,
+ showBorder && styles.border,
+ ]}>
{children}
)
@@ -133,6 +194,13 @@ const styles = StyleSheet.create({
top: 0,
width: '100%',
},
+ desktopHeader: {
+ borderBottomWidth: 1,
+ paddingVertical: 12,
+ },
+ border: {
+ borderBottomWidth: 1,
+ },
titleContainer: {
marginLeft: 'auto',
@@ -148,9 +216,9 @@ const styles = StyleSheet.create({
height: 30,
},
backBtnWide: {
- width: 40,
+ width: 30,
height: 30,
- marginLeft: 6,
+ paddingHorizontal: 6,
},
backIcon: {
marginTop: 6,
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx
index ba0780862d..705178a8af 100644
--- a/src/view/com/util/ViewSelector.tsx
+++ b/src/view/com/util/ViewSelector.tsx
@@ -1,121 +1,214 @@
import React, {useEffect, useState} from 'react'
-import {View} from 'react-native'
-import {Selector} from './Selector'
-import {HorzSwipe} from './gestures/HorzSwipe'
+import {Pressable, RefreshControl, StyleSheet, View} from 'react-native'
import {FlatList} from './Views'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
+import {Text} from './text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
import {clamp} from 'lib/numbers'
-import {s} from 'lib/styles'
+import {s, colors} from 'lib/styles'
+import {isAndroid} from 'platform/detection'
const HEADER_ITEM = {_reactKey: '__header__'}
const SELECTOR_ITEM = {_reactKey: '__selector__'}
const STICKY_HEADER_INDICES = [1]
-export function ViewSelector({
- sections,
- items,
- refreshing,
- swipeEnabled,
- renderHeader,
- renderItem,
- ListFooterComponent,
- onSelectView,
- onScroll,
- onRefresh,
- onEndReached,
-}: {
- sections: string[]
- items: any[]
- refreshing?: boolean
- swipeEnabled?: boolean
- renderHeader?: () => JSX.Element
- renderItem: (item: any) => JSX.Element
- ListFooterComponent?:
- | React.ComponentType
- | React.ReactElement
- | null
- | undefined
- onSelectView?: (viewIndex: number) => void
- onScroll?: OnScrollCb
- onRefresh?: () => void
- onEndReached?: (info: {distanceFromEnd: number}) => void
-}) {
- const [selectedIndex, setSelectedIndex] = useState(0)
- const panX = useAnimatedValue(0)
+export type ViewSelectorHandle = {
+ scrollToTop: () => void
+}
- // events
- // =
-
- const onSwipeEnd = React.useCallback(
- (dx: number) => {
- if (dx !== 0) {
- setSelectedIndex(clamp(selectedIndex + dx, 0, sections.length))
- }
+export const ViewSelector = React.forwardRef<
+ ViewSelectorHandle,
+ {
+ sections: string[]
+ items: any[]
+ refreshing?: boolean
+ swipeEnabled?: boolean
+ renderHeader?: () => JSX.Element
+ renderItem: (item: any) => JSX.Element
+ ListFooterComponent?:
+ | React.ComponentType
+ | React.ReactElement
+ | null
+ | undefined
+ onSelectView?: (viewIndex: number) => void
+ onScroll?: OnScrollCb
+ onRefresh?: () => void
+ onEndReached?: (info: {distanceFromEnd: number}) => void
+ }
+>(
+ (
+ {
+ sections,
+ items,
+ refreshing,
+ renderHeader,
+ renderItem,
+ ListFooterComponent,
+ onSelectView,
+ onScroll,
+ onRefresh,
+ onEndReached,
},
- [setSelectedIndex, selectedIndex, sections],
- )
- const onPressSelection = React.useCallback(
- (index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
- [setSelectedIndex, sections],
- )
- useEffect(() => {
- onSelectView?.(selectedIndex)
- }, [selectedIndex, onSelectView])
+ ref,
+ ) => {
+ const pal = usePalette('default')
+ const [selectedIndex, setSelectedIndex] = useState(0)
+ const flatListRef = React.useRef(null)
- // rendering
- // =
+ // events
+ // =
- const renderItemInternal = React.useCallback(
- ({item}: {item: any}) => {
- if (item === HEADER_ITEM) {
- if (renderHeader) {
- return renderHeader()
+ const keyExtractor = React.useCallback(item => item._reactKey, [])
+
+ const onPressSelection = React.useCallback(
+ (index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
+ [setSelectedIndex, sections],
+ )
+ useEffect(() => {
+ onSelectView?.(selectedIndex)
+ }, [selectedIndex, onSelectView])
+
+ React.useImperativeHandle(ref, () => ({
+ scrollToTop: () => {
+ flatListRef.current?.scrollToOffset({offset: 0})
+ },
+ }))
+
+ // rendering
+ // =
+
+ const renderItemInternal = React.useCallback(
+ ({item}: {item: any}) => {
+ if (item === HEADER_ITEM) {
+ if (renderHeader) {
+ return renderHeader()
+ }
+ return
+ } else if (item === SELECTOR_ITEM) {
+ return (
+
+ )
+ } else {
+ return renderItem(item)
}
- return
- } else if (item === SELECTOR_ITEM) {
- return (
-
- )
- } else {
- return renderItem(item)
- }
- },
- [sections, panX, selectedIndex, onPressSelection, renderHeader, renderItem],
- )
+ },
+ [sections, selectedIndex, onPressSelection, renderHeader, renderItem],
+ )
- const data = React.useMemo(
- () => [HEADER_ITEM, SELECTOR_ITEM, ...items],
- [items],
- )
- return (
- 0}
- canSwipeRight={selectedIndex < sections.length - 1}
- onSwipeEnd={onSwipeEnd}>
+ const data = React.useMemo(
+ () => [HEADER_ITEM, SELECTOR_ITEM, ...items],
+ [items],
+ )
+ return (
item._reactKey}
+ keyExtractor={keyExtractor}
renderItem={renderItemInternal}
ListFooterComponent={ListFooterComponent}
- stickyHeaderIndices={STICKY_HEADER_INDICES}
- refreshing={refreshing}
+ // NOTE sticky header disabled on android due to major performance issues -prf
+ stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
onScroll={onScroll}
- onRefresh={onRefresh}
onEndReached={onEndReached}
+ refreshControl={
+
+ }
onEndReachedThreshold={0.6}
contentContainerStyle={s.contentContainer}
removeClippedSubviews={true}
scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464
/>
-
+ )
+ },
+)
+
+export function Selector({
+ selectedIndex,
+ items,
+ onSelect,
+}: {
+ selectedIndex: number
+ items: string[]
+ onSelect?: (index: number) => void
+}) {
+ const pal = usePalette('default')
+ const borderColor = useColorSchemeStyle(
+ {borderColor: colors.black},
+ {borderColor: colors.white},
+ )
+
+ const onPressItem = (index: number) => {
+ onSelect?.(index)
+ }
+
+ return (
+
+ {items.map((item, i) => {
+ const selected = i === selectedIndex
+ return (
+ onPressItem(i)}
+ accessibilityLabel={item}
+ accessibilityHint={`Selects ${item}`}
+ // TODO: Modify the component API such that lint fails
+ // at the invocation site as well
+ >
+
+
+ {item}
+
+
+
+ )
+ })}
+
)
}
+
+const styles = StyleSheet.create({
+ outer: {
+ flexDirection: 'row',
+ paddingHorizontal: 14,
+ },
+ item: {
+ marginRight: 14,
+ paddingHorizontal: 10,
+ paddingTop: 8,
+ paddingBottom: 12,
+ },
+ itemSelected: {
+ borderBottomWidth: 3,
+ },
+ label: {
+ fontWeight: '600',
+ },
+ labelSelected: {
+ fontWeight: '600',
+ },
+ underline: {
+ position: 'absolute',
+ height: 4,
+ bottom: 0,
+ },
+})
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index d4bb377e5a..3313492e12 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -22,7 +22,12 @@ import {
View,
ViewProps,
} from 'react-native'
-import {addStyle, colors} from 'lib/styles'
+import {addStyle} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+
+interface AddedProps {
+ desktopFixedHeight?: boolean
+}
export function CenteredView({
style,
@@ -37,10 +42,12 @@ export const FlatList = React.forwardRef(function (
contentContainerStyle,
style,
contentOffset,
+ desktopFixedHeight,
...props
- }: React.PropsWithChildren>,
+ }: React.PropsWithChildren & AddedProps>,
ref: React.Ref,
) {
+ const pal = usePalette('default')
contentContainerStyle = addStyle(
contentContainerStyle,
styles.containerScroll,
@@ -58,10 +65,17 @@ export const FlatList = React.forwardRef(function (
paddingTop: Math.abs(contentOffset.y),
})
}
+ if (desktopFixedHeight) {
+ style = addStyle(style, styles.fixedHeight)
+ }
return (
,
ref: React.Ref,
) {
+ const pal = usePalette('default')
+
contentContainerStyle = addStyle(
contentContainerStyle,
styles.containerScroll,
)
return (
@@ -87,6 +107,11 @@ export const ScrollView = React.forwardRef(function (
})
const styles = StyleSheet.create({
+ contentContainer: {
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ minHeight: '100vh',
+ },
container: {
width: '100%',
maxWidth: 600,
@@ -95,15 +120,12 @@ const styles = StyleSheet.create({
},
containerScroll: {
width: '100%',
- maxHeight: '100vh',
maxWidth: 600,
marginLeft: 'auto',
marginRight: 'auto',
},
- containerLight: {
- backgroundColor: colors.gray1,
- },
- containerDark: {
- backgroundColor: colors.gray7,
+ fixedHeight: {
+ height: '100vh',
+ scrollbarGutter: 'stable both-edges',
},
})
diff --git a/src/view/com/util/error/ErrorMessage.tsx b/src/view/com/util/error/ErrorMessage.tsx
index cc0df1b592..370f10ae30 100644
--- a/src/view/com/util/error/ErrorMessage.tsx
+++ b/src/view/com/util/error/ErrorMessage.tsx
@@ -47,7 +47,10 @@ export function ErrorMessage({
+ onPress={onPressTryAgain}
+ accessibilityRole="button"
+ accessibilityLabel="Retry"
+ accessibilityHint="Retries the last action, which errored out">
@@ -43,34 +43,32 @@ export function ErrorScreen({
{title}
- {message}
+ {message}
{details && (
+ style={[styles.details, pal.text, pal.viewLight]}>
{details}
)}
{onPressTryAgain && (
-
+ type="default"
+ style={[styles.btn]}
+ onPress={onPressTryAgain}
+ accessibilityLabel="Retry"
+ accessibilityHint="Retries the last action, which errored out">
-
+
Try again
-
+
)}
@@ -115,11 +113,10 @@ const styles = StyleSheet.create({
marginBottom: 10,
},
errorIcon: {
- borderRadius: 30,
+ borderRadius: 25,
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center',
- marginRight: 5,
},
})
diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx
index 3d44c0dd4e..76824e575c 100644
--- a/src/view/com/util/fab/FABInner.tsx
+++ b/src/view/com/util/fab/FABInner.tsx
@@ -1,25 +1,19 @@
-import React from 'react'
+import React, {ComponentProps} from 'react'
import {observer} from 'mobx-react-lite'
-import {
- Animated,
- GestureResponderEvent,
- StyleSheet,
- TouchableWithoutFeedback,
-} from 'react-native'
+import {Animated, StyleSheet, TouchableWithoutFeedback} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {gradients} from 'lib/styles'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useStores} from 'state/index'
import {isMobileWeb} from 'platform/detection'
-type OnPress = ((event: GestureResponderEvent) => void) | undefined
-export interface FABProps {
+export interface FABProps
+ extends ComponentProps {
testID?: string
icon: JSX.Element
- onPress: OnPress
}
-export const FABInner = observer(({testID, icon, onPress}: FABProps) => {
+export const FABInner = observer(({testID, icon, ...props}: FABProps) => {
const store = useStores()
const interp = useAnimatedValue(0)
React.useEffect(() => {
@@ -34,7 +28,7 @@ export const FABInner = observer(({testID, icon, onPress}: FABProps) => {
transform: [{translateY: Animated.multiply(interp, 60)}],
}
return (
-
+
+ | GestureResponderEvent
+
export type ButtonType =
| 'primary'
| 'secondary'
@@ -21,6 +26,7 @@ export type ButtonType =
| 'secondary-light'
| 'default-light'
+// TODO: Enforce that button always has a label
export function Button({
type = 'primary',
label,
@@ -29,6 +35,10 @@ export function Button({
onPress,
children,
testID,
+ accessibilityLabel,
+ accessibilityHint,
+ accessibilityLabelledBy,
+ onAccessibilityEscape,
}: React.PropsWithChildren<{
type?: ButtonType
label?: string
@@ -36,6 +46,10 @@ export function Button({
labelStyle?: StyleProp
onPress?: () => void
testID?: string
+ accessibilityLabel?: string
+ accessibilityHint?: string
+ accessibilityLabelledBy?: string
+ onAccessibilityEscape?: () => void
}>) {
const theme = useTheme()
const typeOuterStyle = choose>(
@@ -114,11 +128,39 @@ export function Button({
},
},
)
+
+ const onPressWrapped = React.useCallback(
+ (event: Event) => {
+ event.stopPropagation()
+ event.preventDefault()
+ onPress?.()
+ },
+ [onPress],
+ )
+
+ const getStyle = React.useCallback(
+ state => {
+ const arr = [typeOuterStyle, styles.outer, style]
+ if (state.pressed) {
+ arr.push({opacity: 0.6})
+ } else if (state.hovered) {
+ arr.push({opacity: 0.8})
+ }
+ return arr
+ },
+ [typeOuterStyle, style],
+ )
+
return (
-
+
{label ? (
{label}
@@ -126,7 +168,7 @@ export function Button({
) : (
children
)}
-
+
)
}
diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx
new file mode 100644
index 0000000000..4aa5cb6106
--- /dev/null
+++ b/src/view/com/util/forms/DateInput.tsx
@@ -0,0 +1,96 @@
+import React, {useState, useCallback} from 'react'
+import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
+import DateTimePicker, {
+ DateTimePickerEvent,
+} from '@react-native-community/datetimepicker'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {isIOS, isAndroid} from 'platform/detection'
+import {Button, ButtonType} from './Button'
+import {Text} from '../text/Text'
+import {TypographyVariant} from 'lib/ThemeContext'
+import {useTheme} from 'lib/ThemeContext'
+import {usePalette} from 'lib/hooks/usePalette'
+
+interface Props {
+ testID?: string
+ value: Date
+ onChange: (date: Date) => void
+ buttonType?: ButtonType
+ buttonStyle?: StyleProp
+ buttonLabelType?: TypographyVariant
+ buttonLabelStyle?: StyleProp
+ accessibilityLabel: string
+ accessibilityHint: string
+ accessibilityLabelledBy?: string
+}
+
+export function DateInput(props: Props) {
+ const [show, setShow] = useState(false)
+ const theme = useTheme()
+ const pal = usePalette('default')
+
+ const onChangeInternal = useCallback(
+ (event: DateTimePickerEvent, date: Date | undefined) => {
+ setShow(false)
+ if (date) {
+ props.onChange(date)
+ }
+ },
+ [setShow, props],
+ )
+
+ const onPress = useCallback(() => {
+ setShow(true)
+ }, [setShow])
+
+ return (
+
+ {isAndroid && (
+
+
+
+
+ {props.value.toLocaleDateString()}
+
+
+
+ )}
+ {(isIOS || show) && (
+
+ )}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ button: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 10,
+ },
+})
diff --git a/src/view/com/util/forms/DateInput.web.tsx b/src/view/com/util/forms/DateInput.web.tsx
new file mode 100644
index 0000000000..89dff5510c
--- /dev/null
+++ b/src/view/com/util/forms/DateInput.web.tsx
@@ -0,0 +1,92 @@
+import React, {useState, useCallback} from 'react'
+import {
+ StyleProp,
+ StyleSheet,
+ TextInput as RNTextInput,
+ TextStyle,
+ View,
+ ViewStyle,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {useTheme} from 'lib/ThemeContext'
+import {usePalette} from 'lib/hooks/usePalette'
+
+interface Props {
+ testID?: string
+ value: Date
+ onChange: (date: Date) => void
+ buttonType?: string
+ buttonStyle?: StyleProp
+ buttonLabelType?: string
+ buttonLabelStyle?: StyleProp
+ accessibilityLabel: string
+ accessibilityHint: string
+ accessibilityLabelledBy?: string
+}
+
+export function DateInput(props: Props) {
+ const theme = useTheme()
+ const pal = usePalette('default')
+ const palError = usePalette('error')
+ const [value, setValue] = useState(props.value.toLocaleDateString())
+ const [isValid, setIsValid] = useState(true)
+
+ const onChangeInternal = useCallback(
+ (v: string) => {
+ setValue(v)
+ const d = new Date(v)
+ if (!isNaN(Number(d))) {
+ setIsValid(true)
+ props.onChange(d)
+ } else {
+ setIsValid(false)
+ }
+ },
+ [setValue, setIsValid, props],
+ )
+
+ return (
+
+
+ onChangeInternal(v)}
+ value={value}
+ accessibilityLabel={props.accessibilityLabel}
+ accessibilityHint={props.accessibilityHint}
+ accessibilityLabelledBy={props.accessibilityLabelledBy}
+ />
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ borderWidth: 1,
+ borderRadius: 6,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 4,
+ },
+ icon: {
+ marginLeft: 10,
+ },
+ textInput: {
+ flex: 1,
+ width: '100%',
+ paddingVertical: 10,
+ paddingHorizontal: 10,
+ fontSize: 17,
+ letterSpacing: 0.25,
+ fontWeight: '400',
+ borderRadius: 10,
+ },
+})
diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx
index f21323efb2..064b8211b5 100644
--- a/src/view/com/util/forms/DropdownButton.tsx
+++ b/src/view/com/util/forms/DropdownButton.tsx
@@ -1,7 +1,6 @@
-import React, {useRef} from 'react'
+import React, {PropsWithChildren, useMemo, useRef} from 'react'
import {
Dimensions,
- Share,
StyleProp,
StyleSheet,
TouchableOpacity,
@@ -19,23 +18,40 @@ import {toShareUrl} from 'lib/strings/url-helpers'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
-import {isAndroid, isIOS} from 'platform/detection'
-import Clipboard from '@react-native-clipboard/clipboard'
-import * as Toast from '../../util/Toast'
+import {isWeb} from 'platform/detection'
+import {shareUrl} from 'lib/sharing'
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
-const ESTIMATED_MENU_ITEM_HEIGHT = 52
+const ESTIMATED_BTN_HEIGHT = 50
+const ESTIMATED_SEP_HEIGHT = 16
-export interface DropdownItem {
+export interface DropdownItemButton {
testID?: string
icon?: IconProp
label: string
onPress: () => void
}
+export interface DropdownItemSeparator {
+ sep: true
+}
+export type DropdownItem = DropdownItemButton | DropdownItemSeparator
type MaybeDropdownItem = DropdownItem | false | undefined
export type DropdownButtonType = ButtonType | 'bare'
+interface DropdownButtonProps {
+ testID?: string
+ type?: DropdownButtonType
+ style?: StyleProp
+ items: MaybeDropdownItem[]
+ label?: string
+ menuWidth?: number
+ children?: React.ReactNode
+ openToRight?: boolean
+ rightOffset?: number
+ bottomOffset?: number
+}
+
export function DropdownButton({
testID,
type = 'bare',
@@ -47,22 +63,13 @@ export function DropdownButton({
openToRight = false,
rightOffset = 0,
bottomOffset = 0,
-}: {
- testID?: string
- type?: DropdownButtonType
- style?: StyleProp
- items: MaybeDropdownItem[]
- label?: string
- menuWidth?: number
- children?: React.ReactNode
- openToRight?: boolean
- rightOffset?: number
- bottomOffset?: number
-}) {
- const ref = useRef(null)
+}: PropsWithChildren) {
+ const ref1 = useRef(null)
+ const ref2 = useRef(null)
const onPress = () => {
- ref.current?.measure(
+ const ref = ref1.current || ref2.current
+ ref?.measure(
(
_x: number,
_y: number,
@@ -75,7 +82,14 @@ export function DropdownButton({
menuWidth = 200
}
const winHeight = Dimensions.get('window').height
- const estimatedMenuHeight = items.length * ESTIMATED_MENU_ITEM_HEIGHT
+ let estimatedMenuHeight = 0
+ for (const item of items) {
+ if (item && isSep(item)) {
+ estimatedMenuHeight += ESTIMATED_SEP_HEIGHT
+ } else if (item && isBtn(item)) {
+ estimatedMenuHeight += ESTIMATED_BTN_HEIGHT
+ }
+ }
const newX = openToRight
? pageX + width + rightOffset
: pageX + width - menuWidth
@@ -93,6 +107,18 @@ export function DropdownButton({
)
}
+ const numItems = useMemo(
+ () =>
+ items.filter(item => {
+ if (item === undefined || item === false) {
+ return false
+ }
+
+ return isBtn(item)
+ }).length,
+ [items],
+ )
+
if (type === 'bare') {
return (
+ ref={ref1}
+ accessibilityRole="button"
+ accessibilityLabel={`Opens ${numItems} options`}
+ accessibilityHint={`Opens ${numItems} options`}>
{children}
)
}
return (
-
-
+
+
{children}
@@ -122,8 +156,10 @@ export function PostDropdownBtn({
itemCid,
itemHref,
isAuthor,
+ isThreadMuted,
onCopyPostText,
onOpenTranslate,
+ onToggleThreadMute,
onDeletePost,
}: {
testID?: string
@@ -134,8 +170,10 @@ export function PostDropdownBtn({
itemHref: string
itemTitle: string
isAuthor: boolean
+ isThreadMuted: boolean
onCopyPostText: () => void
onOpenTranslate: () => void
+ onToggleThreadMute: () => void
onDeletePost: () => void
}) {
const store = useStores()
@@ -163,18 +201,20 @@ export function PostDropdownBtn({
label: 'Share...',
onPress() {
const url = toShareUrl(itemHref)
-
- if (isIOS || isAndroid) {
- Share.share({url})
- } else {
- // React Native Share is not supported by web. Web Share API
- // has increasing but not full support, so default to clipboard
- Clipboard.setString(url)
- Toast.show('Copied to clipboard')
- }
+ shareUrl(url)
},
},
+ {sep: true},
{
+ testID: 'postDropdownMuteThreadBtn',
+ icon: 'comment-slash',
+ label: isThreadMuted ? 'Unmute thread' : 'Mute thread',
+ onPress() {
+ onToggleThreadMute()
+ },
+ },
+ {sep: true},
+ !isAuthor && {
testID: 'postDropdownReportBtn',
icon: 'circle-exclamation',
label: 'Report post',
@@ -186,21 +226,19 @@ export function PostDropdownBtn({
})
},
},
- isAuthor
- ? {
- testID: 'postDropdownDeleteBtn',
- icon: ['far', 'trash-can'],
- label: 'Delete post',
- onPress() {
- store.shell.openModal({
- name: 'confirm',
- title: 'Delete this post?',
- message: 'Are you sure? This can not be undone.',
- onPressConfirm: onDeletePost,
- })
- },
- }
- : undefined,
+ isAuthor && {
+ testID: 'postDropdownDeleteBtn',
+ icon: ['far', 'trash-can'],
+ label: 'Delete post',
+ onPress() {
+ store.shell.openModal({
+ name: 'confirm',
+ title: 'Delete this post?',
+ message: 'Are you sure? This can not be undone.',
+ onPressConfirm: onDeletePost,
+ })
+ },
+ },
].filter(Boolean) as DropdownItem[]
return (
@@ -208,7 +246,7 @@ export function PostDropdownBtn({
testID={testID}
style={style}
items={dropdownItems}
- menuWidth={200}>
+ menuWidth={isWeb ? 220 : 200}>
{children}
)
@@ -222,7 +260,10 @@ function createDropdownMenu(
): RootSiblings {
const onPressItem = (index: number) => {
sibling.destroy()
- items[index].onPress()
+ const item = items[index]
+ if (isBtn(item)) {
+ item.onPress()
+ }
}
const onOuterPress = () => sibling.destroy()
const sibling = new RootSiblings(
@@ -240,6 +281,93 @@ function createDropdownMenu(
return sibling
}
+type DropDownItemProps = {
+ onOuterPress: () => void
+ x: number
+ y: number
+ width: number
+ items: DropdownItem[]
+ onPressItem: (index: number) => void
+}
+
+const DropdownItems = ({
+ onOuterPress,
+ x,
+ y,
+ width,
+ items,
+ onPressItem,
+}: DropDownItemProps) => {
+ const pal = usePalette('default')
+ const theme = useTheme()
+ const dropDownBackgroundColor =
+ theme.colorScheme === 'dark' ? pal.btn : pal.view
+ const separatorColor =
+ theme.colorScheme === 'dark' ? pal.borderDark : pal.border
+
+ const numItems = items.filter(isBtn).length
+
+ return (
+ <>
+
+ // and elements for keyboard navigation out of the box
+ // - (On mobile) be buttons by default, accept `label` and `nativeID`
+ // props, and always have an explicit label
+ accessibilityRole="button"
+ accessibilityLabel="Toggle dropdown"
+ accessibilityHint="">
+
+
+
+ {items.map((item, index) => {
+ if (isBtn(item)) {
+ return (
+ onPressItem(index)}
+ accessibilityLabel={item.label}
+ accessibilityHint={`Option ${index + 1} of ${numItems}`}>
+ {item.icon && (
+
+ )}
+
+ {item.label}
+
+
+ )
+ } else if (isSep(item)) {
+ return (
+
+ )
+ }
+ return null
+ })}
+
+ >
+ )
+}
+
+function isSep(item: DropdownItem): item is DropdownItemSeparator {
+ return 'sep' in item && item.sep
+}
+function isBtn(item: DropdownItem): item is DropdownItemButton {
+ return !isSep(item)
+}
+
const styles = StyleSheet.create({
bg: {
position: 'absolute',
@@ -277,57 +405,8 @@ const styles = StyleSheet.create({
label: {
fontSize: 18,
},
+ separator: {
+ borderTopWidth: 1,
+ marginVertical: 8,
+ },
})
-type DropDownItemProps = {
- onOuterPress: () => void
- x: number
- y: number
- width: number
- items: DropdownItem[]
- onPressItem: (index: number) => void
-}
-
-const DropdownItems = ({
- onOuterPress,
- x,
- y,
- width,
- items,
- onPressItem,
-}: DropDownItemProps) => {
- const pal = usePalette('default')
- const theme = useTheme()
- const dropDownBackgroundColor =
- theme.colorScheme === 'dark' ? pal.btn : pal.view
-
- return (
- <>
-
-
-
-
- {items.map((item, index) => (
- onPressItem(index)}>
- {item.icon && (
-
- )}
- {item.label}
-
- ))}
-
- >
- )
-}
diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx
index f5696a76d5..9d1cb47497 100644
--- a/src/view/com/util/forms/RadioButton.tsx
+++ b/src/view/com/util/forms/RadioButton.tsx
@@ -15,7 +15,7 @@ export function RadioButton({
}: {
testID?: string
type?: ButtonType
- label: string
+ label: string | JSX.Element
isSelected: boolean
style?: StyleProp
onPress: () => void
@@ -47,7 +47,7 @@ export function RadioButton({
borderColor: theme.palette.default.border,
},
'default-light': {
- borderColor: theme.palette.default.border,
+ borderColor: theme.palette.default.borderDark,
},
})
const circleFillStyle = choose>(
@@ -128,9 +128,13 @@ export function RadioButton({
) : undefined}
-
- {label}
-
+ {typeof label === 'string' ? (
+
+ {label}
+
+ ) : (
+ {label}
+ )}
)
diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx
index 071540b73e..14599e6490 100644
--- a/src/view/com/util/forms/RadioGroup.tsx
+++ b/src/view/com/util/forms/RadioGroup.tsx
@@ -5,7 +5,7 @@ import {ButtonType} from './Button'
import {s} from 'lib/styles'
export interface RadioGroupItem {
- label: string
+ label: string | JSX.Element
key: string
}
diff --git a/src/view/com/util/forms/ToggleButton.tsx b/src/view/com/util/forms/ToggleButton.tsx
index a6e0ba3fec..804d414b3a 100644
--- a/src/view/com/util/forms/ToggleButton.tsx
+++ b/src/view/com/util/forms/ToggleButton.tsx
@@ -142,9 +142,11 @@ export function ToggleButton({
]}
/>
-
- {label}
-
+ {label === '' ? null : (
+
+ {label}
+
+ )}
)
@@ -154,6 +156,7 @@ const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
alignItems: 'center',
+ gap: 10,
},
circle: {
width: 42,
@@ -161,7 +164,6 @@ const styles = StyleSheet.create({
borderRadius: 15,
padding: 4,
borderWidth: 1,
- marginRight: 10,
},
circleFill: {
width: 16,
diff --git a/src/view/com/util/gestures/HorzSwipe.tsx b/src/view/com/util/gestures/HorzSwipe.tsx
deleted file mode 100644
index 09f6c345fc..0000000000
--- a/src/view/com/util/gestures/HorzSwipe.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import React, {useState} from 'react'
-import {
- Animated,
- GestureResponderEvent,
- I18nManager,
- PanResponder,
- PanResponderGestureState,
- useWindowDimensions,
- View,
-} from 'react-native'
-import {clamp} from 'lodash'
-import {s} from 'lib/styles'
-
-interface Props {
- panX: Animated.Value
- canSwipeLeft?: boolean
- canSwipeRight?: boolean
- swipeEnabled?: boolean
- hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
- distThresholdDivisor?: number
- useNativeDriver?: boolean
- onSwipeStart?: () => void
- onSwipeStartDirection?: (dx: number) => void
- onSwipeEnd?: (dx: number) => void
- children: React.ReactNode
-}
-
-export function HorzSwipe({
- panX,
- canSwipeLeft = false,
- canSwipeRight = false,
- swipeEnabled = true,
- hasPriority = false,
- distThresholdDivisor = 1.75,
- useNativeDriver = false,
- onSwipeStart,
- onSwipeStartDirection,
- onSwipeEnd,
- children,
-}: Props) {
- const winDim = useWindowDimensions()
- const [dir, setDir] = useState(0)
-
- const swipeVelocityThreshold = 35
- const swipeDistanceThreshold = winDim.width / distThresholdDivisor
-
- const isMovingHorizontally = (
- _: GestureResponderEvent,
- gestureState: PanResponderGestureState,
- ) => {
- return (
- Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) &&
- Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25)
- )
- }
-
- const canMoveScreen = (
- event: GestureResponderEvent,
- gestureState: PanResponderGestureState,
- ) => {
- if (swipeEnabled === false) {
- return false
- }
-
- const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
- const willHandle =
- isMovingHorizontally(event, gestureState) &&
- ((diffX > 0 && canSwipeLeft) || (diffX < 0 && canSwipeRight))
- return willHandle
- }
-
- const startGesture = () => {
- setDir(0)
- onSwipeStart?.()
-
- panX.stopAnimation()
- // @ts-expect-error: _value is private, but docs use it as well
- panX.setOffset(panX._value)
- }
-
- const respondToGesture = (
- _: GestureResponderEvent,
- gestureState: PanResponderGestureState,
- ) => {
- const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
-
- if (
- // swiping left
- (diffX > 0 && !canSwipeLeft) ||
- // swiping right
- (diffX < 0 && !canSwipeRight)
- ) {
- panX.setValue(0)
- return
- }
-
- panX.setValue(clamp(diffX / swipeDistanceThreshold, -1, 1) * -1)
-
- const newDir = diffX > 0 ? -1 : diffX < 0 ? 1 : 0
- if (newDir !== dir) {
- setDir(newDir)
- onSwipeStartDirection?.(newDir)
- }
- }
-
- const finishGesture = (
- _: GestureResponderEvent,
- gestureState: PanResponderGestureState,
- ) => {
- if (
- Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&
- Math.abs(gestureState.vx) > Math.abs(gestureState.vy) &&
- (Math.abs(gestureState.dx) > swipeDistanceThreshold / 4 ||
- Math.abs(gestureState.vx) > swipeVelocityThreshold)
- ) {
- const final = Math.floor(
- (gestureState.dx / Math.abs(gestureState.dx)) * -1,
- )
- Animated.timing(panX, {
- toValue: final,
- duration: 100,
- useNativeDriver,
- isInteraction: false,
- }).start(() => {
- onSwipeEnd?.(final)
- panX.flattenOffset()
- panX.setValue(0)
- })
- } else {
- onSwipeEnd?.(0)
- Animated.timing(panX, {
- toValue: 0,
- duration: 100,
- useNativeDriver,
- isInteraction: false,
- }).start(() => {
- panX.flattenOffset()
- panX.setValue(0)
- })
- }
- }
-
- const panResponder = PanResponder.create({
- onMoveShouldSetPanResponder: canMoveScreen,
- onPanResponderGrant: startGesture,
- onPanResponderMove: respondToGesture,
- onPanResponderTerminate: finishGesture,
- onPanResponderRelease: finishGesture,
- onPanResponderTerminationRequest: () => !hasPriority,
- })
-
- return (
-
- {children}
-
- )
-}
diff --git a/src/view/com/util/gestures/SwipeAndZoom.tsx b/src/view/com/util/gestures/SwipeAndZoom.tsx
deleted file mode 100644
index 75c679012f..0000000000
--- a/src/view/com/util/gestures/SwipeAndZoom.tsx
+++ /dev/null
@@ -1,302 +0,0 @@
-import React, {useState} from 'react'
-import {
- Animated,
- GestureResponderEvent,
- I18nManager,
- PanResponder,
- PanResponderGestureState,
- useWindowDimensions,
- View,
-} from 'react-native'
-import {clamp} from 'lodash'
-import {s} from 'lib/styles'
-
-export enum Dir {
- None,
- Up,
- Down,
- Left,
- Right,
- Zoom,
-}
-
-interface Props {
- panX: Animated.Value
- panY: Animated.Value
- zoom: Animated.Value
- canSwipeLeft?: boolean
- canSwipeRight?: boolean
- canSwipeUp?: boolean
- canSwipeDown?: boolean
- swipeEnabled?: boolean
- zoomEnabled?: boolean
- hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
- horzDistThresholdDivisor?: number
- vertDistThresholdDivisor?: number
- useNativeDriver?: boolean
- onSwipeStart?: () => void
- onSwipeStartDirection?: (dir: Dir) => void
- onSwipeEnd?: (dir: Dir) => void
- children: React.ReactNode
-}
-
-export function SwipeAndZoom({
- panX,
- panY,
- zoom,
- canSwipeLeft = false,
- canSwipeRight = false,
- canSwipeUp = false,
- canSwipeDown = false,
- swipeEnabled = false,
- zoomEnabled = false,
- hasPriority = false,
- horzDistThresholdDivisor = 1.75,
- vertDistThresholdDivisor = 1.75,
- useNativeDriver = false,
- onSwipeStart,
- onSwipeStartDirection,
- onSwipeEnd,
- children,
-}: Props) {
- const winDim = useWindowDimensions()
- const [dir, setDir] = useState(Dir.None)
- const [initialDistance, setInitialDistance] = useState(
- undefined,
- )
-
- const swipeVelocityThreshold = 35
- const swipeHorzDistanceThreshold = winDim.width / horzDistThresholdDivisor
- const swipeVertDistanceThreshold = winDim.height / vertDistThresholdDivisor
-
- const isMovingHorizontally = (
- _: GestureResponderEvent,
- gestureState: PanResponderGestureState,
- ) => {
- return (
- Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) &&
- Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25)
- )
- }
- const isMovingVertically = (
- _: GestureResponderEvent,
- gestureState: PanResponderGestureState,
- ) => {
- return (
- Math.abs(gestureState.dy) > Math.abs(gestureState.dx * 1.25) &&
- Math.abs(gestureState.vy) > Math.abs(gestureState.vx * 1.25)
- )
- }
-
- const canDir = (d: Dir) => {
- if (d === Dir.Left) {
- return canSwipeLeft
- }
- if (d === Dir.Right) {
- return canSwipeRight
- }
- if (d === Dir.Up) {
- return canSwipeUp
- }
- if (d === Dir.Down) {
- return canSwipeDown
- }
- if (d === Dir.Zoom) {
- return zoomEnabled
- }
- return false
- }
- const isHorz = (d: Dir) => d === Dir.Left || d === Dir.Right
- const isVert = (d: Dir) => d === Dir.Up || d === Dir.Down
-
- const canMoveScreen = (
- event: GestureResponderEvent,
- gestureState: PanResponderGestureState,
- ) => {
- if (zoomEnabled && gestureState.numberActiveTouches === 2) {
- return true
- } else if (swipeEnabled && gestureState.numberActiveTouches === 1) {
- const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
- const dy = gestureState.dy
- const willHandle =
- (isMovingHorizontally(event, gestureState) &&
- ((dx > 0 && canSwipeLeft) || (dx < 0 && canSwipeRight))) ||
- (isMovingVertically(event, gestureState) &&
- ((dy > 0 && canSwipeUp) || (dy < 0 && canSwipeDown)))
- return willHandle
- }
- return false
- }
-
- const startGesture = () => {
- setDir(Dir.None)
- onSwipeStart?.()
-
- // reset all state
- panX.stopAnimation()
- // @ts-expect-error: _value is private, but docs use it as well
- panX.setOffset(panX._value)
- panY.stopAnimation()
- // @ts-expect-error: _value is private, but docs use it as well
- panY.setOffset(panY._value)
- zoom.stopAnimation()
- // @ts-expect-error: _value is private, but docs use it as well
- zoom.setOffset(zoom._value)
- setInitialDistance(undefined)
- }
-
- const respondToGesture = (
- e: GestureResponderEvent,
- gestureState: PanResponderGestureState,
- ) => {
- const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
- const dy = gestureState.dy
-
- let newDir = Dir.None
- if (dir === Dir.None) {
- // establish if the user is swiping horz or vert, or zooming
- if (gestureState.numberActiveTouches === 2) {
- newDir = Dir.Zoom
- } else if (Math.abs(dx) > Math.abs(dy)) {
- newDir = dx > 0 ? Dir.Left : Dir.Right
- } else {
- newDir = dy > 0 ? Dir.Up : Dir.Down
- }
- } else if (isHorz(dir)) {
- // direction update
- newDir = dx > 0 ? Dir.Left : Dir.Right
- } else if (isVert(dir)) {
- // direction update
- newDir = dy > 0 ? Dir.Up : Dir.Down
- } else {
- newDir = dir
- }
-
- if (newDir === Dir.Zoom) {
- if (zoomEnabled) {
- if (gestureState.numberActiveTouches === 2) {
- // zoom in/out
- const x0 = e.nativeEvent.touches[0].pageX
- const x1 = e.nativeEvent.touches[1].pageX
- const y0 = e.nativeEvent.touches[0].pageY
- const y1 = e.nativeEvent.touches[1].pageY
- const zoomDx = Math.abs(x0 - x1)
- const zoomDy = Math.abs(y0 - y1)
- const dist = Math.sqrt(zoomDx * zoomDx + zoomDy * zoomDy) / 100
- if (
- typeof initialDistance === 'undefined' ||
- dist - initialDistance < 0
- ) {
- setInitialDistance(dist)
- } else {
- zoom.setValue(dist - initialDistance)
- }
- } else {
- // pan around after zooming
- panX.setValue(clamp(dx / winDim.width, -1, 1) * -1)
- panY.setValue(clamp(dy / winDim.height, -1, 1) * -1)
- }
- }
- } else if (isHorz(newDir)) {
- // swipe left/right
- panX.setValue(
- clamp(
- dx / swipeHorzDistanceThreshold,
- canSwipeRight ? -1 : 0,
- canSwipeLeft ? 1 : 0,
- ) * -1,
- )
- panY.setValue(0)
- } else if (isVert(newDir)) {
- // swipe up/down
- panY.setValue(
- clamp(
- dy / swipeVertDistanceThreshold,
- canSwipeDown ? -1 : 0,
- canSwipeUp ? 1 : 0,
- ) * -1,
- )
- panX.setValue(0)
- }
-
- if (!canDir(newDir)) {
- newDir = Dir.None
- }
- if (newDir !== dir) {
- setDir(newDir)
- onSwipeStartDirection?.(newDir)
- }
- }
-
- const finishGesture = (
- _: GestureResponderEvent,
- gestureState: PanResponderGestureState,
- ) => {
- const finish = (finalDir: Dir) => () => {
- if (finalDir !== Dir.None) {
- onSwipeEnd?.(finalDir)
- }
- setDir(Dir.None)
- panX.flattenOffset()
- panX.setValue(0)
- panY.flattenOffset()
- panY.setValue(0)
- }
- if (
- isHorz(dir) &&
- (Math.abs(gestureState.dx) > swipeHorzDistanceThreshold / 4 ||
- Math.abs(gestureState.vx) > swipeVelocityThreshold)
- ) {
- // horizontal swipe reset
- Animated.timing(panX, {
- toValue: dir === Dir.Left ? -1 : 1,
- duration: 100,
- useNativeDriver,
- }).start(finish(dir))
- } else if (
- isVert(dir) &&
- (Math.abs(gestureState.dy) > swipeVertDistanceThreshold / 8 ||
- Math.abs(gestureState.vy) > swipeVelocityThreshold)
- ) {
- // vertical swipe reset
- Animated.timing(panY, {
- toValue: dir === Dir.Up ? -1 : 1,
- duration: 100,
- useNativeDriver,
- }).start(finish(dir))
- } else {
- // zoom (or no direction) reset
- onSwipeEnd?.(Dir.None)
- Animated.timing(panX, {
- toValue: 0,
- duration: 100,
- useNativeDriver,
- }).start()
- Animated.timing(panY, {
- toValue: 0,
- duration: 100,
- useNativeDriver,
- }).start()
- Animated.timing(zoom, {
- toValue: 0,
- duration: 100,
- useNativeDriver,
- }).start()
- }
- }
-
- const panResponder = PanResponder.create({
- onMoveShouldSetPanResponder: canMoveScreen,
- onPanResponderGrant: startGesture,
- onPanResponderMove: respondToGesture,
- onPanResponderTerminate: finishGesture,
- onPanResponderRelease: finishGesture,
- onPanResponderTerminationRequest: () => !hasPriority,
- })
-
- return (
-
- {children}
-
- )
-}
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 17e3e809b2..e6aba46f3e 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -9,29 +9,33 @@ import {
import {Image} from 'expo-image'
import {clamp} from 'lib/numbers'
import {useStores} from 'state/index'
-import {Dim} from 'lib/media/manip'
+import {Dimensions} from 'lib/media/types'
export const DELAY_PRESS_IN = 500
const MIN_ASPECT_RATIO = 0.33 // 1/3
const MAX_ASPECT_RATIO = 5 // 5/1
-export function AutoSizedImage({
- uri,
- onPress,
- onLongPress,
- onPressIn,
- style,
- children = null,
-}: {
+interface Props {
+ alt?: string
uri: string
onPress?: () => void
onLongPress?: () => void
onPressIn?: () => void
style?: StyleProp
children?: React.ReactNode
-}) {
+}
+
+export function AutoSizedImage({
+ alt,
+ uri,
+ onPress,
+ onLongPress,
+ onPressIn,
+ style,
+ children = null,
+}: Props) {
const store = useStores()
- const [dim, setDim] = React.useState(
+ const [dim, setDim] = React.useState(
store.imageSizes.get(uri),
)
const [aspectRatio, setAspectRatio] = React.useState(
@@ -58,21 +62,39 @@ export function AutoSizedImage({
onLongPress={onLongPress}
onPressIn={onPressIn}
delayPressIn={DELAY_PRESS_IN}
- style={[styles.container, style]}>
-
+ style={[styles.container, style]}
+ accessible={true}
+ accessibilityLabel="Share image"
+ accessibilityHint="Opens ways of sharing image">
+
{children}
)
}
+
return (
-
+
{children}
)
}
-function calc(dim: Dim) {
+function calc(dim: Dimensions) {
if (dim.width === 0 || dim.height === 0) {
return 1
}
diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx
new file mode 100644
index 0000000000..723db289c1
--- /dev/null
+++ b/src/view/com/util/images/Gallery.tsx
@@ -0,0 +1,67 @@
+import {AppBskyEmbedImages} from '@atproto/api'
+import React, {ComponentProps, FC} from 'react'
+import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
+import {Image} from 'expo-image'
+
+type EventFunction = (index: number) => void
+
+interface GalleryItemProps {
+ images: AppBskyEmbedImages.ViewImage[]
+ index: number
+ onPress?: EventFunction
+ onLongPress?: EventFunction
+ onPressIn?: EventFunction
+ imageStyle: ComponentProps['style']
+}
+
+const DELAY_PRESS_IN = 500
+
+export const GalleryItem: FC = ({
+ images,
+ index,
+ imageStyle,
+ onPress,
+ onPressIn,
+ onLongPress,
+}) => {
+ const image = images[index]
+
+ return (
+
+ onPress(index) : undefined}
+ onPressIn={onPressIn ? () => onPressIn(index) : undefined}
+ onLongPress={onLongPress ? () => onLongPress(index) : undefined}
+ accessibilityRole="button"
+ accessibilityLabel="View image"
+ accessibilityHint="">
+
+
+ {image.alt === '' ? null : ALT }
+
+ )
+}
+
+const styles = StyleSheet.create({
+ alt: {
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
+ borderRadius: 6,
+ color: 'white',
+ fontSize: 12,
+ fontWeight: 'bold',
+ letterSpacing: 1,
+ paddingHorizontal: 10,
+ paddingVertical: 3,
+ position: 'absolute',
+ left: 6,
+ bottom: 6,
+ },
+})
diff --git a/src/view/com/util/images/Image.tsx b/src/view/com/util/images/Image.tsx
index e3d0d7fcc2..e779fa3787 100644
--- a/src/view/com/util/images/Image.tsx
+++ b/src/view/com/util/images/Image.tsx
@@ -8,5 +8,7 @@ export function HighPriorityImage({source, ...props}: HighPriorityImageProps) {
const updatedSource = {
uri: typeof source === 'object' && source ? source.uri : '',
} satisfies ImageSource
- return
+ return (
+
+ )
}
diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx
index 40f1948d69..14a8dd7e71 100644
--- a/src/view/com/util/images/ImageHorzList.tsx
+++ b/src/view/com/util/images/ImageHorzList.tsx
@@ -1,28 +1,25 @@
import React from 'react'
-import {
- StyleProp,
- StyleSheet,
- TouchableWithoutFeedback,
- View,
- ViewStyle,
-} from 'react-native'
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {Image} from 'expo-image'
+import {AppBskyEmbedImages} from '@atproto/api'
-export function ImageHorzList({
- uris,
- onPress,
- style,
-}: {
- uris: string[]
- onPress?: (index: number) => void
+interface Props {
+ images: AppBskyEmbedImages.ViewImage[]
style?: StyleProp
-}) {
+}
+
+export function ImageHorzList({images, style}: Props) {
return (
- {uris.map((uri, i) => (
- onPress?.(i)}>
-
-
+ {images.map(({thumb, alt}) => (
+
))}
)
diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx
index f4fe59522e..4c09013043 100644
--- a/src/view/com/util/images/ImageLayoutGrid.tsx
+++ b/src/view/com/util/images/ImageLayoutGrid.tsx
@@ -3,189 +3,111 @@ import {
LayoutChangeEvent,
StyleProp,
StyleSheet,
- TouchableOpacity,
View,
ViewStyle,
} from 'react-native'
-import {Image, ImageStyle} from 'expo-image'
+import {ImageStyle} from 'expo-image'
import {Dimensions} from 'lib/media/types'
+import {AppBskyEmbedImages} from '@atproto/api'
+import {GalleryItem} from './Gallery'
-export const DELAY_PRESS_IN = 500
-
-export type ImageLayoutGridType = number
-
-export function ImageLayoutGrid({
- type,
- uris,
- onPress,
- onLongPress,
- onPressIn,
- style,
-}: {
- type: ImageLayoutGridType
- uris: string[]
+interface ImageLayoutGridProps {
+ images: AppBskyEmbedImages.ViewImage[]
onPress?: (index: number) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
style?: StyleProp
-}) {
+}
+
+export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) {
const [containerInfo, setContainerInfo] = useState()
const onLayout = (evt: LayoutChangeEvent) => {
+ const {width, height} = evt.nativeEvent.layout
setContainerInfo({
- width: evt.nativeEvent.layout.width,
- height: evt.nativeEvent.layout.height,
+ width,
+ height,
})
}
return (
{containerInfo ? (
-
+
) : undefined}
)
}
-function ImageLayoutGridInner({
- type,
- uris,
- onPress,
- onLongPress,
- onPressIn,
- containerInfo,
-}: {
- type: ImageLayoutGridType
- uris: string[]
+interface ImageLayoutGridInnerProps {
+ images: AppBskyEmbedImages.ViewImage[]
onPress?: (index: number) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
containerInfo: Dimensions
-}) {
+}
+
+function ImageLayoutGridInner({
+ containerInfo,
+ ...props
+}: ImageLayoutGridInnerProps) {
+ const count = props.images.length
const size1 = useMemo(() => {
- if (type === 3) {
+ if (count === 3) {
const size = (containerInfo.width - 10) / 3
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
} else {
const size = (containerInfo.width - 5) / 2
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
}
- }, [type, containerInfo])
+ }, [count, containerInfo])
const size2 = React.useMemo(() => {
- if (type === 3) {
+ if (count === 3) {
const size = ((containerInfo.width - 10) / 3) * 2 + 5
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
} else {
const size = (containerInfo.width - 5) / 2
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
}
- }, [type, containerInfo])
+ }, [count, containerInfo])
- if (type === 2) {
- return (
-
- onPress?.(0)}
- onPressIn={() => onPressIn?.(0)}
- onLongPress={() => onLongPress?.(0)}>
-
-
-
- onPress?.(1)}
- onPressIn={() => onPressIn?.(1)}
- onLongPress={() => onLongPress?.(1)}>
-
-
-
- )
- }
- if (type === 3) {
- return (
-
- onPress?.(0)}
- onPressIn={() => onPressIn?.(0)}
- onLongPress={() => onLongPress?.(0)}>
-
-
-
-
- onPress?.(1)}
- onPressIn={() => onPressIn?.(1)}
- onLongPress={() => onLongPress?.(1)}>
-
-
-
- onPress?.(2)}
- onPressIn={() => onPressIn?.(2)}
- onLongPress={() => onLongPress?.(2)}>
-
-
+ switch (count) {
+ case 2:
+ return (
+
+
+
-
- )
- }
- if (type === 4) {
- return (
-
-
- onPress?.(0)}
- onPressIn={() => onPressIn?.(0)}
- onLongPress={() => onLongPress?.(0)}>
-
-
-
- onPress?.(2)}
- onPressIn={() => onPressIn?.(2)}
- onLongPress={() => onLongPress?.(2)}>
-
-
+ )
+ case 3:
+ return (
+
+
+
+
+
+
-
-
- onPress?.(1)}
- onPressIn={() => onPressIn?.(1)}
- onLongPress={() => onLongPress?.(1)}>
-
-
-
- onPress?.(3)}
- onPressIn={() => onPressIn?.(3)}
- onLongPress={() => onLongPress?.(3)}>
-
-
+ )
+ case 4:
+ return (
+
+
+
+
+
+
+
+
+
-
- )
+ )
+ default:
+ return null
}
- return
}
const styles = StyleSheet.create({
- flexRow: {flexDirection: 'row'},
- wSpace: {width: 5},
- hSpace: {height: 5},
+ flexRow: {flexDirection: 'row', gap: 5},
+ flexColumn: {flexDirection: 'column', gap: 5},
})
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx
index 22a8fbadaa..fefc540c0a 100644
--- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx
@@ -1,8 +1,8 @@
import React from 'react'
import {StyleSheet, TouchableOpacity} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
-import {UpIcon} from 'lib/icons'
import {LoadLatestBtn as LoadLatestBtnMobile} from './LoadLatestBtnMobile'
import {isMobileWeb} from 'platform/detection'
@@ -11,48 +11,97 @@ const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
export const LoadLatestBtn = ({
onPress,
label,
+ showIndicator,
+ minimalShellMode,
}: {
onPress: () => void
label: string
+ showIndicator: boolean
+ minimalShellMode?: boolean
}) => {
const pal = usePalette('default')
if (isMobileWeb) {
- return
+ return (
+
+ )
}
return (
-
-
-
- Load new {label}
-
-
+ <>
+ {showIndicator && (
+
+
+ {label}
+
+
+ )}
+
+
+
+
+
+ >
)
}
const styles = StyleSheet.create({
loadLatest: {
flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
position: 'absolute',
left: '50vw',
// @ts-ignore web only -prf
- transform: 'translateX(-50%)',
- top: 30,
- shadowColor: '#000',
- shadowOpacity: 0.2,
- shadowOffset: {width: 0, height: 2},
- shadowRadius: 4,
- paddingLeft: 20,
- paddingRight: 24,
- paddingVertical: 10,
+ transform: 'translateX(-282px)',
+ bottom: 40,
+ width: 54,
+ height: 54,
borderRadius: 30,
borderWidth: 1,
},
icon: {
position: 'relative',
top: 2,
- marginRight: 5,
+ },
+ loadLatestCentered: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ position: 'absolute',
+ left: '50vw',
+ // @ts-ignore web only -prf
+ transform: 'translateX(-50%)',
+ top: 60,
+ paddingHorizontal: 24,
+ paddingVertical: 14,
+ borderRadius: 30,
+ borderWidth: 1,
+ },
+ loadLatestCenteredMinimal: {
+ top: 20,
},
})
diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
index 75a812760c..412b9b803d 100644
--- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
@@ -1,38 +1,46 @@
import React from 'react'
-import {StyleSheet, TouchableOpacity} from 'react-native'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite'
-import LinearGradient from 'react-native-linear-gradient'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {Text} from '../text/Text'
-import {colors, gradients} from 'lib/styles'
import {clamp} from 'lodash'
import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {colors} from 'lib/styles'
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
export const LoadLatestBtn = observer(
- ({onPress, label}: {onPress: () => void; label: string}) => {
+ ({
+ onPress,
+ label,
+ showIndicator,
+ }: {
+ onPress: () => void
+ label: string
+ showIndicator: boolean
+ minimalShellMode?: boolean // NOTE not used on mobile -prf
+ }) => {
const store = useStores()
+ const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets()
return (
-
-
- Load new {label}
-
-
+ hitSlop={HITSLOP}
+ accessibilityRole="button"
+ accessibilityLabel={label}
+ accessibilityHint="">
+
+ {showIndicator && }
)
},
@@ -41,19 +49,24 @@ export const LoadLatestBtn = observer(
const styles = StyleSheet.create({
loadLatest: {
position: 'absolute',
- left: 20,
+ left: 18,
bottom: 35,
- shadowColor: '#000',
- shadowOpacity: 0.3,
- shadowOffset: {width: 0, height: 1},
- },
- loadLatestInner: {
+ borderWidth: 1,
+ width: 52,
+ height: 52,
+ borderRadius: 26,
flexDirection: 'row',
- paddingHorizontal: 14,
- paddingVertical: 10,
- borderRadius: 30,
+ alignItems: 'center',
+ justifyContent: 'center',
},
- loadLatestText: {
- color: colors.white,
+ indicator: {
+ position: 'absolute',
+ top: 3,
+ right: 3,
+ backgroundColor: colors.blue3,
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ borderWidth: 1,
},
})
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
index 42a97cd347..ac5c8395d4 100644
--- a/src/view/com/util/moderation/ContentHider.tsx
+++ b/src/view/com/util/moderation/ContentHider.tsx
@@ -1,37 +1,36 @@
import React from 'react'
-import {
- StyleProp,
- StyleSheet,
- TouchableOpacity,
- View,
- ViewStyle,
-} from 'react-native'
-import {ComAtprotoLabelDefs} from '@atproto/api'
+import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
import {Text} from '../text/Text'
import {addStyle} from 'lib/styles'
+import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
export function ContentHider({
testID,
- isMuted,
- labels,
+ moderation,
style,
containerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
- isMuted?: boolean
- labels: ComAtprotoLabelDefs.Label[] | undefined
+ moderation: ModerationBehavior
style?: StyleProp
containerStyle?: StyleProp
}>) {
const pal = usePalette('default')
const [override, setOverride] = React.useState(false)
- const store = useStores()
- const labelPref = store.preferences.getLabelPreference(labels)
+ const onPressShow = React.useCallback(() => {
+ setOverride(true)
+ }, [setOverride])
+ const onPressHide = React.useCallback(() => {
+ setOverride(false)
+ }, [setOverride])
- if (!isMuted && labelPref.pref === 'show') {
+ if (
+ moderation.behavior === ModerationBehaviorCode.Show ||
+ moderation.behavior === ModerationBehaviorCode.Warn ||
+ moderation.behavior === ModerationBehaviorCode.WarnImages
+ ) {
return (
{children}
@@ -39,33 +38,35 @@ export function ContentHider({
)
}
- if (labelPref.pref === 'hide') {
+ if (moderation.behavior === ModerationBehaviorCode.Hide) {
return null
}
return (
-
- {isMuted ? (
- <>Post from an account you muted.>
- ) : (
- <>Warning: {labelPref.desc.warning || labelPref.desc.title}>
- )}
+ {moderation.reason || 'Content warning'}
- setOverride(v => !v)}>
-
+
+
{override ? 'Hide' : 'Show'}
-
-
+
+
{override && (
diff --git a/src/view/com/util/moderation/ImageHider.tsx b/src/view/com/util/moderation/ImageHider.tsx
new file mode 100644
index 0000000000..40add5b67b
--- /dev/null
+++ b/src/view/com/util/moderation/ImageHider.tsx
@@ -0,0 +1,128 @@
+import React from 'react'
+import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {usePalette} from 'lib/hooks/usePalette'
+import {Text} from '../text/Text'
+import {BlurView} from '../BlurView'
+import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
+import {isAndroid} from 'platform/detection'
+
+export function ImageHider({
+ testID,
+ moderation,
+ style,
+ containerStyle,
+ children,
+}: React.PropsWithChildren<{
+ testID?: string
+ moderation: ModerationBehavior
+ style?: StyleProp
+ containerStyle?: StyleProp
+}>) {
+ const pal = usePalette('default')
+ const [override, setOverride] = React.useState(false)
+ const onPressShow = React.useCallback(() => {
+ setOverride(true)
+ }, [setOverride])
+ const onPressHide = React.useCallback(() => {
+ setOverride(false)
+ }, [setOverride])
+
+ if (moderation.behavior === ModerationBehaviorCode.Hide) {
+ return null
+ }
+
+ if (moderation.behavior !== ModerationBehaviorCode.WarnImages) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ return (
+
+
+ {children}
+
+ {override ? (
+
+
+ Hide
+
+
+ ) : (
+ <>
+ {isAndroid ? (
+ /* android has an issue that breaks the blurview */
+ /* see https://github.com/Kureev/react-native-blur/issues/486 */
+
+ ) : (
+
+ )}
+
+
+
+ {moderation.reason || 'Content warning'}
+
+
+ Show
+
+
+
+ >
+ )}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'relative',
+ marginBottom: 10,
+ },
+ overlay: {
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ },
+ blurView: {
+ borderRadius: 8,
+ },
+ coverView: {
+ borderRadius: 8,
+ },
+ info: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ showBtn: {
+ flexDirection: 'row',
+ gap: 8,
+ paddingHorizontal: 18,
+ paddingVertical: 14,
+ borderRadius: 24,
+ },
+ hideBtn: {
+ position: 'absolute',
+ left: 8,
+ bottom: 20,
+ paddingHorizontal: 8,
+ paddingVertical: 6,
+ borderRadius: 8,
+ },
+})
diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx
index bafc7aecf5..50ccf595b0 100644
--- a/src/view/com/util/moderation/PostHider.tsx
+++ b/src/view/com/util/moderation/PostHider.tsx
@@ -1,82 +1,82 @@
-import React from 'react'
-import {
- StyleProp,
- StyleSheet,
- TouchableOpacity,
- View,
- ViewStyle,
-} from 'react-native'
-import {ComAtprotoLabelDefs} from '@atproto/api'
+import React, {ComponentProps} from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {usePalette} from 'lib/hooks/usePalette'
import {Link} from '../Link'
import {Text} from '../text/Text'
import {addStyle} from 'lib/styles'
-import {useStores} from 'state/index'
+import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types'
+
+interface Props extends ComponentProps {
+ // testID?: string
+ // href?: string
+ // style: StyleProp
+ moderation: ModerationBehavior
+}
export function PostHider({
testID,
href,
- isMuted,
- labels,
+ moderation,
style,
children,
-}: React.PropsWithChildren<{
- testID?: string
- href: string
- isMuted: boolean | undefined
- labels: ComAtprotoLabelDefs.Label[] | undefined
- style: StyleProp
-}>) {
- const store = useStores()
+ ...props
+}: Props) {
const pal = usePalette('default')
const [override, setOverride] = React.useState(false)
const bg = override ? pal.viewLight : pal.view
- const labelPref = store.preferences.getLabelPreference(labels)
- if (labelPref.pref === 'hide') {
- return <>>
+ if (moderation.behavior === ModerationBehaviorCode.Hide) {
+ return null
}
- if (!isMuted) {
- // NOTE: any further label enforcement should occur in ContentContainer
+ if (moderation.behavior === ModerationBehaviorCode.Warn) {
return (
-
- {children}
-
+ <>
+
+
+
+ {moderation.reason || 'Content warning'}
+
+ setOverride(v => !v)}
+ accessibilityRole="button">
+
+ {override ? 'Hide' : 'Show'} post
+
+
+
+ {override && (
+
+
+ {children}
+
+
+ )}
+ >
)
}
+ // NOTE: any further label enforcement should occur in ContentContainer
return (
- <>
-
-
-
- Post from an account you muted.
-
- setOverride(v => !v)}>
-
- {override ? 'Hide' : 'Show'} post
-
-
-
- {override && (
-
-
- {children}
-
-
- )}
- >
+
+ {children}
+
)
}
diff --git a/src/view/com/util/moderation/ProfileHeaderLabels.tsx b/src/view/com/util/moderation/ProfileHeaderLabels.tsx
deleted file mode 100644
index c6fbfaf6ba..0000000000
--- a/src/view/com/util/moderation/ProfileHeaderLabels.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
-import {ComAtprotoLabelDefs} from '@atproto/api'
-import {
- FontAwesomeIcon,
- FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
-import {Text} from '../text/Text'
-import {usePalette} from 'lib/hooks/usePalette'
-import {getLabelValueGroup} from 'lib/labeling/helpers'
-
-export function ProfileHeaderLabels({
- labels,
-}: {
- labels: ComAtprotoLabelDefs.Label[] | undefined
-}) {
- const palErr = usePalette('error')
- if (!labels?.length) {
- return null
- }
- return (
- <>
- {labels.map((label, i) => {
- const labelGroup = getLabelValueGroup(label?.val || '')
- return (
-
-
-
- This account has been flagged for{' '}
- {(labelGroup.warning || labelGroup.title).toLocaleLowerCase()}.
-
-
- )
- })}
- >
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 10,
- borderWidth: 1,
- borderRadius: 6,
- paddingHorizontal: 10,
- paddingVertical: 8,
- },
-})
diff --git a/src/view/com/util/moderation/ProfileHeaderWarnings.tsx b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx
new file mode 100644
index 0000000000..7a1a8e295b
--- /dev/null
+++ b/src/view/com/util/moderation/ProfileHeaderWarnings.tsx
@@ -0,0 +1,44 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {Text} from '../text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {ModerationBehavior, ModerationBehaviorCode} from 'lib/labeling/types'
+
+export function ProfileHeaderWarnings({
+ moderation,
+}: {
+ moderation: ModerationBehavior
+}) {
+ const palErr = usePalette('error')
+ if (moderation.behavior === ModerationBehaviorCode.Show) {
+ return null
+ }
+ return (
+
+
+
+ This account has been flagged: {moderation.reason}
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 10,
+ borderWidth: 1,
+ borderRadius: 6,
+ paddingHorizontal: 10,
+ paddingVertical: 8,
+ },
+})
diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx
new file mode 100644
index 0000000000..2e7b07e1a3
--- /dev/null
+++ b/src/view/com/util/moderation/ScreenHider.tsx
@@ -0,0 +1,129 @@
+import React from 'react'
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {useNavigation} from '@react-navigation/native'
+import {usePalette} from 'lib/hooks/usePalette'
+import {NavigationProp} from 'lib/routes/types'
+import {Text} from '../text/Text'
+import {Button} from '../forms/Button'
+import {isDesktopWeb} from 'platform/detection'
+import {ModerationBehaviorCode, ModerationBehavior} from 'lib/labeling/types'
+
+export function ScreenHider({
+ testID,
+ screenDescription,
+ moderation,
+ style,
+ containerStyle,
+ children,
+}: React.PropsWithChildren<{
+ testID?: string
+ screenDescription: string
+ moderation: ModerationBehavior
+ style?: StyleProp
+ containerStyle?: StyleProp
+}>) {
+ const pal = usePalette('default')
+ const palInverted = usePalette('inverted')
+ const [override, setOverride] = React.useState(false)
+ const navigation = useNavigation()
+
+ const onPressBack = React.useCallback(() => {
+ if (navigation.canGoBack()) {
+ navigation.goBack()
+ } else {
+ navigation.navigate('Home')
+ }
+ }, [navigation])
+
+ if (moderation.behavior !== ModerationBehaviorCode.Hide || override) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+ Content Warning
+
+
+ This {screenDescription} has been flagged:{' '}
+ {moderation.reason || 'Content warning'}
+
+ {!isDesktopWeb && }
+
+
+
+ Go back
+
+
+ {!moderation.noOverride && (
+ setOverride(v => !v)}
+ style={styles.btn}>
+
+ Show anyway
+
+
+ )}
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ spacer: {
+ flex: 1,
+ },
+ container: {
+ flex: 1,
+ paddingTop: 100,
+ paddingBottom: 150,
+ },
+ iconContainer: {
+ alignItems: 'center',
+ marginBottom: 10,
+ },
+ icon: {
+ borderRadius: 25,
+ width: 50,
+ height: 50,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ title: {
+ textAlign: 'center',
+ marginBottom: 10,
+ },
+ description: {
+ marginBottom: 10,
+ paddingHorizontal: 20,
+ textAlign: 'center',
+ },
+ btnContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginVertical: 10,
+ gap: 10,
+ },
+ btn: {
+ paddingHorizontal: 20,
+ paddingVertical: 14,
+ },
+})
diff --git a/src/view/com/util/numeric/format.ts b/src/view/com/util/numeric/format.ts
new file mode 100644
index 0000000000..7aa5a4f4df
--- /dev/null
+++ b/src/view/com/util/numeric/format.ts
@@ -0,0 +1,15 @@
+export const formatCount = (num: number) =>
+ Intl.NumberFormat('en-US', {
+ notation: 'compact',
+ maximumFractionDigits: 1,
+ }).format(num)
+
+export function formatCountShortOnly(num: number): string {
+ if (num >= 1000000) {
+ return (num / 1000000).toFixed(1) + 'M'
+ }
+ if (num >= 1000) {
+ return (num / 1000).toFixed(1) + 'K'
+ }
+ return String(num)
+}
diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
similarity index 61%
rename from src/view/com/util/PostCtrls.tsx
rename to src/view/com/util/post-ctrls/PostCtrls.tsx
index 6441d3c77d..12d4c48c89 100644
--- a/src/view/com/util/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {useCallback} from 'react'
import {
StyleProp,
StyleSheet,
@@ -10,23 +10,19 @@ import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
-import ReactNativeHapticFeedback from 'react-native-haptic-feedback'
// DISABLED see #135
// import {
// TriggerableAnimated,
// TriggerableAnimatedRef,
// } from './anim/TriggerableAnimated'
-import {Text} from './text/Text'
-import {PostDropdownBtn} from './forms/DropdownButton'
-import {
- HeartIcon,
- HeartIconSolid,
- RepostIcon,
- CommentBottomArrow,
-} from 'lib/icons'
+import {Text} from '../text/Text'
+import {PostDropdownBtn} from '../forms/DropdownButton'
+import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
import {s, colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {useStores} from 'state/index'
+import {RepostButton} from './RepostButton'
+import {Haptics} from 'lib/haptics'
interface PostCtrlsOpts {
itemUri: string
@@ -48,11 +44,13 @@ interface PostCtrlsOpts {
likeCount?: number
isReposted: boolean
isLiked: boolean
+ isThreadMuted: boolean
onPressReply: () => void
onPressToggleRepost: () => Promise
onPressToggleLike: () => Promise
onCopyPostText: () => void
onOpenTranslate: () => void
+ onToggleThreadMute: () => void
onDeletePost: () => void
}
@@ -106,10 +104,10 @@ export function PostCtrls(opts: PostCtrlsOpts) {
// DISABLED see #135
// const repostRef = React.useRef(null)
// const likeRef = React.useRef(null)
- const onRepost = () => {
+ const onRepost = useCallback(() => {
store.shell.closeModal()
if (!opts.isReposted) {
- ReactNativeHapticFeedback.trigger('impactMedium')
+ Haptics.default()
opts.onPressToggleRepost().catch(_e => undefined)
// DISABLED see #135
// repostRef.current?.trigger(
@@ -122,9 +120,9 @@ export function PostCtrls(opts: PostCtrlsOpts) {
} else {
opts.onPressToggleRepost().catch(_e => undefined)
}
- }
+ }, [opts, store.shell])
- const onQuote = () => {
+ const onQuote = useCallback(() => {
store.shell.closeModal()
store.shell.openComposer({
quote: {
@@ -135,21 +133,19 @@ export function PostCtrls(opts: PostCtrlsOpts) {
indexedAt: opts.indexedAt,
},
})
- ReactNativeHapticFeedback.trigger('impactMedium')
- }
-
- const onPressToggleRepostWrapper = () => {
- store.shell.openModal({
- name: 'repost',
- onRepost: onRepost,
- onQuote: onQuote,
- isReposted: opts.isReposted,
- })
- }
+ Haptics.default()
+ }, [
+ opts.author,
+ opts.indexedAt,
+ opts.itemCid,
+ opts.itemUri,
+ opts.text,
+ store.shell,
+ ])
const onPressToggleLikeWrapper = async () => {
if (!opts.isLiked) {
- ReactNativeHapticFeedback.trigger('impactMedium')
+ Haptics.default()
await opts.onPressToggleLike().catch(_e => undefined)
// DISABLED see #135
// likeRef.current?.trigger(
@@ -168,83 +164,58 @@ export function PostCtrls(opts: PostCtrlsOpts) {
return (
-
-
-
+
+ {typeof opts.replyCount !== 'undefined' ? (
+
+ {opts.replyCount}
+
+ ) : undefined}
+
+
+
+ {opts.isLiked ? (
+
+ ) : (
+
- {typeof opts.replyCount !== 'undefined' ? (
-
- {opts.replyCount}
-
- ) : undefined}
-
-
-
-
- )
- : defaultCtrlColor
- }
- strokeWidth={2.4}
- size={opts.big ? 24 : 20}
- />
- {typeof opts.repostCount !== 'undefined' ? (
-
- {opts.repostCount}
-
- ) : undefined}
-
-
-
-
- {opts.isLiked ? (
- }
- size={opts.big ? 22 : 16}
- />
- ) : (
-
- )}
- {typeof opts.likeCount !== 'undefined' ? (
-
- {opts.likeCount}
-
- ) : undefined}
-
-
+ opts.isLiked
+ ? [s.bold, s.red3, s.f15, s.ml5]
+ : [defaultCtrlColor, s.f15, s.ml5]
+ }>
+ {opts.likeCount}
+
+ ) : undefined}
+
{opts.big ? undefined : (
void
+ onQuote: () => void
+}
+
+export const RepostButton = ({
+ isReposted,
+ repostCount,
+ big,
+ onRepost,
+ onQuote,
+}: Props) => {
+ const store = useStores()
+ const theme = useTheme()
+
+ const defaultControlColor = React.useMemo(
+ () => ({
+ color: theme.palette.default.postCtrl,
+ }),
+ [theme],
+ )
+
+ const onPressToggleRepostWrapper = useCallback(() => {
+ store.shell.openModal({
+ name: 'repost',
+ onRepost: onRepost,
+ onQuote: onQuote,
+ isReposted,
+ })
+ }, [onRepost, onQuote, isReposted, store.shell])
+
+ return (
+
+ )
+ : defaultControlColor
+ }
+ strokeWidth={2.4}
+ size={big ? 24 : 20}
+ />
+ {typeof repostCount !== 'undefined' ? (
+
+ {repostCount}
+
+ ) : undefined}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ control: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 5,
+ margin: -5,
+ },
+ reposted: {
+ color: colors.green3,
+ },
+ repostCount: {
+ color: 'currentColor',
+ },
+})
diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx
new file mode 100644
index 0000000000..66cc0d123b
--- /dev/null
+++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx
@@ -0,0 +1,86 @@
+import React, {useMemo} from 'react'
+import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {RepostIcon} from 'lib/icons'
+import {DropdownButton} from '../forms/DropdownButton'
+import {colors} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {Text} from '../text/Text'
+
+interface Props {
+ isReposted: boolean
+ repostCount?: number
+ big?: boolean
+ onRepost: () => void
+ onQuote: () => void
+}
+
+export const RepostButton = ({
+ isReposted,
+ repostCount,
+ big,
+ onRepost,
+ onQuote,
+}: Props) => {
+ const theme = useTheme()
+
+ const defaultControlColor = React.useMemo(
+ () => ({
+ color: theme.palette.default.postCtrl,
+ }),
+ [theme],
+ )
+
+ const items = useMemo(
+ () => [
+ {
+ label: isReposted ? 'Undo repost' : 'Repost',
+ icon: 'retweet' as const,
+ onPress: onRepost,
+ },
+ {label: 'Quote post', icon: 'quote-left' as const, onPress: onQuote},
+ ],
+ [isReposted, onRepost, onQuote],
+ )
+
+ return (
+
+ ,
+ ]}>
+
+ {typeof repostCount !== 'undefined' ? (
+
+ {repostCount ?? 0}
+
+ ) : undefined}
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ control: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ },
+ reposted: {
+ color: colors.green3,
+ },
+ repostCount: {
+ color: 'currentColor',
+ },
+})
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index c15986b76f..8156d7732b 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -5,6 +5,7 @@ import {
View,
ViewStyle,
Image as RNImage,
+ Text,
} from 'react-native'
import {
AppBskyEmbedImages,
@@ -12,18 +13,20 @@ import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedPost,
+ AppBskyFeedDefs,
} from '@atproto/api'
import {Link} from '../Link'
-import {AutoSizedImage} from '../images/AutoSizedImage'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {ImagesLightbox} from 'state/models/ui/shell'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
-import {saveImageModal} from 'lib/media/manip'
import {YoutubeEmbed} from './YoutubeEmbed'
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {getYoutubeVideoId} from 'lib/strings/url-helpers'
import QuoteEmbed from './QuoteEmbed'
+import {AutoSizedImage} from '../images/AutoSizedImage'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
type Embed =
| AppBskyEmbedRecord.View
@@ -42,6 +45,8 @@ export function PostEmbeds({
const pal = usePalette('default')
const store = useStores()
+ // quote post with media
+ // =
if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
@@ -65,6 +70,8 @@ export function PostEmbeds({
)
}
+ // quote post
+ // =
if (AppBskyEmbedRecord.isView(embed)) {
if (
AppBskyEmbedRecord.isViewRecord(embed.record) &&
@@ -87,55 +94,58 @@ export function PostEmbeds({
}
}
+ // image embed
+ // =
if (AppBskyEmbedImages.isView(embed)) {
- if (embed.images.length > 0) {
- const uris = embed.images.map(img => img.fullsize)
+ const {images} = embed
+
+ if (images.length > 0) {
+ const items = embed.images.map(img => ({uri: img.fullsize, alt: img.alt}))
const openLightbox = (index: number) => {
- store.shell.openLightbox(new ImagesLightbox(uris, index))
- }
- const onLongPress = (index: number) => {
- saveImageModal({uri: uris[index]})
+ store.shell.openLightbox(new ImagesLightbox(items, index))
}
const onPressIn = (index: number) => {
- const firstImageToShow = uris[index]
+ const firstImageToShow = items[index].uri
RNImage.prefetch(firstImageToShow)
- uris.forEach(uri => {
- if (firstImageToShow !== uri) {
- // First image already prefeched above
- RNImage.prefetch(uri)
+ items.forEach(item => {
+ if (firstImageToShow !== item.uri) {
+ // First image already prefetched above
+ RNImage.prefetch(item.uri)
}
})
}
- switch (embed.images.length) {
- case 1:
- return (
-
- openLightbox(0)}
- onLongPress={() => onLongPress(0)}
- onPressIn={() => onPressIn(0)}
- style={styles.singleImage}
- />
-
- )
- default:
- return (
-
- img.thumb)}
- onPress={openLightbox}
- onLongPress={onLongPress}
- onPressIn={onPressIn}
- />
-
- )
+ if (images.length === 1) {
+ const {alt, thumb} = images[0]
+ return (
+
+ openLightbox(0)}
+ onPressIn={() => onPressIn(0)}
+ style={styles.singleImage}>
+ {alt === '' ? null : ALT }
+
+
+ )
}
+
+ return (
+
+
+
+ )
}
}
+ // external link embed
+ // =
if (AppBskyEmbedExternal.isView(embed)) {
const link = embed.external
const youtubeVideoId = getYoutubeVideoId(link.uri)
@@ -153,9 +163,35 @@ export function PostEmbeds({
)
}
+
+ // custom feed embed (i.e. generator view)
+ // =
+ if (
+ AppBskyEmbedRecord.isView(embed) &&
+ AppBskyFeedDefs.isGeneratorView(embed.record)
+ ) {
+ return
+ }
+
return
}
+function CustomFeedEmbed({record}: {record: AppBskyFeedDefs.GeneratorView}) {
+ const pal = usePalette('default')
+ const store = useStores()
+ const item = React.useMemo(
+ () => new CustomFeedModel(store, record),
+ [store, record],
+ )
+ return (
+
+ )
+}
+
const styles = StyleSheet.create({
stackContainer: {
gap: 6,
@@ -172,4 +208,24 @@ const styles = StyleSheet.create({
borderRadius: 8,
marginTop: 4,
},
+ customFeedOuter: {
+ borderWidth: 1,
+ borderRadius: 8,
+ marginTop: 4,
+ paddingHorizontal: 12,
+ paddingVertical: 12,
+ },
+ alt: {
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
+ borderRadius: 6,
+ color: 'white',
+ fontSize: 12,
+ fontWeight: 'bold',
+ letterSpacing: 1,
+ paddingHorizontal: 10,
+ paddingVertical: 3,
+ position: 'absolute',
+ left: 6,
+ bottom: 6,
+ },
})
diff --git a/src/view/index.ts b/src/view/index.ts
index e6e3426974..f06bdacccc 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -8,20 +8,20 @@ import {faAngleUp} from '@fortawesome/free-solid-svg-icons/faAngleUp'
import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight'
import {faArrowUp} from '@fortawesome/free-solid-svg-icons/faArrowUp'
-import {
- faArrowRightFromBracket,
- faQuoteLeft,
-} from '@fortawesome/free-solid-svg-icons'
+import {faArrowDown} from '@fortawesome/free-solid-svg-icons/faArrowDown'
+import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowRightFromBracket'
import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare'
import {faArrowRotateLeft} from '@fortawesome/free-solid-svg-icons/faArrowRotateLeft'
import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate'
import {faAt} from '@fortawesome/free-solid-svg-icons/faAt'
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
+import {faBan} from '@fortawesome/free-solid-svg-icons/faBan'
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell'
import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark'
import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark'
+import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar'
import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera'
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'
import {faCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck'
@@ -30,6 +30,7 @@ import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser'
import {faClone} from '@fortawesome/free-solid-svg-icons/faClone'
import {faClone as farClone} from '@fortawesome/free-regular-svg-icons/faClone'
import {faComment} from '@fortawesome/free-regular-svg-icons/faComment'
+import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash'
import {faCompass} from '@fortawesome/free-regular-svg-icons/faCompass'
import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis'
import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope'
@@ -38,6 +39,8 @@ import {faEye} from '@fortawesome/free-solid-svg-icons/faEye'
import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash'
import {faGear} from '@fortawesome/free-solid-svg-icons/faGear'
import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe'
+import {faHand} from '@fortawesome/free-solid-svg-icons/faHand'
+import {faHand as farHand} from '@fortawesome/free-regular-svg-icons/faHand'
import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart'
import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
@@ -46,6 +49,7 @@ import {faImage} from '@fortawesome/free-solid-svg-icons/faImage'
import {faInfo} from '@fortawesome/free-solid-svg-icons/faInfo'
import {faLanguage} from '@fortawesome/free-solid-svg-icons/faLanguage'
import {faLink} from '@fortawesome/free-solid-svg-icons/faLink'
+import {faListUl} from '@fortawesome/free-solid-svg-icons/faListUl'
import {faLock} from '@fortawesome/free-solid-svg-icons/faLock'
import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage'
@@ -55,25 +59,30 @@ import {faPen} from '@fortawesome/free-solid-svg-icons/faPen'
import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib'
import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare'
import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'
+import {faQuoteLeft} from '@fortawesome/free-solid-svg-icons/faQuoteLeft'
+import {faReply} from '@fortawesome/free-solid-svg-icons/faReply'
+import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet'
+import {faRss} from '@fortawesome/free-solid-svg-icons/faRss'
+import {faSatelliteDish} from '@fortawesome/free-solid-svg-icons/faSatelliteDish'
import {faShare} from '@fortawesome/free-solid-svg-icons/faShare'
import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare'
import {faShield} from '@fortawesome/free-solid-svg-icons/faShield'
import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus'
import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal'
-import {faReply} from '@fortawesome/free-solid-svg-icons/faReply'
-import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet'
-import {faRss} from '@fortawesome/free-solid-svg-icons/faRss'
+import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
+import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
import {faUser} from '@fortawesome/free-regular-svg-icons/faUser'
import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers'
import {faUserCheck} from '@fortawesome/free-solid-svg-icons/faUserCheck'
+import {faUserSlash} from '@fortawesome/free-solid-svg-icons/faUserSlash'
import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus'
import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark'
-import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
-import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
+import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash'
import {faX} from '@fortawesome/free-solid-svg-icons/faX'
import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
import {faPause} from '@fortawesome/free-solid-svg-icons/faPause'
+import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
export function setup() {
library.add(
@@ -85,17 +94,20 @@ export function setup() {
faArrowLeft,
faArrowRight,
faArrowUp,
+ faArrowDown,
faArrowRightFromBracket,
faArrowUpFromBracket,
faArrowUpRightFromSquare,
faArrowRotateLeft,
faArrowsRotate,
faAt,
+ faBan,
faBars,
faBell,
farBell,
faBookmark,
farBookmark,
+ farCalendar,
faCamera,
faCheck,
faCircleCheck,
@@ -104,6 +116,7 @@ export function setup() {
faClone,
farClone,
faComment,
+ faCommentSlash,
faCompass,
faEllipsis,
faEnvelope,
@@ -112,6 +125,8 @@ export function setup() {
farEyeSlash,
faGear,
faGlobe,
+ faHand,
+ farHand,
faHeart,
fasHeart,
faHouse,
@@ -120,6 +135,7 @@ export function setup() {
faInfo,
faLanguage,
faLink,
+ faListUl,
faLock,
faMagnifyingGlass,
faMessage,
@@ -133,6 +149,7 @@ export function setup() {
faReply,
faRetweet,
faRss,
+ faSatelliteDish,
faShare,
faShareFromSquare,
faShield,
@@ -141,10 +158,13 @@ export function setup() {
faUser,
faUsers,
faUserCheck,
+ faUserSlash,
faUserPlus,
faUserXmark,
+ faUsersSlash,
faTicket,
faTrashCan,
+ faThumbtack,
faX,
faXmark,
faPlay,
diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx
new file mode 100644
index 0000000000..ca60787d58
--- /dev/null
+++ b/src/view/screens/AppPasswords.tsx
@@ -0,0 +1,284 @@
+import React from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {ScrollView} from 'react-native-gesture-handler'
+import {Text} from '../com/util/text/Text'
+import {Button} from '../com/util/forms/Button'
+import * as Toast from '../com/util/Toast'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb} from 'platform/detection'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {observer} from 'mobx-react-lite'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {CommonNavigatorParams} from 'lib/routes/types'
+import {useAnalytics} from 'lib/analytics'
+import {useFocusEffect} from '@react-navigation/native'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {CenteredView} from 'view/com/util/Views'
+
+type Props = NativeStackScreenProps
+export const AppPasswords = withAuthRequired(
+ observer(({}: Props) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const {screen} = useAnalytics()
+
+ useFocusEffect(
+ React.useCallback(() => {
+ screen('AppPasswords')
+ store.shell.setMinimalShellMode(false)
+ }, [screen, store]),
+ )
+
+ const onAdd = React.useCallback(async () => {
+ store.shell.openModal({name: 'add-app-password'})
+ }, [store])
+
+ // no app passwords (empty) state
+ if (store.me.appPasswords.length === 0) {
+ return (
+
+
+
+
+ You have not created any app passwords yet. You can create one by
+ pressing the button below.
+
+
+ {!isDesktopWeb && }
+
+
+
+
+ )
+ }
+
+ // has app passwords
+ return (
+
+
+
+ {store.me.appPasswords.map((password, i) => (
+
+ ))}
+ {isDesktopWeb && (
+
+
+
+ )}
+
+ {!isDesktopWeb && (
+
+
+
+ )}
+
+ )
+ }),
+)
+
+function AppPasswordsHeader() {
+ const pal = usePalette('default')
+ return (
+ <>
+
+
+ Use app passwords to login to other Bluesky clients without giving full
+ access to your account or password.
+
+ >
+ )
+}
+
+function AppPassword({
+ testID,
+ name,
+ createdAt,
+}: {
+ testID: string
+ name: string
+ createdAt: string
+}) {
+ const pal = usePalette('default')
+ const store = useStores()
+
+ const onDelete = React.useCallback(async () => {
+ store.shell.openModal({
+ name: 'confirm',
+ title: 'Delete App Password',
+ message: `Are you sure you want to delete the app password "${name}"?`,
+ async onPressConfirm() {
+ await store.me.deleteAppPassword(name)
+ Toast.show('App password deleted')
+ },
+ })
+ }, [store, name])
+
+ const {contentLanguages} = store.preferences
+
+ const primaryLocale =
+ contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
+
+ return (
+
+
+
+ {name}
+
+
+ Created{' '}
+ {Intl.DateTimeFormat(primaryLocale, {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ }).format(new Date(createdAt))}
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingBottom: isDesktopWeb ? 0 : 100,
+ },
+ containerDesktop: {
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ },
+ title: {
+ textAlign: 'center',
+ marginTop: 12,
+ marginBottom: 12,
+ },
+ description: {
+ textAlign: 'center',
+ paddingHorizontal: 20,
+ marginBottom: 14,
+ },
+ descriptionDesktop: {
+ marginTop: 14,
+ },
+
+ scrollContainer: {
+ borderTopWidth: 1,
+ marginTop: 4,
+ marginBottom: 16,
+ },
+
+ flex1: {
+ flex: 1,
+ },
+ empty: {
+ paddingHorizontal: 20,
+ paddingVertical: 20,
+ borderRadius: 16,
+ marginHorizontal: 24,
+ marginTop: 10,
+ },
+ emptyText: {
+ textAlign: 'center',
+ },
+
+ item: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderBottomWidth: 1,
+ paddingHorizontal: 20,
+ paddingVertical: 14,
+ },
+ pr10: {
+ marginRight: 10,
+ },
+ btnContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+ btnContainerDesktop: {
+ marginTop: 14,
+ },
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 32,
+ paddingHorizontal: 60,
+ paddingVertical: 14,
+ },
+ btnLabel: {
+ fontSize: 18,
+ },
+
+ trashIcon: {
+ color: 'red',
+ minWidth: 16,
+ },
+})
diff --git a/src/view/screens/CommunityGuidelines.tsx b/src/view/screens/CommunityGuidelines.tsx
index 5d00786da1..e80eba905d 100644
--- a/src/view/screens/CommunityGuidelines.tsx
+++ b/src/view/screens/CommunityGuidelines.tsx
@@ -1,14 +1,14 @@
import React from 'react'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
+import {Text} from 'view/com/util/text/Text'
+import {TextLink} from 'view/com/util/Link'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {ViewHeader} from '../com/util/ViewHeader'
import {useStores} from 'state/index'
import {ScrollView} from 'view/com/util/Views'
-import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
-import Html from '../../locale/en/community-guidelines'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
@@ -29,10 +29,14 @@ export const CommunityGuidelinesScreen = (_props: Props) => {
-
- Community Guidelines
+
+ The Community Guidelines have been moved to{' '}
+
-
diff --git a/src/view/screens/CopyrightPolicy.tsx b/src/view/screens/CopyrightPolicy.tsx
index 756a79c039..9de4dc9e72 100644
--- a/src/view/screens/CopyrightPolicy.tsx
+++ b/src/view/screens/CopyrightPolicy.tsx
@@ -1,14 +1,14 @@
import React from 'react'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
+import {Text} from 'view/com/util/text/Text'
+import {TextLink} from 'view/com/util/Link'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {ViewHeader} from '../com/util/ViewHeader'
import {useStores} from 'state/index'
import {ScrollView} from 'view/com/util/Views'
-import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
-import Html from '../../locale/en/copyright-policy'
type Props = NativeStackScreenProps
export const CopyrightPolicyScreen = (_props: Props) => {
@@ -26,10 +26,14 @@ export const CopyrightPolicyScreen = (_props: Props) => {
-
- Copyright Policy
+
+ The Copyright Policy has been moved to{' '}
+
-
diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx
new file mode 100644
index 0000000000..4149cd49d1
--- /dev/null
+++ b/src/view/screens/CustomFeed.tsx
@@ -0,0 +1,418 @@
+import React, {useMemo, useRef} from 'react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {usePalette} from 'lib/hooks/usePalette'
+import {HeartIcon, HeartIconSolid} from 'lib/icons'
+import {CommonNavigatorParams} from 'lib/routes/types'
+import {makeRecordUri} from 'lib/strings/url-helpers'
+import {colors, s} from 'lib/styles'
+import {observer} from 'mobx-react-lite'
+import {FlatList, StyleSheet, View} from 'react-native'
+import {useStores} from 'state/index'
+import {PostsFeedModel} from 'state/models/feeds/posts'
+import {useCustomFeed} from 'lib/hooks/useCustomFeed'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {Feed} from 'view/com/posts/Feed'
+import {pluralize} from 'lib/strings/helpers'
+import {TextLink} from 'view/com/util/Link'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {Button} from 'view/com/util/forms/Button'
+import {Text} from 'view/com/util/text/Text'
+import * as Toast from 'view/com/util/Toast'
+import {isDesktopWeb} from 'platform/detection'
+import {useSetTitle} from 'lib/hooks/useSetTitle'
+import {shareUrl} from 'lib/sharing'
+import {toShareUrl} from 'lib/strings/url-helpers'
+import {Haptics} from 'lib/haptics'
+import {ComposeIcon2} from 'lib/icons'
+import {FAB} from '../com/util/fab/FAB'
+import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
+import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton'
+import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
+import {EmptyState} from 'view/com/util/EmptyState'
+
+type Props = NativeStackScreenProps
+export const CustomFeedScreen = withAuthRequired(
+ observer(({route}: Props) => {
+ const store = useStores()
+ const pal = usePalette('default')
+ const {rkey, name} = route.params
+ const uri = useMemo(
+ () => makeRecordUri(name, 'app.bsky.feed.generator', rkey),
+ [rkey, name],
+ )
+ const scrollElRef = useRef(null)
+ const currentFeed = useCustomFeed(uri)
+ const algoFeed: PostsFeedModel = useMemo(() => {
+ const feed = new PostsFeedModel(store, 'custom', {
+ feed: uri,
+ })
+ feed.setup()
+ return feed
+ }, [store, uri])
+ const isPinned = store.me.savedFeeds.isPinned(uri)
+ const [onMainScroll, isScrolledDown, resetMainScroll] =
+ useOnMainScroll(store)
+ useSetTitle(currentFeed?.displayName)
+
+ const onToggleSaved = React.useCallback(async () => {
+ try {
+ Haptics.default()
+ if (currentFeed?.isSaved) {
+ await currentFeed?.unsave()
+ } else {
+ await currentFeed?.save()
+ }
+ } catch (err) {
+ Toast.show(
+ 'There was an an issue updating your feeds, please check your internet connection and try again.',
+ )
+ store.log.error('Failed up update feeds', {err})
+ }
+ }, [store, currentFeed])
+
+ const onToggleLiked = React.useCallback(async () => {
+ Haptics.default()
+ try {
+ if (currentFeed?.isLiked) {
+ await currentFeed?.unlike()
+ } else {
+ await currentFeed?.like()
+ }
+ } catch (err) {
+ Toast.show(
+ 'There was an an issue contacting the server, please check your internet connection and try again.',
+ )
+ store.log.error('Failed up toggle like', {err})
+ }
+ }, [store, currentFeed])
+
+ const onTogglePinned = React.useCallback(async () => {
+ Haptics.default()
+ store.me.savedFeeds.togglePinnedFeed(currentFeed!).catch(e => {
+ Toast.show('There was an issue contacting the server')
+ store.log.error('Failed to toggle pinned feed', {e})
+ })
+ }, [store, currentFeed])
+
+ const onPressShare = React.useCallback(() => {
+ const url = toShareUrl(`/profile/${name}/feed/${rkey}`)
+ shareUrl(url)
+ }, [name, rkey])
+
+ const onScrollToTop = React.useCallback(() => {
+ scrollElRef.current?.scrollToOffset({offset: 0, animated: true})
+ resetMainScroll()
+ }, [scrollElRef, resetMainScroll])
+
+ const onPressCompose = React.useCallback(() => {
+ store.shell.openComposer({})
+ }, [store])
+
+ const dropdownItems: DropdownItem[] = React.useMemo(() => {
+ let items: DropdownItem[] = [
+ {
+ testID: 'feedHeaderDropdownRemoveBtn',
+ label: 'Remove from my feeds',
+ onPress: onToggleSaved,
+ },
+ {
+ testID: 'feedHeaderDropdownShareBtn',
+ label: 'Share link',
+ onPress: onPressShare,
+ },
+ ]
+ return items
+ }, [onToggleSaved, onPressShare])
+
+ const renderHeaderBtns = React.useCallback(() => {
+ return (
+
+
+ {currentFeed?.isLiked ? (
+
+ ) : (
+
+ )}
+
+ {currentFeed?.isSaved ? (
+
+
+
+ ) : undefined}
+ {currentFeed?.isSaved ? (
+
+
+
+ ) : (
+
+
+
+ Add to My Feeds
+
+
+ )}
+
+ )
+ }, [
+ pal,
+ currentFeed?.isSaved,
+ currentFeed?.isLiked,
+ isPinned,
+ onToggleSaved,
+ onTogglePinned,
+ onToggleLiked,
+ dropdownItems,
+ ])
+
+ const renderListHeaderComponent = React.useCallback(() => {
+ return (
+ <>
+
+
+
+ {currentFeed?.displayName}
+
+ {currentFeed && (
+
+ by{' '}
+ {currentFeed.data.creator.did === store.me.did ? (
+ 'you'
+ ) : (
+
+ )}
+
+ )}
+ {isDesktopWeb && (
+
+
+
+
+
+
+ {currentFeed?.isLiked ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ {currentFeed?.data.description ? (
+
+ {currentFeed.data.description}
+
+ ) : null}
+
+ {currentFeed ? (
+
+ ) : null}
+
+
+
+
+
+ Feed
+
+
+
+ >
+ )
+ }, [
+ pal,
+ currentFeed,
+ store.me.did,
+ onToggleSaved,
+ onToggleLiked,
+ onPressShare,
+ name,
+ rkey,
+ isPinned,
+ onTogglePinned,
+ ])
+
+ const renderEmptyState = React.useCallback(() => {
+ return
+ }, [])
+
+ return (
+
+
+
+ {isScrolledDown ? (
+
+ ) : null}
+ }
+ accessibilityRole="button"
+ accessibilityLabel="Compose post"
+ accessibilityHint=""
+ />
+
+ )
+ }),
+)
+
+const styles = StyleSheet.create({
+ header: {
+ flexDirection: 'row',
+ gap: 12,
+ paddingHorizontal: 16,
+ paddingTop: 12,
+ paddingBottom: 16,
+ borderTopWidth: 1,
+ },
+ headerBtns: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ headerBtnsDesktop: {
+ marginTop: 8,
+ gap: 4,
+ },
+ headerAddBtn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ paddingLeft: 4,
+ },
+ headerDetails: {
+ paddingHorizontal: 16,
+ paddingBottom: 16,
+ },
+ headerDetailsFooter: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ fakeSelector: {
+ flexDirection: 'row',
+ paddingHorizontal: isDesktopWeb ? 16 : 6,
+ },
+ fakeSelectorItem: {
+ paddingHorizontal: 12,
+ paddingBottom: 8,
+ borderBottomWidth: 3,
+ },
+ liked: {
+ color: colors.red3,
+ },
+ top1: {
+ position: 'relative',
+ top: 1,
+ },
+ top2: {
+ position: 'relative',
+ top: 2,
+ },
+})
diff --git a/src/view/screens/CustomFeedLikedBy.tsx b/src/view/screens/CustomFeedLikedBy.tsx
new file mode 100644
index 0000000000..49d0d04829
--- /dev/null
+++ b/src/view/screens/CustomFeedLikedBy.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import {View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
+import {useStores} from 'state/index'
+import {makeRecordUri} from 'lib/strings/url-helpers'
+
+type Props = NativeStackScreenProps
+export const CustomFeedLikedByScreen = withAuthRequired(({route}: Props) => {
+ const store = useStores()
+ const {name, rkey} = route.params
+ const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey)
+
+ useFocusEffect(
+ React.useCallback(() => {
+ store.shell.setMinimalShellMode(false)
+ }, [store]),
+ )
+
+ return (
+
+
+
+
+ )
+})
diff --git a/src/view/screens/DiscoverFeeds.tsx b/src/view/screens/DiscoverFeeds.tsx
new file mode 100644
index 0000000000..98f164a61e
--- /dev/null
+++ b/src/view/screens/DiscoverFeeds.tsx
@@ -0,0 +1,112 @@
+import React from 'react'
+import {RefreshControl, StyleSheet, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {useStores} from 'state/index'
+import {FeedsDiscoveryModel} from 'state/models/discovery/feeds'
+import {CenteredView, FlatList} from 'view/com/util/Views'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
+import {Text} from 'view/com/util/text/Text'
+import {isDesktopWeb} from 'platform/detection'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+
+type Props = NativeStackScreenProps
+export const DiscoverFeedsScreen = withAuthRequired(
+ observer(({}: Props) => {
+ const store = useStores()
+ const pal = usePalette('default')
+ const feeds = React.useMemo(() => new FeedsDiscoveryModel(store), [store])
+
+ useFocusEffect(
+ React.useCallback(() => {
+ store.shell.setMinimalShellMode(false)
+ feeds.refresh()
+ }, [store, feeds]),
+ )
+
+ const onRefresh = React.useCallback(() => {
+ feeds.refresh()
+ }, [feeds])
+
+ const renderListEmptyComponent = React.useCallback(() => {
+ return (
+
+
+ {feeds.isLoading
+ ? 'Loading...'
+ : `We can't find any feeds for some reason. This is probably an error - try refreshing!`}
+
+
+ )
+ }, [pal, feeds.isLoading])
+
+ const renderItem = React.useCallback(
+ ({item}) => (
+
+ ),
+ [],
+ )
+
+ return (
+
+
+
+
+ item.data.uri}
+ contentContainerStyle={styles.contentContainer}
+ refreshControl={
+
+ }
+ renderItem={renderItem}
+ initialNumToRender={10}
+ ListEmptyComponent={renderListEmptyComponent}
+ extraData={feeds.isLoading}
+ />
+
+ )
+ }),
+)
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ contentContainer: {
+ paddingBottom: 100,
+ },
+ containerDesktop: {
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ },
+ empty: {
+ paddingHorizontal: 18,
+ paddingVertical: 16,
+ borderRadius: 8,
+ marginHorizontal: 18,
+ marginTop: 10,
+ },
+})
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
new file mode 100644
index 0000000000..7d44523841
--- /dev/null
+++ b/src/view/screens/Feeds.tsx
@@ -0,0 +1,141 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import isEqual from 'lodash.isequal'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {FlatList} from 'view/com/util/Views'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
+import {FAB} from 'view/com/util/fab/FAB'
+import {Link} from 'view/com/util/Link'
+import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types'
+import {observer} from 'mobx-react-lite'
+import {PostsMultiFeedModel} from 'state/models/feeds/multi-feed'
+import {MultiFeed} from 'view/com/posts/MultiFeed'
+import {isDesktopWeb} from 'platform/detection'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useTimer} from 'lib/hooks/useTimer'
+import {useStores} from 'state/index'
+import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
+import {ComposeIcon2, CogIcon} from 'lib/icons'
+import {s} from 'lib/styles'
+
+const LOAD_NEW_PROMPT_TIME = 60e3 // 60 seconds
+const HEADER_OFFSET = isDesktopWeb ? 0 : 40
+
+type Props = NativeStackScreenProps
+export const FeedsScreen = withAuthRequired(
+ observer(({}: Props) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const flatListRef = React.useRef(null)
+ const multifeed = React.useMemo(
+ () => new PostsMultiFeedModel(store),
+ [store],
+ )
+ const [onMainScroll, isScrolledDown, resetMainScroll] =
+ useOnMainScroll(store)
+ const [loadPromptVisible, setLoadPromptVisible] = React.useState(false)
+ const [resetPromptTimer] = useTimer(LOAD_NEW_PROMPT_TIME, () => {
+ setLoadPromptVisible(true)
+ })
+
+ const onSoftReset = React.useCallback(() => {
+ flatListRef.current?.scrollToOffset({offset: 0})
+ multifeed.loadLatest()
+ resetPromptTimer()
+ setLoadPromptVisible(false)
+ resetMainScroll()
+ }, [
+ flatListRef,
+ resetMainScroll,
+ multifeed,
+ resetPromptTimer,
+ setLoadPromptVisible,
+ ])
+
+ useFocusEffect(
+ React.useCallback(() => {
+ const softResetSub = store.onScreenSoftReset(onSoftReset)
+ const multifeedCleanup = multifeed.registerListeners()
+ const cleanup = () => {
+ softResetSub.remove()
+ multifeedCleanup()
+ }
+
+ store.shell.setMinimalShellMode(false)
+ return cleanup
+ }, [store, multifeed, onSoftReset]),
+ )
+
+ React.useEffect(() => {
+ if (
+ isEqual(
+ multifeed.feedInfos.map(f => f.uri),
+ store.me.savedFeeds.all.map(f => f.uri),
+ )
+ ) {
+ // no changes
+ return
+ }
+ multifeed.refresh()
+ }, [multifeed, store.me.savedFeeds.all])
+
+ const onPressCompose = React.useCallback(() => {
+ store.shell.openComposer({})
+ }, [store])
+
+ const renderHeaderBtn = React.useCallback(() => {
+ return (
+
+
+
+ )
+ }, [pal])
+
+ return (
+
+
+
+ {isScrolledDown || loadPromptVisible ? (
+
+ ) : null}
+ }
+ accessibilityRole="button"
+ accessibilityLabel="Compose post"
+ accessibilityHint=""
+ />
+
+ )
+ }),
+)
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+})
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 4fb605e3d9..95fb69400a 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,102 +1,144 @@
import React from 'react'
import {FlatList, View} from 'react-native'
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
+import {AppBskyFeedGetFeed as GetCustomFeed} from '@atproto/api'
import {observer} from 'mobx-react-lite'
import useAppState from 'react-native-appstate-hook'
+import isEqual from 'lodash.isequal'
import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
import {PostsFeedModel} from 'state/models/feeds/posts'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {useTabFocusEffect} from 'lib/hooks/useTabFocusEffect'
import {Feed} from '../com/posts/Feed'
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
+import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
import {FeedsTabBar} from '../com/pager/FeedsTabBar'
-import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager'
+import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {FAB} from '../com/util/fab/FAB'
import {useStores} from 'state/index'
import {s} from 'lib/styles'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {useAnalytics} from 'lib/analytics/analytics'
import {ComposeIcon2} from 'lib/icons'
-import {isDesktopWeb} from 'platform/detection'
+import {isDesktopWeb, isMobileWebMediaQuery, isWeb} from 'platform/detection'
-const HEADER_OFFSET = isDesktopWeb ? 50 : 40
+const HEADER_OFFSET_MOBILE = 78
+const HEADER_OFFSET_DESKTOP = 50
+const HEADER_OFFSET = isDesktopWeb
+ ? HEADER_OFFSET_DESKTOP
+ : HEADER_OFFSET_MOBILE
const POLL_FREQ = 30e3 // 30sec
type Props = NativeStackScreenProps
-export const HomeScreen = withAuthRequired((_opts: Props) => {
- const store = useStores()
- const [selectedPage, setSelectedPage] = React.useState(0)
+export const HomeScreen = withAuthRequired(
+ observer((_opts: Props) => {
+ const store = useStores()
+ const pagerRef = React.useRef(null)
+ const [selectedPage, setSelectedPage] = React.useState(0)
+ const [customFeeds, setCustomFeeds] = React.useState([])
- const algoFeed = React.useMemo(() => {
- const feed = new PostsFeedModel(store, 'goodstuff', {})
- feed.setup()
- return feed
- }, [store])
-
- useFocusEffect(
- React.useCallback(() => {
- store.shell.setMinimalShellMode(false)
- store.shell.setIsDrawerSwipeDisabled(selectedPage > 0)
- return () => {
- store.shell.setIsDrawerSwipeDisabled(false)
+ React.useEffect(() => {
+ const {pinned} = store.me.savedFeeds
+ if (
+ isEqual(
+ pinned.map(p => p.uri),
+ customFeeds.map(f => (f.params as GetCustomFeed.QueryParams).feed),
+ )
+ ) {
+ // no changes
+ return
}
- }, [store, selectedPage]),
- )
- const onPageSelected = React.useCallback(
- (index: number) => {
- store.shell.setMinimalShellMode(false)
- setSelectedPage(index)
- store.shell.setIsDrawerSwipeDisabled(index > 0)
- },
- [store],
- )
+ const feeds = []
+ for (const feed of pinned) {
+ const model = new PostsFeedModel(store, 'custom', {feed: feed.uri})
+ model.setup()
+ feeds.push(model)
+ }
+ pagerRef.current?.setPage(0)
+ setCustomFeeds(feeds)
+ }, [
+ store,
+ store.me.savedFeeds.pinned,
+ customFeeds,
+ setCustomFeeds,
+ pagerRef,
+ ])
- const onPressSelected = React.useCallback(() => {
- store.emitScreenSoftReset()
- }, [store])
+ useFocusEffect(
+ React.useCallback(() => {
+ store.shell.setMinimalShellMode(false)
+ store.shell.setIsDrawerSwipeDisabled(selectedPage > 0)
+ return () => {
+ store.shell.setIsDrawerSwipeDisabled(false)
+ }
+ }, [store, selectedPage]),
+ )
- const renderTabBar = React.useCallback(
- (props: RenderTabBarFnProps) => {
- return (
- {
+ store.shell.setMinimalShellMode(false)
+ setSelectedPage(index)
+ store.shell.setIsDrawerSwipeDisabled(index > 0)
+ },
+ [store, setSelectedPage],
+ )
+
+ const onPressSelected = React.useCallback(() => {
+ store.emitScreenSoftReset()
+ }, [store])
+
+ const renderTabBar = React.useCallback(
+ (props: RenderTabBarFnProps) => {
+ return (
+
+ )
+ },
+ [onPressSelected],
+ )
+
+ const renderFollowingEmptyState = React.useCallback(() => {
+ return
+ }, [])
+
+ const renderCustomFeedEmptyState = React.useCallback(() => {
+ return
+ }, [])
+
+ return (
+
+
- )
- },
- [onPressSelected],
- )
-
- const renderFollowingEmptyState = React.useCallback(() => {
- return
- }, [])
-
- const initialPage = store.me.followsCount === 0 ? 1 : 0
- return (
-
-
-
-
- )
-})
+ {customFeeds.map((f, index) => {
+ return (
+
+ )
+ })}
+
+ )
+ }),
+)
const FeedPage = observer(
({
@@ -111,8 +153,10 @@ const FeedPage = observer(
renderEmptyState?: () => JSX.Element
}) => {
const store = useStores()
- const onMainScroll = useOnMainScroll(store)
+ const [onMainScroll, isScrolledDown, resetMainScroll] =
+ useOnMainScroll(store)
const {screen, track} = useAnalytics()
+ const [headerOffset, setHeaderOffset] = React.useState(HEADER_OFFSET)
const scrollElRef = React.useRef(null)
const {appState} = useAppState({
onForeground: () => doPoll(true),
@@ -138,15 +182,30 @@ const FeedPage = observer(
)
const scrollToTop = React.useCallback(() => {
- scrollElRef.current?.scrollToOffset({offset: -HEADER_OFFSET})
- }, [scrollElRef])
+ scrollElRef.current?.scrollToOffset({offset: -headerOffset})
+ resetMainScroll()
+ }, [headerOffset, resetMainScroll])
const onSoftReset = React.useCallback(() => {
if (isPageFocused) {
scrollToTop()
+ feed.refresh()
}
- }, [isPageFocused, scrollToTop])
+ }, [isPageFocused, scrollToTop, feed])
+ // listens for resize events
+ const listenForResize = React.useCallback(() => {
+ // @ts-ignore we know window exists -prf
+ const isMobileWeb = global.window.matchMedia(
+ isMobileWebMediaQuery,
+ )?.matches
+ setHeaderOffset(
+ isMobileWeb ? HEADER_OFFSET_MOBILE : HEADER_OFFSET_DESKTOP,
+ )
+ }, [])
+
+ // fires when screen is activated/deactivated
+ // - set up polls/listeners, update content
useFocusEffect(
React.useCallback(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
@@ -166,6 +225,31 @@ const FeedPage = observer(
}
}, [store, doPoll, onSoftReset, screen, feed]),
)
+ // fires when tab is activated/deactivated
+ // - check for latest
+ useTabFocusEffect(
+ 'Home',
+ React.useCallback(
+ isInside => {
+ if (!isPageFocused || !isInside) {
+ return
+ }
+ feed.checkForLatest()
+ },
+ [isPageFocused, feed],
+ ),
+ )
+ // fires when page within screen is activated/deactivated
+ // - check for latest
+ React.useEffect(() => {
+ if (isPageFocused && isScreenFocused) {
+ feed.checkForLatest()
+ }
+ isWeb && window.addEventListener('resize', listenForResize)
+ return () => {
+ isWeb && window.removeEventListener('resize', listenForResize)
+ }
+ }, [isPageFocused, isScreenFocused, feed, listenForResize])
const onPressCompose = React.useCallback(() => {
track('HomeScreen:PressCompose')
@@ -181,6 +265,7 @@ const FeedPage = observer(
feed.refresh()
}, [feed, scrollToTop])
+ const hasNew = feed.hasNewLatest && !feed.isRefreshing
return (
- {feed.hasNewLatest && !feed.isRefreshing && (
-
+ {(isScrolledDown || hasNew) && (
+
)}
}
+ accessibilityRole="button"
+ accessibilityLabel="Compose post"
+ accessibilityHint=""
/>
)
diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx
index 8e0fe8dd3b..4a747e5bf7 100644
--- a/src/view/screens/Log.tsx
+++ b/src/view/screens/Log.tsx
@@ -46,7 +46,9 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
+ onPress={toggler(entry.id)}
+ accessibilityLabel="View debug entry"
+ accessibilityHint="Opens additional details for a debug entry">
{entry.type === 'debug' ? (
) : (
diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx
new file mode 100644
index 0000000000..4c52301cb6
--- /dev/null
+++ b/src/view/screens/Moderation.tsx
@@ -0,0 +1,137 @@
+import React from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {observer} from 'mobx-react-lite'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {useStores} from 'state/index'
+import {s} from 'lib/styles'
+import {CenteredView} from '../com/util/Views'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {Link} from '../com/util/Link'
+import {Text} from '../com/util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useAnalytics} from 'lib/analytics'
+import {isDesktopWeb} from 'platform/detection'
+
+type Props = NativeStackScreenProps
+export const ModerationScreen = withAuthRequired(
+ observer(function Moderation({}: Props) {
+ const pal = usePalette('default')
+ const store = useStores()
+ const {screen, track} = useAnalytics()
+
+ useFocusEffect(
+ React.useCallback(() => {
+ screen('Moderation')
+ store.shell.setMinimalShellMode(false)
+ }, [screen, store]),
+ )
+
+ const onPressContentFiltering = React.useCallback(() => {
+ track('Moderation:ContentfilteringButtonClicked')
+ store.shell.openModal({name: 'content-filtering-settings'})
+ }, [track, store])
+
+ return (
+
+
+
+
+
+
+
+
+ Content filtering
+
+
+
+
+
+
+
+ Mute lists
+
+
+
+
+
+
+
+ Muted accounts
+
+
+
+
+
+
+
+ Blocked accounts
+
+
+
+ )
+ }),
+)
+
+const styles = StyleSheet.create({
+ desktopContainer: {
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ },
+ spacer: {
+ height: 6,
+ },
+ linkCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 12,
+ paddingHorizontal: 18,
+ marginBottom: 1,
+ },
+ iconContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 40,
+ height: 40,
+ borderRadius: 30,
+ marginRight: 12,
+ },
+})
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
new file mode 100644
index 0000000000..cd506d6305
--- /dev/null
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -0,0 +1,175 @@
+import React, {useMemo} from 'react'
+import {
+ ActivityIndicator,
+ FlatList,
+ RefreshControl,
+ StyleSheet,
+ View,
+} from 'react-native'
+import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
+import {Text} from '../com/util/text/Text'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb} from 'platform/detection'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {observer} from 'mobx-react-lite'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {CommonNavigatorParams} from 'lib/routes/types'
+import {BlockedAccountsModel} from 'state/models/lists/blocked-accounts'
+import {useAnalytics} from 'lib/analytics'
+import {useFocusEffect} from '@react-navigation/native'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {CenteredView} from 'view/com/util/Views'
+import {ProfileCard} from 'view/com/profile/ProfileCard'
+
+type Props = NativeStackScreenProps<
+ CommonNavigatorParams,
+ 'ModerationBlockedAccounts'
+>
+export const ModerationBlockedAccounts = withAuthRequired(
+ observer(({}: Props) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const {screen} = useAnalytics()
+ const blockedAccounts = useMemo(
+ () => new BlockedAccountsModel(store),
+ [store],
+ )
+
+ useFocusEffect(
+ React.useCallback(() => {
+ screen('BlockedAccounts')
+ store.shell.setMinimalShellMode(false)
+ blockedAccounts.refresh()
+ }, [screen, store, blockedAccounts]),
+ )
+
+ const onRefresh = React.useCallback(() => {
+ blockedAccounts.refresh()
+ }, [blockedAccounts])
+ const onEndReached = React.useCallback(() => {
+ blockedAccounts
+ .loadMore()
+ .catch(err =>
+ store.log.error('Failed to load more blocked accounts', err),
+ )
+ }, [blockedAccounts, store])
+
+ const renderItem = ({
+ item,
+ index,
+ }: {
+ item: ActorDefs.ProfileView
+ index: number
+ }) => (
+
+ )
+ return (
+
+
+
+ Blocked accounts cannot reply in your threads, mention you, or
+ otherwise interact with you. You will not see their content and they
+ will be prevented from seeing yours.
+
+ {!blockedAccounts.hasContent ? (
+
+
+
+ You have not blocked any accounts yet. To block an account, go
+ to their profile and selected "Block account" from the menu on
+ their account.
+
+
+
+ ) : (
+ item.did}
+ refreshControl={
+
+ }
+ onEndReached={onEndReached}
+ renderItem={renderItem}
+ initialNumToRender={15}
+ ListFooterComponent={() => (
+
+ {blockedAccounts.isLoading && }
+
+ )}
+ extraData={blockedAccounts.isLoading}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
+ />
+ )}
+
+ )
+ }),
+)
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingBottom: isDesktopWeb ? 0 : 100,
+ },
+ containerDesktop: {
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ },
+ title: {
+ textAlign: 'center',
+ marginTop: 12,
+ marginBottom: 12,
+ },
+ description: {
+ textAlign: 'center',
+ paddingHorizontal: 30,
+ marginBottom: 14,
+ },
+ descriptionDesktop: {
+ marginTop: 14,
+ },
+
+ flex1: {
+ flex: 1,
+ },
+ empty: {
+ paddingHorizontal: 20,
+ paddingVertical: 20,
+ borderRadius: 16,
+ marginHorizontal: 24,
+ marginTop: 10,
+ },
+ emptyText: {
+ textAlign: 'center',
+ },
+
+ footer: {
+ height: 200,
+ paddingTop: 20,
+ },
+})
diff --git a/src/view/screens/ModerationMuteLists.tsx b/src/view/screens/ModerationMuteLists.tsx
new file mode 100644
index 0000000000..0b81f432f6
--- /dev/null
+++ b/src/view/screens/ModerationMuteLists.tsx
@@ -0,0 +1,122 @@
+import React from 'react'
+import {StyleSheet} from 'react-native'
+import {useFocusEffect, useNavigation} from '@react-navigation/native'
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {AtUri} from '@atproto/api'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {EmptyStateWithButton} from 'view/com/util/EmptyStateWithButton'
+import {useStores} from 'state/index'
+import {ListsListModel} from 'state/models/lists/lists-list'
+import {ListsList} from 'view/com/lists/ListsList'
+import {Button} from 'view/com/util/forms/Button'
+import {NavigationProp} from 'lib/routes/types'
+import {usePalette} from 'lib/hooks/usePalette'
+import {CenteredView} from 'view/com/util/Views'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {isDesktopWeb} from 'platform/detection'
+
+type Props = NativeStackScreenProps<
+ CommonNavigatorParams,
+ 'ModerationMuteLists'
+>
+export const ModerationMuteListsScreen = withAuthRequired(({}: Props) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const navigation = useNavigation()
+
+ const mutelists: ListsListModel = React.useMemo(
+ () => new ListsListModel(store, 'my-modlists'),
+ [store],
+ )
+
+ useFocusEffect(
+ React.useCallback(() => {
+ store.shell.setMinimalShellMode(false)
+ mutelists.refresh()
+ }, [store, mutelists]),
+ )
+
+ const onPressNewMuteList = React.useCallback(() => {
+ store.shell.openModal({
+ name: 'create-or-edit-mute-list',
+ onSave: (uri: string) => {
+ try {
+ const urip = new AtUri(uri)
+ navigation.navigate('ProfileList', {
+ name: urip.hostname,
+ rkey: urip.rkey,
+ })
+ } catch {}
+ },
+ })
+ }, [store, navigation])
+
+ const renderEmptyState = React.useCallback(() => {
+ return (
+
+ )
+ }, [onPressNewMuteList])
+
+ const renderHeaderButton = React.useCallback(
+ () => (
+
+
+
+ ),
+ [onPressNewMuteList, pal],
+ )
+
+ return (
+
+
+
+
+ )
+})
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingBottom: isDesktopWeb ? 0 : 100,
+ },
+ containerDesktop: {
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ },
+ createBtn: {
+ width: 40,
+ },
+})
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
new file mode 100644
index 0000000000..22b8c0d33c
--- /dev/null
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -0,0 +1,171 @@
+import React, {useMemo} from 'react'
+import {
+ ActivityIndicator,
+ FlatList,
+ RefreshControl,
+ StyleSheet,
+ View,
+} from 'react-native'
+import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
+import {Text} from '../com/util/text/Text'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isDesktopWeb} from 'platform/detection'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {observer} from 'mobx-react-lite'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {CommonNavigatorParams} from 'lib/routes/types'
+import {MutedAccountsModel} from 'state/models/lists/muted-accounts'
+import {useAnalytics} from 'lib/analytics'
+import {useFocusEffect} from '@react-navigation/native'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {CenteredView} from 'view/com/util/Views'
+import {ProfileCard} from 'view/com/profile/ProfileCard'
+
+type Props = NativeStackScreenProps<
+ CommonNavigatorParams,
+ 'ModerationMutedAccounts'
+>
+export const ModerationMutedAccounts = withAuthRequired(
+ observer(({}: Props) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const {screen} = useAnalytics()
+ const mutedAccounts = useMemo(() => new MutedAccountsModel(store), [store])
+
+ useFocusEffect(
+ React.useCallback(() => {
+ screen('MutedAccounts')
+ store.shell.setMinimalShellMode(false)
+ mutedAccounts.refresh()
+ }, [screen, store, mutedAccounts]),
+ )
+
+ const onRefresh = React.useCallback(() => {
+ mutedAccounts.refresh()
+ }, [mutedAccounts])
+ const onEndReached = React.useCallback(() => {
+ mutedAccounts
+ .loadMore()
+ .catch(err =>
+ store.log.error('Failed to load more muted accounts', err),
+ )
+ }, [mutedAccounts, store])
+
+ const renderItem = ({
+ item,
+ index,
+ }: {
+ item: ActorDefs.ProfileView
+ index: number
+ }) => (
+
+ )
+ return (
+
+
+
+ Muted accounts have their posts removed from your feed and from your
+ notifications. Mutes are completely private.
+
+ {!mutedAccounts.hasContent ? (
+
+
+
+ You have not muted any accounts yet. To mute an account, go to
+ their profile and selected "Mute account" from the menu on their
+ account.
+
+
+
+ ) : (
+ item.did}
+ refreshControl={
+
+ }
+ onEndReached={onEndReached}
+ renderItem={renderItem}
+ initialNumToRender={15}
+ ListFooterComponent={() => (
+
+ {mutedAccounts.isLoading && }
+
+ )}
+ extraData={mutedAccounts.isLoading}
+ // @ts-ignore our .web version only -prf
+ desktopFixedHeight
+ />
+ )}
+
+ )
+ }),
+)
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingBottom: isDesktopWeb ? 0 : 100,
+ },
+ containerDesktop: {
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ },
+ title: {
+ textAlign: 'center',
+ marginTop: 12,
+ marginBottom: 12,
+ },
+ description: {
+ textAlign: 'center',
+ paddingHorizontal: 30,
+ marginBottom: 14,
+ },
+ descriptionDesktop: {
+ marginTop: 14,
+ },
+
+ flex1: {
+ flex: 1,
+ },
+ empty: {
+ paddingHorizontal: 20,
+ paddingVertical: 20,
+ borderRadius: 16,
+ marginHorizontal: 24,
+ marginTop: 10,
+ },
+ emptyText: {
+ textAlign: 'center',
+ },
+
+ footer: {
+ height: 200,
+ paddingTop: 20,
+ },
+})
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index ec42cf0a1d..15bbf4fd07 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -13,8 +13,10 @@ import {InvitedUsers} from '../com/notifications/InvitedUsers'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {useStores} from 'state/index'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
+import {useTabFocusEffect} from 'lib/hooks/useTabFocusEffect'
import {s} from 'lib/styles'
import {useAnalytics} from 'lib/analytics/analytics'
+import {isWeb} from 'platform/detection'
type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams,
@@ -23,7 +25,8 @@ type Props = NativeStackScreenProps<
export const NotificationsScreen = withAuthRequired(
observer(({}: Props) => {
const store = useStores()
- const onMainScroll = useOnMainScroll(store)
+ const [onMainScroll, isScrolledDown, resetMainScroll] =
+ useOnMainScroll(store)
const scrollElRef = React.useRef(null)
const {screen} = useAnalytics()
@@ -35,7 +38,8 @@ export const NotificationsScreen = withAuthRequired(
const scrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: 0})
- }, [scrollElRef])
+ resetMainScroll()
+ }, [scrollElRef, resetMainScroll])
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
@@ -58,7 +62,35 @@ export const NotificationsScreen = withAuthRequired(
}
}, [store, screen, onPressLoadLatest]),
)
+ useTabFocusEffect(
+ 'Notifications',
+ React.useCallback(
+ isInside => {
+ // on mobile:
+ // fires with `isInside=true` when the user navigates to the root tab
+ // but not when the user goes back to the screen by pressing back
+ // on web:
+ // essentially equivalent to useFocusEffect because we dont used tabbed
+ // navigation
+ if (isInside) {
+ if (isWeb) {
+ store.me.notifications.syncQueue()
+ } else {
+ if (store.me.notifications.unreadCount > 0) {
+ store.me.notifications.refresh()
+ } else {
+ store.me.notifications.syncQueue()
+ }
+ }
+ }
+ },
+ [store],
+ ),
+ )
+ const hasNew =
+ store.me.notifications.hasNewLatest &&
+ !store.me.notifications.isRefreshing
return (
@@ -69,10 +101,14 @@ export const NotificationsScreen = withAuthRequired(
onScroll={onMainScroll}
scrollElRef={scrollElRef}
/>
- {store.me.notifications.hasNewLatest &&
- !store.me.notifications.isRefreshing && (
-
- )}
+ {(isScrolledDown || hasNew) && (
+
+ )}
)
}),
diff --git a/src/view/screens/PrivacyPolicy.tsx b/src/view/screens/PrivacyPolicy.tsx
index ec39ac2d83..0112a550de 100644
--- a/src/view/screens/PrivacyPolicy.tsx
+++ b/src/view/screens/PrivacyPolicy.tsx
@@ -1,14 +1,14 @@
import React from 'react'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
+import {Text} from 'view/com/util/text/Text'
+import {TextLink} from 'view/com/util/Link'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {ViewHeader} from '../com/util/ViewHeader'
import {useStores} from 'state/index'
import {ScrollView} from 'view/com/util/Views'
-import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
-import PrivacyPolicyHtml from '../../locale/en/privacy-policy'
type Props = NativeStackScreenProps
export const PrivacyPolicyScreen = (_props: Props) => {
@@ -26,10 +26,14 @@ export const PrivacyPolicyScreen = (_props: Props) => {
-
- Privacy Policy
+
+ The Privacy Policy has been moved to{' '}
+
-
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index dbe916e4db..525dec767c 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -4,14 +4,19 @@ import {observer} from 'mobx-react-lite'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {ViewSelector} from '../com/util/ViewSelector'
+import {ViewSelector, ViewSelectorHandle} from '../com/util/ViewSelector'
import {CenteredView} from '../com/util/Views'
-import {ProfileUiModel} from 'state/models/ui/profile'
+import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
+import {ProfileUiModel, Sections} from 'state/models/ui/profile'
import {useStores} from 'state/index'
-import {PostsFeedSliceModel} from 'state/models/feeds/posts'
+import {PostsFeedSliceModel} from 'state/models/feeds/post'
import {ProfileHeader} from '../com/profile/ProfileHeader'
import {FeedSlice} from '../com/posts/FeedSlice'
-import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder'
+import {ListCard} from 'view/com/lists/ListCard'
+import {
+ PostFeedLoadingPlaceholder,
+ ProfileCardFeedLoadingPlaceholder,
+} from '../com/util/LoadingPlaceholder'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {EmptyState} from '../com/util/EmptyState'
@@ -21,26 +26,40 @@ import {s, colors} from 'lib/styles'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {useAnalytics} from 'lib/analytics/analytics'
import {ComposeIcon2} from 'lib/icons'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import {useSetTitle} from 'lib/hooks/useSetTitle'
+import {combinedDisplayName} from 'lib/strings/display-names'
type Props = NativeStackScreenProps
export const ProfileScreen = withAuthRequired(
observer(({route}: Props) => {
const store = useStores()
const {screen, track} = useAnalytics()
+ const viewSelectorRef = React.useRef(null)
useEffect(() => {
screen('Profile')
}, [screen])
- const onMainScroll = useOnMainScroll(store)
const [hasSetup, setHasSetup] = useState(false)
const uiState = React.useMemo(
() => new ProfileUiModel(store, {user: route.params.name}),
[route.params.name, store],
)
+ useSetTitle(combinedDisplayName(uiState.profile))
+
+ const onSoftReset = React.useCallback(() => {
+ viewSelectorRef.current?.scrollToTop()
+ }, [])
+
+ useEffect(() => {
+ setHasSetup(false)
+ }, [route.params.name])
useFocusEffect(
React.useCallback(() => {
+ const softResetSub = store.onScreenSoftReset(onSoftReset)
let aborted = false
store.shell.setMinimalShellMode(false)
const feedCleanup = uiState.feed.registerListeners()
@@ -57,8 +76,9 @@ export const ProfileScreen = withAuthRequired(
return () => {
aborted = true
feedCleanup()
+ softResetSub.remove()
}
- }, [hasSetup, uiState, store]),
+ }, [store, onSoftReset, uiState, hasSetup]),
)
// events
@@ -68,9 +88,12 @@ export const ProfileScreen = withAuthRequired(
track('ProfileScreen:PressCompose')
store.shell.openComposer({})
}, [store, track])
- const onSelectView = (index: number) => {
- uiState.setSelectedViewIndex(index)
- }
+ const onSelectView = React.useCallback(
+ (index: number) => {
+ uiState.setSelectedViewIndex(index)
+ },
+ [uiState],
+ )
const onRefresh = React.useCallback(() => {
uiState
.refresh()
@@ -109,47 +132,130 @@ export const ProfileScreen = withAuthRequired(
}, [uiState.showLoadingMoreFooter])
const renderItem = React.useCallback(
(item: any) => {
- if (item === ProfileUiModel.END_ITEM) {
- return - end of feed -
- } else if (item === ProfileUiModel.LOADING_ITEM) {
- return
- } else if (item._reactKey === '__error__') {
- return (
-
-
+ } else if (item._reactKey === '__error__') {
+ return (
+
+
+
+ )
+ } else if (item === ProfileUiModel.EMPTY_ITEM) {
+ return (
+
-
- )
- } else if (item === ProfileUiModel.EMPTY_ITEM) {
- return (
-
- )
- } else if (item instanceof PostsFeedSliceModel) {
- return
+ )
+ } else {
+ return
+ }
+ // if section is custom algorithms
+ } else if (uiState.selectedView === Sections.CustomAlgorithms) {
+ if (item === ProfileUiModel.LOADING_ITEM) {
+ return
+ } else if (item._reactKey === '__error__') {
+ return (
+
+
+
+ )
+ } else if (item === ProfileUiModel.EMPTY_ITEM) {
+ return (
+
+ )
+ } else if (item instanceof CustomFeedModel) {
+ return
+ }
+ // if section is posts or posts & replies
+ } else {
+ if (item === ProfileUiModel.END_ITEM) {
+ return - end of feed -
+ } else if (item === ProfileUiModel.LOADING_ITEM) {
+ return
+ } else if (item._reactKey === '__error__') {
+ if (uiState.feed.isBlocking) {
+ return (
+
+ )
+ }
+ if (uiState.feed.isBlockedBy) {
+ return (
+
+ )
+ }
+ return (
+
+
+
+ )
+ } else if (item === ProfileUiModel.EMPTY_ITEM) {
+ return (
+
+ )
+ } else if (item instanceof PostsFeedSliceModel) {
+ return (
+
+ )
+ }
}
return
},
- [onPressTryAgain, uiState.profile.did],
+ [
+ onPressTryAgain,
+ uiState.selectedView,
+ uiState.profile.did,
+ uiState.feed.isBlocking,
+ uiState.feed.isBlockedBy,
+ ],
)
return (
-
+
{uiState.profile.hasError ? (
) : uiState.profile.hasLoaded ? (
@@ -170,7 +275,7 @@ export const ProfileScreen = withAuthRequired(
onPress={onPressCompose}
icon={ }
/>
-
+
)
}),
)
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
new file mode 100644
index 0000000000..7c3ed831c3
--- /dev/null
+++ b/src/view/screens/ProfileList.tsx
@@ -0,0 +1,177 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {useNavigation} from '@react-navigation/native'
+import {observer} from 'mobx-react-lite'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {CenteredView} from 'view/com/util/Views'
+import {ListItems} from 'view/com/lists/ListItems'
+import {EmptyState} from 'view/com/util/EmptyState'
+import {Button} from 'view/com/util/forms/Button'
+import * as Toast from 'view/com/util/Toast'
+import {ListModel} from 'state/models/content/list'
+import {useStores} from 'state/index'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useSetTitle} from 'lib/hooks/useSetTitle'
+import {NavigationProp} from 'lib/routes/types'
+import {isDesktopWeb} from 'platform/detection'
+
+type Props = NativeStackScreenProps
+export const ProfileListScreen = withAuthRequired(
+ observer(({route}: Props) => {
+ const store = useStores()
+ const navigation = useNavigation()
+ const pal = usePalette('default')
+ const {name, rkey} = route.params
+
+ const list: ListModel = React.useMemo(() => {
+ const model = new ListModel(
+ store,
+ `at://${name}/app.bsky.graph.list/${rkey}`,
+ )
+ return model
+ }, [store, name, rkey])
+ useSetTitle(list.list?.name)
+
+ useFocusEffect(
+ React.useCallback(() => {
+ store.shell.setMinimalShellMode(false)
+ list.loadMore(true)
+ }, [store, list]),
+ )
+
+ const onToggleSubscribed = React.useCallback(async () => {
+ try {
+ if (list.list?.viewer?.muted) {
+ await list.unsubscribe()
+ } else {
+ await list.subscribe()
+ }
+ } catch (err) {
+ Toast.show(
+ 'There was an an issue updating your subscription, please check your internet connection and try again.',
+ )
+ store.log.error('Failed up update subscription', {err})
+ }
+ }, [store, list])
+
+ const onPressEditList = React.useCallback(() => {
+ store.shell.openModal({
+ name: 'create-or-edit-mute-list',
+ list,
+ onSave() {
+ list.refresh()
+ },
+ })
+ }, [store, list])
+
+ const onPressDeleteList = React.useCallback(() => {
+ store.shell.openModal({
+ name: 'confirm',
+ title: 'Delete List',
+ message: 'Are you sure?',
+ async onPressConfirm() {
+ await list.delete()
+ if (navigation.canGoBack()) {
+ navigation.goBack()
+ } else {
+ navigation.navigate('Home')
+ }
+ },
+ })
+ }, [store, list, navigation])
+
+ const renderEmptyState = React.useCallback(() => {
+ return
+ }, [])
+
+ const renderHeaderBtns = React.useCallback(() => {
+ return (
+
+ {list?.isOwner && (
+
+ )}
+ {list?.isOwner && (
+
+ )}
+ {list.list?.viewer?.muted ? (
+
+ ) : (
+
+ )}
+
+ )
+ }, [
+ list?.isOwner,
+ list.list?.viewer?.muted,
+ onPressDeleteList,
+ onPressEditList,
+ onToggleSubscribed,
+ ])
+
+ return (
+
+
+
+
+ )
+ }),
+)
+
+const styles = StyleSheet.create({
+ headerBtns: {
+ flexDirection: 'row',
+ gap: 8,
+ },
+ container: {
+ flex: 1,
+ paddingBottom: isDesktopWeb ? 0 : 100,
+ },
+ containerDesktop: {
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ },
+})
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
new file mode 100644
index 0000000000..103b18c70e
--- /dev/null
+++ b/src/view/screens/SavedFeeds.tsx
@@ -0,0 +1,293 @@
+import React, {useCallback, useMemo} from 'react'
+import {
+ RefreshControl,
+ StyleSheet,
+ View,
+ ActivityIndicator,
+ Pressable,
+ TouchableOpacity,
+} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {useAnalytics} from 'lib/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {CommonNavigatorParams} from 'lib/routes/types'
+import {observer} from 'mobx-react-lite'
+import {useStores} from 'state/index'
+import {withAuthRequired} from 'view/com/auth/withAuthRequired'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {CenteredView} from 'view/com/util/Views'
+import {Text} from 'view/com/util/text/Text'
+import {isDesktopWeb, isWeb} from 'platform/detection'
+import {s, colors} from 'lib/styles'
+import DraggableFlatList, {
+ ShadowDecorator,
+ ScaleDecorator,
+} from 'react-native-draggable-flatlist'
+import {CustomFeed} from 'view/com/feeds/CustomFeed'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {CustomFeedModel} from 'state/models/feeds/custom-feed'
+import * as Toast from 'view/com/util/Toast'
+import {Haptics} from 'lib/haptics'
+import {Link, TextLink} from 'view/com/util/Link'
+
+type Props = NativeStackScreenProps
+
+export const SavedFeeds = withAuthRequired(
+ observer(({}: Props) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const {screen} = useAnalytics()
+
+ const savedFeeds = useMemo(() => store.me.savedFeeds, [store])
+ useFocusEffect(
+ useCallback(() => {
+ screen('SavedFeeds')
+ store.shell.setMinimalShellMode(false)
+ savedFeeds.refresh()
+ }, [screen, store, savedFeeds]),
+ )
+
+ const renderListEmptyComponent = useCallback(() => {
+ return (
+
+
+ You don't have any saved feeds.
+
+
+ )
+ }, [pal])
+
+ const renderListFooterComponent = useCallback(() => {
+ return (
+ <>
+
+
+
+
+ Discover new feeds
+
+
+
+
+
+ Feeds are custom algorithms that users build with a little coding
+ expertise.{' '}
+ {' '}
+ for more information.
+
+
+ {savedFeeds.isLoading && }
+ >
+ )
+ }, [pal, savedFeeds.isLoading])
+
+ const onRefresh = useCallback(() => savedFeeds.refresh(), [savedFeeds])
+
+ const onDragEnd = useCallback(
+ async ({data}) => {
+ try {
+ await savedFeeds.reorderPinnedFeeds(data)
+ } catch (e) {
+ Toast.show('There was an issue contacting the server')
+ store.log.error('Failed to save pinned feed order', {e})
+ }
+ },
+ [savedFeeds, store],
+ )
+
+ return (
+
+
+ item.data.uri}
+ refreshing={savedFeeds.isRefreshing}
+ refreshControl={
+
+ }
+ renderItem={({item, drag}) => }
+ getItemLayout={(data, index) => ({
+ length: 77,
+ offset: 77 * index,
+ index,
+ })}
+ initialNumToRender={10}
+ ListFooterComponent={renderListFooterComponent}
+ ListEmptyComponent={renderListEmptyComponent}
+ extraData={savedFeeds.isLoading}
+ onDragEnd={onDragEnd}
+ />
+
+ )
+ }),
+)
+
+const ListItem = observer(
+ ({item, drag}: {item: CustomFeedModel; drag: () => void}) => {
+ const pal = usePalette('default')
+ const store = useStores()
+ const savedFeeds = useMemo(() => store.me.savedFeeds, [store])
+ const isPinned = savedFeeds.isPinned(item)
+
+ const onTogglePinned = useCallback(() => {
+ Haptics.default()
+ savedFeeds.togglePinnedFeed(item).catch(e => {
+ Toast.show('There was an issue contacting the server')
+ store.log.error('Failed to toggle pinned feed', {e})
+ })
+ }, [savedFeeds, item, store])
+ const onPressUp = useCallback(
+ () =>
+ savedFeeds.movePinnedFeed(item, 'up').catch(e => {
+ Toast.show('There was an issue contacting the server')
+ store.log.error('Failed to set pinned feed order', {e})
+ }),
+ [store, savedFeeds, item],
+ )
+ const onPressDown = useCallback(
+ () =>
+ savedFeeds.movePinnedFeed(item, 'down').catch(e => {
+ Toast.show('There was an issue contacting the server')
+ store.log.error('Failed to set pinned feed order', {e})
+ }),
+ [store, savedFeeds, item],
+ )
+
+ return (
+
+
+
+ {isPinned && isWeb ? (
+
+
+
+
+
+
+
+
+ ) : isPinned ? (
+
+ ) : null}
+
+
+
+
+
+
+
+ )
+ },
+)
+
+const styles = StyleSheet.create({
+ desktopContainer: {
+ borderLeftWidth: 1,
+ borderRightWidth: 1,
+ minHeight: '100vh',
+ },
+ empty: {
+ paddingHorizontal: 20,
+ paddingVertical: 20,
+ borderRadius: 16,
+ marginHorizontal: 24,
+ marginTop: 10,
+ },
+ itemContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderBottomWidth: 1,
+ paddingRight: 16,
+ },
+ webArrowButtonsContainer: {
+ paddingLeft: 16,
+ flexDirection: 'column',
+ justifyContent: 'space-around',
+ },
+ webArrowUpButton: {
+ marginBottom: 10,
+ },
+ noBorder: {
+ borderTopWidth: 0,
+ },
+ footerText: {
+ paddingHorizontal: 26,
+ paddingTop: 22,
+ paddingBottom: 100,
+ },
+ footerLinks: {
+ borderBottomWidth: 1,
+ borderTopWidth: 0,
+ },
+ footerLink: {
+ flexDirection: 'row',
+ paddingHorizontal: 26,
+ paddingVertical: 18,
+ gap: 18,
+ },
+})
diff --git a/src/view/screens/Search.web.tsx b/src/view/screens/Search.web.tsx
index 62f3fb900d..85e8c212e2 100644
--- a/src/view/screens/Search.web.tsx
+++ b/src/view/screens/Search.web.tsx
@@ -44,12 +44,12 @@ export const SearchScreen = withAuthRequired(
}
}, [foafs, suggestedActors, searchUIModel, params.q])
+ const {isDesktop} = useWebMediaQueries()
+
if (searchUIModel) {
return
}
- const {isDesktop} = useWebMediaQueries()
-
if (!isDesktop) {
return
}
diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx
index de64b2d67f..6a2fca5bc9 100644
--- a/src/view/screens/SearchMobile.tsx
+++ b/src/view/screens/SearchMobile.tsx
@@ -35,7 +35,7 @@ export const SearchScreen = withAuthRequired(
const store = useStores()
const scrollViewRef = React.useRef(null)
const flatListRef = React.useRef(null)
- const onMainScroll = useOnMainScroll(store)
+ const [onMainScroll] = useOnMainScroll(store)
const [isInputFocused, setIsInputFocused] = React.useState(false)
const [query, setQuery] = React.useState('')
const autocompleteView = React.useMemo(
@@ -118,7 +118,7 @@ export const SearchScreen = withAuthRequired(
}, [])
return (
-
+
{query && autocompleteView.searchRes.length ? (
<>
- {autocompleteView.searchRes.map(
- ({did, handle, displayName, labels, avatar}, index) => (
-
- ),
- )}
+ {autocompleteView.searchRes.map((profile, index) => (
+
+ ))}
>
) : query && !autocompleteView.searchRes.length ? (
@@ -169,7 +164,7 @@ export const SearchScreen = withAuthRequired(
) : isInputFocused ? (
- Search for users on the network
+ Search for users and posts on the network
) : null}
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index b39bb6c09d..3d057451ab 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -1,6 +1,8 @@
import React from 'react'
import {
ActivityIndicator,
+ Platform,
+ Pressable,
StyleSheet,
TextStyle,
TouchableOpacity,
@@ -29,12 +31,22 @@ import {Text} from '../com/util/text/Text'
import * as Toast from '../com/util/Toast'
import {UserAvatar} from '../com/util/UserAvatar'
import {DropdownButton} from 'view/com/util/forms/DropdownButton'
+import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {usePalette} from 'lib/hooks/usePalette'
import {useCustomPalette} from 'lib/hooks/useCustomPalette'
import {AccountData} from 'state/models/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {NavigationProp} from 'lib/routes/types'
+import {isDesktopWeb} from 'platform/detection'
import {pluralize} from 'lib/strings/helpers'
+import {formatCount} from 'view/com/util/numeric/format'
+import {isColorMode} from 'state/models/ui/shell'
+import Clipboard from '@react-native-clipboard/clipboard'
+
+// TEMPORARY (APP-700)
+// remove after backend testing finishes
+// -prf
+import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header'
type Props = NativeStackScreenProps
export const SettingsScreen = withAuthRequired(
@@ -44,6 +56,9 @@ export const SettingsScreen = withAuthRequired(
const navigation = useNavigation()
const {screen, track} = useAnalytics()
const [isSwitching, setIsSwitching] = React.useState(false)
+ const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
+ store.agent,
+ )
const primaryBg = useCustomPalette({
light: {backgroundColor: colors.blue0},
@@ -53,6 +68,7 @@ export const SettingsScreen = withAuthRequired(
light: {color: colors.blue3},
dark: {color: colors.blue2},
})
+
const dangerBg = useCustomPalette({
light: {backgroundColor: colors.red1},
dark: {backgroundColor: colors.red7},
@@ -124,9 +140,9 @@ export const SettingsScreen = withAuthRequired(
store.shell.openModal({name: 'invite-codes'})
}, [track, store])
- const onPressContentFiltering = React.useCallback(() => {
- track('Settings:ContentfilteringButtonClicked')
- store.shell.openModal({name: 'content-filtering-settings'})
+ const onPressContentLanguages = React.useCallback(() => {
+ track('Settings:ContentlanguagesButtonClicked')
+ store.shell.openModal({name: 'content-languages-settings'})
}, [track, store])
const onPressSignout = React.useCallback(() => {
@@ -138,11 +154,42 @@ export const SettingsScreen = withAuthRequired(
store.shell.openModal({name: 'delete-account'})
}, [store])
+ const onPressResetPreferences = React.useCallback(async () => {
+ await store.preferences.reset()
+ Toast.show('Preferences reset')
+ }, [store])
+
+ const onPressBuildInfo = React.useCallback(() => {
+ Clipboard.setString(
+ `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
+ )
+ Toast.show('Copied build version to clipboard')
+ }, [])
+
return (
-
+
+ {store.session.currentSession !== undefined ? (
+ <>
+
+ Account
+
+
+
+ Email:{' '}
+
+ {store.session.currentSession?.email}
+
+
+
+
+ >
+ ) : null}
Signed in as
@@ -172,7 +219,10 @@ export const SettingsScreen = withAuthRequired(
+ onPress={isSwitching ? undefined : onPressSignout}
+ accessibilityRole="button"
+ accessibilityLabel="Sign out"
+ accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
Sign out
@@ -187,7 +237,10 @@ export const SettingsScreen = withAuthRequired(
style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
onPress={
isSwitching ? undefined : () => onPressSwitchAccount(account)
- }>
+ }
+ accessibilityRole="button"
+ accessibilityLabel={`Switch to ${account.handle}`}
+ accessibilityHint="Switches the account you are logged in to">
@@ -205,7 +258,10 @@ export const SettingsScreen = withAuthRequired(
+ onPress={isSwitching ? undefined : onPressAddAccount}
+ accessibilityRole="button"
+ accessibilityLabel="Add account"
+ accessibilityHint="Create a new Bluesky account">
- Invite a friend
+ Invite a Friend
+ onPress={isSwitching ? undefined : onPressInviteCodes}
+ accessibilityRole="button"
+ accessibilityLabel="Invite"
+ accessibilityHint="Opens invite code list">
0 ? pal.link : pal.text}>
- {store.me.invitesAvailable} invite{' '}
+ {formatCount(store.me.invitesAvailable)} invite{' '}
{pluralize(store.me.invitesAvailable, 'code')} available
+
+
+ Appearance
+
+
+
+
+ store.shell.setColorMode(isColorMode(v) ? v : 'system')
+ }
+ />
+
+ store.shell.setColorMode(isColorMode(v) ? v : 'system')
+ }
+ />
+
+ store.shell.setColorMode(isColorMode(v) ? v : 'system')
+ }
+ />
+
+
Advanced
-
+ href="/settings/app-passwords">
- Content moderation
+ App passwords
+
+
+
+
+
+
+
+ Saved Feeds
+
+
+
+
+
+
+
+ Content languages
+ onPress={isSwitching ? undefined : onPressChangeHandle}
+ accessibilityRole="button"
+ accessibilityLabel="Change handle"
+ accessibilityHint="Choose a new Bluesky username or create">
-
- Change my handle
+
+ Change handle
-
-
- Danger zone
+ Danger Zone
+ onPress={onPressDeleteAccount}
+ accessible={true}
+ accessibilityRole="button"
+ accessibilityLabel="Delete account"
+ accessibilityHint="Opens modal for account deletion confirmation. Requires email code.">
- Delete my account
+ Delete my account…
-
-
- Developer tools
+ Developer Tools
-
-
- Storybook
+ {isDesktopWeb ? (
+
+ ) : null}
+ {__DEV__ ? (
+ <>
+
+
+ Storybook
+
+
+
+
+ Reset preferences state
+
+
+ >
+ ) : null}
+
+
+ Build version {AppInfo.appVersion} {AppInfo.updateChannel}
-
-
- Build version {AppInfo.appVersion} ({AppInfo.buildVersion})
-
+
@@ -335,6 +488,7 @@ export const SettingsScreen = withAuthRequired(
function AccountDropdownBtn({handle}: {handle: string}) {
const store = useStores()
+ const pal = usePalette('default')
const items = [
{
label: 'Remove account',
@@ -347,12 +501,54 @@ function AccountDropdownBtn({handle}: {handle: string}) {
return (
-
+
)
}
+interface SelectableBtnProps {
+ current: string
+ value: string
+ label: string
+ left?: boolean
+ right?: boolean
+ onChange: (v: string) => void
+}
+
+function SelectableBtn({
+ current,
+ value,
+ label,
+ left,
+ right,
+ onChange,
+}: SelectableBtnProps) {
+ const pal = usePalette('default')
+ const palPrimary = usePalette('inverted')
+ return (
+ onChange(value)}
+ accessibilityRole="button"
+ accessibilityLabel={value}
+ accessibilityHint={`Set color theme to ${value}`}>
+
+ {label}
+
+
+ )
+}
+
const styles = StyleSheet.create({
dimmed: {
opacity: 0.5,
@@ -364,6 +560,10 @@ const styles = StyleSheet.create({
paddingHorizontal: 18,
paddingBottom: 6,
},
+ infoLine: {
+ paddingHorizontal: 18,
+ paddingBottom: 6,
+ },
profile: {
flexDirection: 'row',
marginVertical: 6,
@@ -400,4 +600,45 @@ const styles = StyleSheet.create({
paddingVertical: 8,
paddingHorizontal: 18,
},
+
+ colorModeText: {
+ marginLeft: 10,
+ marginBottom: 6,
+ },
+
+ selectableBtns: {
+ flexDirection: 'row',
+ },
+ selectableBtn: {
+ flex: isDesktopWeb ? undefined : 1,
+ width: isDesktopWeb ? 100 : undefined,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ borderWidth: 1,
+ borderLeftWidth: 0,
+ paddingHorizontal: 10,
+ paddingVertical: 10,
+ },
+ selectableBtnLeft: {
+ borderTopLeftRadius: 8,
+ borderBottomLeftRadius: 8,
+ borderLeftWidth: 1,
+ },
+ selectableBtnRight: {
+ borderTopRightRadius: 8,
+ borderBottomRightRadius: 8,
+ },
+
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ borderRadius: 32,
+ padding: 14,
+ backgroundColor: colors.gray1,
+ },
+ toggleBtn: {
+ paddingHorizontal: 0,
+ },
})
diff --git a/src/view/screens/TermsOfService.tsx b/src/view/screens/TermsOfService.tsx
index 804200e077..09b2a7f22b 100644
--- a/src/view/screens/TermsOfService.tsx
+++ b/src/view/screens/TermsOfService.tsx
@@ -1,14 +1,14 @@
import React from 'react'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
+import {Text} from 'view/com/util/text/Text'
+import {TextLink} from 'view/com/util/Link'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {ViewHeader} from '../com/util/ViewHeader'
import {useStores} from 'state/index'
import {ScrollView} from 'view/com/util/Views'
-import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
-import Html from '../../locale/en/terms-of-service'
type Props = NativeStackScreenProps
export const TermsOfServiceScreen = (_props: Props) => {
@@ -26,10 +26,14 @@ export const TermsOfServiceScreen = (_props: Props) => {
-
- Terms of Service
+
+ The Terms of Service have been moved to{' '}
+
-
diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx
index e0a75090da..e87fea647e 100644
--- a/src/view/shell/Composer.tsx
+++ b/src/view/shell/Composer.tsx
@@ -56,7 +56,10 @@ export const Composer = observer(
}
return (
-
+
-
+
+
{
const theme = useTheme()
@@ -46,9 +50,11 @@ export const DrawerContent = observer(() => {
const store = useStores()
const navigation = useNavigation()
const {track} = useAnalytics()
- const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
+ const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} =
useNavigationTabState()
+ const {notifications} = store.me
+
// events
// =
@@ -91,6 +97,17 @@ export const DrawerContent = observer(() => {
onPressTab('MyProfile')
}, [onPressTab])
+ const onPressMyFeeds = React.useCallback(
+ () => onPressTab('Feeds'),
+ [onPressTab],
+ )
+
+ const onPressModeration = React.useCallback(() => {
+ track('Menu:ItemClicked', {url: 'Moderation'})
+ navigation.navigate('Moderation')
+ store.shell.closeDrawer()
+ }, [navigation, track, store.shell])
+
const onPressSettings = React.useCallback(() => {
track('Menu:ItemClicked', {url: 'Settings'})
navigation.navigate('Settings')
@@ -101,12 +118,6 @@ export const DrawerContent = observer(() => {
track('Menu:FeedbackClicked')
Linking.openURL(FEEDBACK_FORM_URL)
}, [track])
-
- const onDarkmodePress = React.useCallback(() => {
- track('Menu:ItemClicked', {url: '#darkmode'})
- store.shell.setDarkMode(!store.shell.darkMode)
- }, [track, store])
-
// rendering
// =
@@ -119,7 +130,11 @@ export const DrawerContent = observer(() => {
]}>
-
+
{
type="xl"
style={[pal.textLight, styles.profileCardFollowers]}>
- {store.me.followersCount || 0}
+ {formatCountShortOnly(store.me.followersCount ?? 0)}
{' '}
{pluralize(store.me.followersCount || 0, 'follower')} ·{' '}
- {store.me.followsCount || 0}
+ {formatCountShortOnly(store.me.followsCount ?? 0)}
{' '}
following
-
-
+
{
)
}
label="Search"
+ accessibilityLabel="Search"
+ accessibilityHint=""
bold={isAtSearch}
onPress={onPressSearch}
/>
@@ -183,6 +199,8 @@ export const DrawerContent = observer(() => {
)
}
label="Home"
+ accessibilityLabel="Home"
+ accessibilityHint=""
bold={isAtHome}
onPress={onPressHome}
/>
@@ -203,10 +221,44 @@ export const DrawerContent = observer(() => {
)
}
label="Notifications"
- count={store.me.notifications.unreadCountLabel}
+ accessibilityLabel="Notifications"
+ accessibilityHint={
+ notifications.unreadCountLabel === ''
+ ? ''
+ : `${notifications.unreadCountLabel} unread`
+ }
+ count={notifications.unreadCountLabel}
bold={isAtNotifications}
onPress={onPressNotifications}
/>
+
+ ) : (
+
+ )
+ }
+ label="My Feeds"
+ accessibilityLabel="My Feeds"
+ accessibilityHint=""
+ onPress={onPressMyFeeds}
+ />
+ }
+ label="Moderation"
+ accessibilityLabel="Moderation"
+ accessibilityHint=""
+ onPress={onPressModeration}
+ />
{
)
}
label="Profile"
+ accessibilityLabel="Profile"
+ accessibilityHint=""
onPress={onPressProfile}
/>
{
/>
}
label="Settings"
+ accessibilityLabel="Settings"
+ accessibilityHint=""
onPress={onPressSettings}
/>
-
-
+
+
- {!isWeb && (
-
- }
- strokeWidth={2}
- />
-
- )}
{
)
})
-function MenuItem({
- icon,
- label,
- count,
- bold,
- onPress,
-}: {
+interface MenuItemProps extends ComponentProps {
icon: JSX.Element
label: string
count?: string
bold?: boolean
- onPress: () => void
-}) {
+}
+
+function MenuItem({
+ icon,
+ label,
+ accessibilityLabel,
+ count,
+ bold,
+ onPress,
+}: MenuItemProps) {
const pal = usePalette('default')
return (
+ onPress={onPress}
+ accessibilityRole="tab"
+ accessibilityLabel={accessibilityLabel}
+ accessibilityHint="">
{icon}
{count ? (
@@ -331,6 +379,7 @@ const InviteCodes = observer(() => {
const {track} = useAnalytics()
const store = useStores()
const pal = usePalette('default')
+ const {invitesAvailable} = store.me
const onPress = React.useCallback(() => {
track('Menu:ItemClicked', {url: '#invite-codes'})
store.shell.closeDrawer()
@@ -340,7 +389,14 @@ const InviteCodes = observer(() => {
+ onPress={onPress}
+ accessibilityRole="button"
+ accessibilityLabel={
+ invitesAvailable === 1
+ ? 'Invite codes: 1 available'
+ : `Invite codes: ${invitesAvailable} available`
+ }
+ accessibilityHint="Opens list of invite codes">
{
0 ? pal.link : pal.textLight}>
- {store.me.invitesAvailable} invite{' '}
+ {formatCount(store.me.invitesAvailable)} invite{' '}
{pluralize(store.me.invitesAvailable, 'code')}
@@ -364,12 +420,17 @@ const styles = StyleSheet.create({
flex: 1,
paddingTop: 20,
paddingBottom: 50,
+ maxWidth: 300,
},
viewDarkMode: {
backgroundColor: '#1B1919',
},
main: {
paddingLeft: 20,
+ paddingTop: 20,
+ },
+ smallSpacer: {
+ height: 20,
},
profileCardDisplayName: {
@@ -445,9 +506,6 @@ const styles = StyleSheet.create({
padding: 10,
borderRadius: 25,
},
- footerBtnDarkMode: {
- backgroundColor: colors.black,
- },
footerBtnFeedback: {
paddingHorizontal: 24,
},
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index 5ac54968f4..cdff261a8e 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {ComponentProps} from 'react'
import {
Animated,
GestureResponderEvent,
@@ -18,16 +18,17 @@ import {
HomeIconSolid,
MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid,
+ SatelliteDishIcon,
+ SatelliteDishIconSolid,
BellIcon,
BellIconSolid,
- UserIcon,
- UserIconSolid,
} from 'lib/icons'
import {usePalette} from 'lib/hooks/usePalette'
import {getTabState, TabState} from 'lib/routes/helpers'
import {styles} from './BottomBarStyles'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
+import {UserAvatar} from 'view/com/util/UserAvatar'
type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile'
@@ -36,10 +37,11 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets()
const {track} = useAnalytics()
- const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile} =
+ const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} =
useNavigationTabState()
const {footerMinimalShellTransform} = useMinimalShellMode()
+ const {notifications} = store.me
const onPressTab = React.useCallback(
(tab: TabOptions) => {
@@ -61,6 +63,10 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
() => onPressTab('Search'),
[onPressTab],
)
+ const onPressFeeds = React.useCallback(
+ () => onPressTab('Feeds'),
+ [onPressTab],
+ )
const onPressNotifications = React.useCallback(
() => onPressTab('Notifications'),
[onPressTab],
@@ -96,6 +102,9 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
)
}
onPress={onPressHome}
+ accessibilityRole="tab"
+ accessibilityLabel="Home"
+ accessibilityHint=""
/>
{
)
}
onPress={onPressSearch}
+ accessibilityRole="search"
+ accessibilityLabel="Search"
+ accessibilityHint=""
+ />
+
+ ) : (
+
+ )
+ }
+ onPress={onPressFeeds}
+ accessibilityRole="tab"
+ accessibilityLabel="Feeds"
+ accessibilityHint=""
/>
{
)
}
onPress={onPressNotifications}
- notificationCount={store.me.notifications.unreadCountLabel}
+ notificationCount={notifications.unreadCountLabel}
+ accessible={true}
+ accessibilityRole="tab"
+ accessibilityLabel="Notifications"
+ accessibilityHint={
+ notifications.unreadCountLabel === ''
+ ? ''
+ : `${notifications.unreadCountLabel} unread`
+ }
/>
{isAtMyProfile ? (
-
+
+
+
) : (
-
+
+
+
)}
}
onPress={onPressProfile}
+ accessibilityRole="tab"
+ accessibilityLabel="Profile"
+ accessibilityHint=""
/>
)
})
+interface BtnProps
+ extends Pick<
+ ComponentProps,
+ | 'accessible'
+ | 'accessibilityRole'
+ | 'accessibilityHint'
+ | 'accessibilityLabel'
+ > {
+ testID?: string
+ icon: JSX.Element
+ notificationCount?: string
+ onPress?: (event: GestureResponderEvent) => void
+ onLongPress?: (event: GestureResponderEvent) => void
+}
+
function Btn({
testID,
icon,
notificationCount,
onPress,
onLongPress,
-}: {
- testID?: string
- icon: JSX.Element
- notificationCount?: string
- onPress?: (event: GestureResponderEvent) => void
- onLongPress?: (event: GestureResponderEvent) => void
-}) {
+ accessible,
+ accessibilityHint,
+ accessibilityLabel,
+}: BtnProps) {
return (
+ onLongPress={onLongPress}
+ accessible={accessible}
+ accessibilityLabel={accessibilityLabel}
+ accessibilityHint={accessibilityHint}>
{notificationCount ? (
{notificationCount}
diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx
index 3d5adbc9eb..2414b99112 100644
--- a/src/view/shell/bottom-bar/BottomBarStyles.tsx
+++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx
@@ -58,4 +58,9 @@ export const styles = StyleSheet.create({
profileIcon: {
top: -4,
},
+ onProfile: {
+ borderColor: colors.black,
+ borderWidth: 1,
+ borderRadius: 100,
+ },
})
diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx
index b7daac5af8..cbaafd1fdf 100644
--- a/src/view/shell/bottom-bar/BottomBarWeb.tsx
+++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx
@@ -15,6 +15,8 @@ import {
HomeIconSolid,
MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid,
+ SatelliteDishIcon,
+ SatelliteDishIconSolid,
UserIcon,
} from 'lib/icons'
import {Link} from 'view/com/util/Link'
@@ -61,6 +63,18 @@ export const BottomBarWeb = observer(() => {
)
}}
+
+ {({isActive}) => {
+ const Icon = isActive ? SatelliteDishIconSolid : SatelliteDishIcon
+ return (
+
+ )
+ }}
+
{({isActive}) => {
const Icon = isActive ? BellIconSolid : BellIcon
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index bcff844f1d..9f047418b8 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -2,7 +2,11 @@ import React from 'react'
import {observer} from 'mobx-react-lite'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {PressableWithHover} from 'view/com/util/PressableWithHover'
-import {useNavigation, useNavigationState} from '@react-navigation/native'
+import {
+ useLinkProps,
+ useNavigation,
+ useNavigationState,
+} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -25,9 +29,12 @@ import {
CogIcon,
CogIconSolid,
ComposeIcon2,
+ HandIcon,
+ SatelliteDishIcon,
+ SatelliteDishIconSolid,
} from 'lib/icons'
import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers'
-import {NavigationProp} from 'lib/routes/types'
+import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types'
import {router} from '../../../routes'
const ProfileCard = observer(() => {
@@ -59,7 +66,10 @@ function BackBtn() {
+ style={styles.backBtn}
+ accessibilityRole="button"
+ accessibilityLabel="Go back"
+ accessibilityHint="">
{
const pal = usePalette('default')
+ const store = useStores()
const [pathName] = React.useMemo(() => router.matchPath(href), [href])
- const currentRouteName = useNavigationState(state => {
+ const currentRouteInfo = useNavigationState(state => {
if (!state) {
- return 'Home'
+ return {name: 'Home'}
}
- return getCurrentRoute(state).name
+ return getCurrentRoute(state)
})
- const isCurrent = isTab(currentRouteName, pathName)
+ let isCurrent =
+ currentRouteInfo.name === 'Profile'
+ ? isTab(currentRouteInfo.name, pathName) &&
+ (currentRouteInfo.params as CommonNavigatorParams['Profile']).name ===
+ store.me.handle
+ : isTab(currentRouteInfo.name, pathName)
+ const {onPress} = useLinkProps({to: href})
+ const onPressWrapped = React.useCallback(
+ (e: React.MouseEvent) => {
+ if (e.ctrlKey || e.metaKey || e.altKey) {
+ return
+ }
+ e.preventDefault()
+ if (isCurrent) {
+ store.emitScreenSoftReset()
+ } else {
+ onPress()
+ }
+ },
+ [onPress, isCurrent, store],
+ )
return (
-
-
- {isCurrent ? iconFilled : icon}
- {typeof count === 'string' && count && (
-
- {count}
-
- )}
-
-
- {label}
-
-
+ hoverStyle={pal.viewLight}
+ // @ts-ignore the function signature differs on web -prf
+ onPress={onPressWrapped}
+ // @ts-ignore web only -prf
+ href={href}
+ dataSet={{noUnderline: 1}}
+ accessibilityRole="tab"
+ accessibilityLabel={label}
+ accessibilityHint="">
+
+ {isCurrent ? iconFilled : icon}
+ {typeof count === 'string' && count ? (
+
+ {count}
+
+ ) : null}
+
+
+ {label}
+
)
},
@@ -115,7 +152,12 @@ function ComposeBtn() {
const onPressCompose = () => store.shell.openComposer({})
return (
-
+
+
+ }
+ iconFilled={
+
+ }
+ label="My Feeds"
+ />
+
+ }
+ iconFilled={
+
+ }
+ label="Moderation"
+ />
{store.session.hasSession && (
{
- store.shell.setDarkMode(!store.shell.darkMode)
- }, [store])
+ const palError = usePalette('error')
return (
{store.session.hasSession && }
-
- Welcome to Bluesky! This is a beta application that's still in
- development.
-
+ {store.session.isSandbox ? (
+
+
+ SANDBOX. Posts and accounts are not permanent.
+
+
+ ) : (
+
+ Welcome to Bluesky! This is a beta application that's still in
+ development.
+
+ )}
-
-
-
-
-
-
- {mode} mode
-
-
-
)
})
@@ -78,13 +69,22 @@ const InviteCodes = observer(() => {
const store = useStores()
const pal = usePalette('default')
+ const {invitesAvailable} = store.me
+
const onPress = React.useCallback(() => {
store.shell.openModal({name: 'invite-codes'})
}, [store])
return (
+ onPress={onPress}
+ accessibilityRole="button"
+ accessibilityLabel={
+ invitesAvailable === 1
+ ? 'Invite codes: 1 available'
+ : `Invite codes: ${invitesAvailable} available`
+ }
+ accessibilityHint="Opens list of invite codes">
{
0 ? pal.link : pal.textLight}>
- {store.me.invitesAvailable} invite{' '}
+ {formatCount(store.me.invitesAvailable)} invite{' '}
{pluralize(store.me.invitesAvailable, 'code')} available
@@ -107,8 +107,8 @@ const styles = StyleSheet.create({
rightNav: {
position: 'absolute',
top: 20,
- left: 'calc(50vw + 330px)',
- width: 300,
+ left: 'calc(50vw + 310px)',
+ width: 304,
},
message: {
@@ -130,19 +130,4 @@ const styles = StyleSheet.create({
inviteCodesIcon: {
marginRight: 6,
},
-
- darkModeToggle: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- marginHorizontal: 12,
- },
- darkModeToggleIcon: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- width: 26,
- height: 26,
- borderRadius: 15,
- },
})
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index 9954719443..c7b322b581 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -67,10 +67,18 @@ export const DesktopSearch = observer(function DesktopSearch() {
onBlur={() => setIsInputFocused(false)}
onChangeText={onChangeQuery}
onSubmitEditing={onSubmit}
+ accessibilityRole="search"
+ accessibilityLabel="Search"
+ accessibilityHint=""
/>
{query ? (
-
+
Cancel
@@ -85,14 +93,7 @@ export const DesktopSearch = observer(function DesktopSearch() {
{autocompleteView.searchRes.length ? (
<>
{autocompleteView.searchRes.map((item, i) => (
-
+
))}
>
) : (
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index eab050fd0a..08a93868b2 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -13,11 +13,15 @@ import {DrawerContent} from './Drawer'
import {Composer} from './Composer'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
+import * as backHandler from 'lib/routes/back-handler'
import {RoutesContainer, TabsNavigator} from '../../Navigation'
import {isStateAtTabRoot} from 'lib/routes/helpers'
+import {SafeAreaProvider} from 'react-native-safe-area-context'
+import {useOTAUpdate} from 'lib/hooks/useOTAUpdate'
const ShellInner = observer(() => {
const store = useStores()
+ useOTAUpdate() // this hook polls for OTA updates every few seconds
const winDim = useWindowDimensions()
const safeAreaInsets = useSafeAreaInsets()
const containerPadding = React.useMemo(
@@ -34,6 +38,9 @@ const ShellInner = observer(() => {
[store],
)
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
+ React.useEffect(() => {
+ backHandler.init(store)
+ }, [store])
return (
<>
@@ -54,7 +61,6 @@ const ShellInner = observer(() => {
-
{
onPost={store.shell.composerOpts?.onPost}
quote={store.shell.composerOpts?.quote}
/>
+
>
)
})
export const Shell: React.FC = observer(() => {
- const theme = useTheme()
const pal = usePalette('default')
+ const theme = useTheme()
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
)
})
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 5d7ed259a3..5e38752683 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {useEffect} from 'react'
import {observer} from 'mobx-react-lite'
import {View, StyleSheet, TouchableOpacity} from 'react-native'
import {useStores} from 'state/index'
@@ -14,13 +14,21 @@ import {RoutesContainer, FlatNavigator} from '../../Navigation'
import {DrawerContent} from './Drawer'
import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries'
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
-import {usePalette} from 'lib/hooks/usePalette'
+import {useNavigation} from '@react-navigation/native'
+import {NavigationProp} from 'lib/routes/types'
const ShellInner = observer(() => {
const store = useStores()
- const pal = usePalette('default')
const {isDesktop} = useWebMediaQueries()
+ const navigator = useNavigation()
+
+ useEffect(() => {
+ navigator.addListener('state', () => {
+ store.shell.closeAnyActiveElement()
+ })
+ }, [navigator, store.shell])
+
return (
<>
@@ -28,24 +36,10 @@ const ShellInner = observer(() => {
- {isDesktop && (
+ {isDesktop && store.session.hasSession && (
<>
-
-
>
)}
{
{!isDesktop && store.shell.isDrawerOpen && (
store.shell.closeDrawer()}
- style={styles.drawerMask}>
+ style={styles.drawerMask}
+ accessibilityLabel="Close navigation footer"
+ accessibilityHint="Closes bottom navigation bar">
@@ -90,18 +86,6 @@ const styles = StyleSheet.create({
bgDark: {
backgroundColor: colors.black, // TODO
},
- viewBorder: {
- position: 'absolute',
- width: 1,
- height: '100%',
- borderLeftWidth: 1,
- },
- viewBorderLeft: {
- left: 'calc(50vw - 300px)',
- },
- viewBorderRight: {
- left: 'calc(50vw + 300px)',
- },
drawerMask: {
position: 'absolute',
width: '100%',
diff --git a/tsconfig.check.json b/tsconfig.check.json
new file mode 100644
index 0000000000..f2e29bbb78
--- /dev/null
+++ b/tsconfig.check.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["__e2e__", "dist"],
+}
diff --git a/web/index.html b/web/index.html
index b1b9d51ddd..1bfa8f68d0 100644
--- a/web/index.html
+++ b/web/index.html
@@ -60,10 +60,6 @@
}
}*/
- /* Remove focus state on inputs */
- *:focus {
- outline: 0;
- }
/* Remove default link styling */
a {
color: inherit;
@@ -71,6 +67,9 @@
a[role="link"]:hover {
text-decoration: underline;
}
+ a[role="link"][data-no-underline="1"]:hover {
+ text-decoration: none;
+ }
/* Styling hacks */
*[data-word-wrap] {
@@ -102,28 +101,17 @@
color: #0085ff;
cursor: pointer;
}
+ /* OLLIE: TODO -- this is not accessible */
+ /* Remove focus state on inputs */
+ .ProseMirror-focused {
+ outline: 0;
+ }
+ textarea:focus,
+ input:focus {
+ outline: 0;
+ }
.tippy-content .items {
- border-radius: 6px;
- background: #F3F3F8;
- border: 1px solid #e0d9d9;
- padding: 3px 3px;
- }
- .tippy-content .items .item {
- display: block;
- background: transparent;
- color: #8a8c9a;
- border: 0;
- font: 17px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- padding: 7px 10px 8px;
- width: 100%;
- text-align: left;
- box-sizing: border-box;
- letter-spacing: 0.2px;
- }
- .tippy-content .items .item.is-selected {
- background: #fff;
- border-radius: 4px;
- color: #333;
+ width: fit-content;
}
diff --git a/yarn.lock b/yarn.lock
index f1cb70cf8c..b9e8fe5b0c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,12 +2,22 @@
# yarn lockfile v1
+"@0no-co/graphql.web@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@0no-co/graphql.web/-/graphql.web-1.0.1.tgz#db3da0d2cd41548b50f0583c0d2f4743c767e56b"
+ integrity sha512-6Yaxyv6rOwRkLIvFaL0NrLDgfNqC/Ng9QOPmTmlqW4mORXMEKmh5NYGkIvvt5Yw8fZesnMAqkj8cIqTj8f40cQ==
+
+"@alloc/quick-lru@^5.2.0":
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
+ integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
+
"@ampproject/remapping@^2.2.0":
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
- integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
+ integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
dependencies:
- "@jridgewell/gen-mapping" "^0.1.0"
+ "@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@apideck/better-ajv-errors@^0.3.1":
@@ -20,9 +30,9 @@
leven "^3.1.0"
"@atproto/api@*":
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.0.tgz#4a60f8f1de91105ad93526d69abcf011bbeaa3be"
- integrity sha512-AntqYOVrMalBJapnNBV0akh/PWcsKdWq8zfuvv8hZW/jwOkJTVPTRFOP2OHJFcfz4WezytX43ml/L2kSG9z4+Q==
+ version "0.3.10"
+ resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.10.tgz#98f0ec7b52356f46dc50981a03b64d268b150f96"
+ integrity sha512-6vbJ96kvnvzjUW9OjO5fU/v97UZl4hUwYAjGgRNU1C6Wk8OWa3R2CYolSeotV6pMi9hKpiEb3+cv+Noumthivg==
dependencies:
"@atproto/common-web" "*"
"@atproto/uri" "*"
@@ -30,10 +40,10 @@
tlds "^1.234.0"
typed-emitter "^2.1.0"
-"@atproto/api@0.2.7":
- version "0.2.7"
- resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.7.tgz#ca917a8e7f5054c32c11ce09c82424fbe24112cd"
- integrity sha512-Sz+lLD5apC2f0FSClkElIrt4w+aLgzqJ/wqtFO7xuQH8+hGfxdfGuVIK5GEDQ7epeDlWvVhVSouP6ZdGSKKtSA==
+"@atproto/api@0.3.8":
+ version "0.3.8"
+ resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.8.tgz#3fc0ebd092cc212c2d0b31a600fe1945a02f9cf7"
+ integrity sha512-7qaIZGEP5J9FW4z8bXezzAmLRzHSXXHo6bWP9Jyu5MLp8tYt9vG6yR2N0QA7GvO0xSYqP87Q5vblPjYXGqtDKg==
dependencies:
"@atproto/common-web" "*"
"@atproto/uri" "*"
@@ -41,16 +51,6 @@
tlds "^1.234.0"
typed-emitter "^2.1.0"
-"@atproto/auth@*":
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/@atproto/auth/-/auth-0.0.1.tgz#0ae07bfb6e4e86605504a20f0302e448ba3f8b0e"
- integrity sha512-eom7V/LmXttlFE31TcOJ0BInTszkm5ZBS2mqoLqbnA5ZTcTsgQsMKhGzARFf2zwBM9h8pbVa1XMI83gnrTHfxA==
- dependencies:
- "@atproto/crypto" "*"
- "@atproto/did-resolver" "*"
- "@ucans/core" "0.11.0"
- uint8arrays "3.0.0"
-
"@atproto/common-web@*":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.1.0.tgz#5529fa66f9533aa00cfd13f0a25757df7b26bd3d"
@@ -61,14 +61,15 @@
zod "^3.14.2"
"@atproto/common@*":
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.1.tgz#ec33a3b4995c91d3ad2e90fc4cdbc65284ceff84"
- integrity sha512-GYwot5wF/z8iYGSPjrLHuratLc0CVgovmwfJss7+BUOB6y2/Vw8+1Vw0n9DDI0gb5vmx3UI8z0uJgC8aa8yuJg==
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.2.0.tgz#e74502edf636f30e332f516dcb96f7342b71ff1b"
+ integrity sha512-PVYSC30pyonz2MOxuBLk27uGdwyZQ42gJfCA/NE9jLeuenVDmZnVrK5WqJ7eGg+F88rZj7NcGfRsZdP0GMykEQ==
dependencies:
+ "@atproto/common-web" "*"
"@ipld/dag-cbor" "^7.0.3"
+ cbor-x "^1.5.1"
multiformats "^9.6.4"
pino "^8.6.1"
- zod "^3.14.2"
"@atproto/common@0.1.0":
version "0.1.0"
@@ -80,7 +81,28 @@
pino "^8.6.1"
zod "^3.14.2"
-"@atproto/crypto@*", "@atproto/crypto@0.1.0":
+"@atproto/common@0.1.1":
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.1.tgz#ec33a3b4995c91d3ad2e90fc4cdbc65284ceff84"
+ integrity sha512-GYwot5wF/z8iYGSPjrLHuratLc0CVgovmwfJss7+BUOB6y2/Vw8+1Vw0n9DDI0gb5vmx3UI8z0uJgC8aa8yuJg==
+ dependencies:
+ "@ipld/dag-cbor" "^7.0.3"
+ multiformats "^9.6.4"
+ pino "^8.6.1"
+ zod "^3.14.2"
+
+"@atproto/crypto@*":
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.1.tgz#54afad2124c3867091e4d9b271f22d375fcfdf9e"
+ integrity sha512-/7Ntn55dRZPtCnOd6dVo1IvZzpVut6YTAkZ8iFry9JW29l7ZeNkJd+NTnmWRz3aGQody10jngb4SNxQNi/f3+A==
+ dependencies:
+ "@noble/secp256k1" "^1.7.0"
+ big-integer "^1.6.51"
+ multiformats "^9.6.4"
+ one-webcrypto "^1.0.3"
+ uint8arrays "3.0.0"
+
+"@atproto/crypto@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.0.tgz#bc73a479f9dbe06fa025301c182d7f7ab01bc568"
integrity sha512-9xgFEPtsCiJEPt9o3HtJT30IdFTGw5cQRSJVIy5CFhqBA4vDLcdXiRDLCjkzHEVbtNCsHUW6CrlfOgbeLPcmcg==
@@ -92,14 +114,14 @@
uint8arrays "3.0.0"
"@atproto/did-resolver@*":
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/@atproto/did-resolver/-/did-resolver-0.0.1.tgz#e54c1b7fddff2cd6adf87c044b4a3b6f00d5eff7"
- integrity sha512-sdva3+nydMaWXwHJED558UZdVZuajfC2CHcsIZz0pQybicm3VI+khkf42ClZeOhf4Bwa4V4SOaaAqwyf86bDew==
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@atproto/did-resolver/-/did-resolver-0.1.0.tgz#58f42447700aaad61bad2f0d70b721966268aa02"
+ integrity sha512-ztljyMMCqXvJSi/Qqa2zEQFvOm1AUUR7Bybr3cM1BCddbhW46gk6/g8BgdZeDt2sMBdye37qTctR9O/FjhigvQ==
dependencies:
- "@atproto/common" "*"
+ "@atproto/common-web" "*"
"@atproto/crypto" "*"
- axios "^0.24.0"
- did-resolver "^4.0.0"
+ axios "^0.27.2"
+ zod "^3.14.2"
"@atproto/identifier@*":
version "0.1.0"
@@ -109,12 +131,16 @@
"@atproto/common-web" "*"
"@atproto/lexicon@*":
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.0.4.tgz#f0a6688ad54adb2ec4a8d1f11fcbf45e96203c4b"
- integrity sha512-00lqIKJetVlxQzNmEhrFzZeT9k+zGPBsHwtYpG7rH4vZ211i5WiDkmQcBwwFs2g/qCBt+nVq0dlgl3JhCLJXQg==
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.1.0.tgz#e7784cc868c734314d5bf9af83487aba7ccae0b3"
+ integrity sha512-Iy+gV9w42xLhrZrmcbZh7VFoHjXuzWvecGHIfz44owNjjv7aE/d2P5BbOX/XicSkmQ8Qkpg0BqwYDD1XBVS+DQ==
dependencies:
+ "@atproto/common-web" "*"
+ "@atproto/identifier" "*"
"@atproto/nsid" "*"
+ "@atproto/uri" "*"
iso-datestring-validator "^2.2.2"
+ multiformats "^9.6.4"
zod "^3.14.2"
"@atproto/nsid@*":
@@ -122,10 +148,10 @@
resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4"
integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw==
-"@atproto/pds@^0.1.4":
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.4.tgz#43379912e127d6d4f79a514e785dab9b54fd7810"
- integrity sha512-vrFYL+2nNm/0fJyUIgFK9h9FRuEf4rHjU/LJV7/nBO+HA3hP3U/mTgvVxuuHHvcRsRL5AVpAJR0xWFUoYsFmmg==
+"@atproto/pds@^0.1.10":
+ version "0.1.11"
+ resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.11.tgz#ed59803222558842a343021c20517dbf6d19dc42"
+ integrity sha512-5JE4IkZh1b+tRs1qnChVB74g6As1vDJ/nPkZPg8M57VUDy4gdb+bcctV0AHF45EQo6Mvy9lCw9ShWVT/P/tLoQ==
dependencies:
"@atproto/api" "*"
"@atproto/common" "*"
@@ -154,7 +180,7 @@
nodemailer "^6.8.0"
nodemailer-html-to-text "^3.2.0"
p-queue "^6.6.2"
- pg "^8.8.0"
+ pg "^8.10.0"
pino "^8.6.1"
pino-http "^8.2.1"
sharp "^0.31.2"
@@ -162,12 +188,14 @@
uint8arrays "3.0.0"
"@atproto/repo@*":
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.0.1.tgz#41c63943a7e6a0942fc3e721c05d8c836c2fcfc2"
- integrity sha512-tBZjaeaRL7fJynZCA5F+ZjRQuf5fpL7Cj5VqP6KtXYacuNP/LufwrHARSOwxJMMZpPOoWmwv4R8bETiQozehEA==
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.1.0.tgz#8c546af16c30fe5ba4c883ac73b68be9d7eca273"
+ integrity sha512-O4qs5WfSjEFvUtpOTB4n9cLcK6YP/w/ly6Qxc3S8IFevLGYX58NPPr5zlg3dxs64uLKbWWjzhQM7JAqO44MEKw==
dependencies:
- "@atproto/auth" "*"
"@atproto/common" "*"
+ "@atproto/crypto" "*"
+ "@atproto/did-resolver" "*"
+ "@atproto/lexicon" "*"
"@atproto/nsid" "*"
"@ipld/car" "^3.2.3"
"@ipld/dag-cbor" "^7.0.0"
@@ -176,26 +204,33 @@
zod "^3.14.2"
"@atproto/uri@*":
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/@atproto/uri/-/uri-0.0.1.tgz#bfab68eda17ec987647f10d102168d417bc8a326"
- integrity sha512-Tm+20Bxdie+a4yvberrfWaDhrze/p3AvA5v5IV6XyZJYu2+fnionUrufUjkcs3PIWeSd6VMgVcRp3GaoiUvSvQ==
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/@atproto/uri/-/uri-0.0.2.tgz#c6d3788e6f12d66ba72690d2d70fe6c291b4acfb"
+ integrity sha512-/6otLZF7BLpT9suSdHuXLbL12nINcWPsLmcOI+dctqovWUjH+XIRVNXDQgBYSrPVetxMiknuEwWelmnA33AEXg==
+ dependencies:
+ "@atproto/identifier" "*"
+ "@atproto/nsid" "*"
"@atproto/xrpc-server@*":
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.0.1.tgz#62891d8e24b0813a7006d8ba947716b7c69e5667"
- integrity sha512-W9pb9k9wgDlZdDF3eIDMXhEs1trg3zSRd70f1BfN22h+Or4wsoq5dAxXg6q9os3+DNkVkD9BWeRwVppCF6FxGg==
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.2.0.tgz#a36616c2ac70339cd79cda83ede0a0b305c74f9b"
+ integrity sha512-sCJuVUIb1tDIlKCFwHPRHbAgEy0HYGlQ7XhpNqMRKXECh8Z+DRICEne3gLDVaXhyNaC/N7OjHcsyuofDDbuGFQ==
dependencies:
"@atproto/common" "*"
+ "@atproto/crypto" "*"
"@atproto/lexicon" "*"
+ cbor-x "^1.5.1"
express "^4.17.2"
http-errors "^2.0.0"
mime-types "^2.1.35"
+ uint8arrays "3.0.0"
+ ws "^8.12.0"
zod "^3.14.2"
"@atproto/xrpc@*":
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.0.4.tgz#d7dd45cdb21e29b9715ca30eb18320548f293413"
- integrity sha512-Hxh+GgZx21Zvlb2RMlSlJDd3r3GR0vAS6OOZPW2xzWiVHsetb9ZlFB6D0AeAPj2R+U2UUkmdUR8G3U/nkgnQFA==
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.1.0.tgz#798569095538ac060475ae51f1b4c071ff8776d6"
+ integrity sha512-LhBeZkQwPezjEtricGYnG62udFglOqlnmMSS0KyWgEAPi4KMp4H2F4jNoXcf5NPtZ9S4N4hJaErHX4PJYv2lfA==
dependencies:
"@atproto/lexicon" "*"
zod "^3.14.2"
@@ -207,33 +242,33 @@
dependencies:
"@babel/highlight" "^7.10.4"
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
- integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.21.4", "@babel/code-frame@^7.8.3":
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39"
+ integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==
dependencies:
"@babel/highlight" "^7.18.6"
-"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.0.tgz#c241dc454e5b5917e40d37e525e2f4530c399298"
- integrity sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==
+"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.0", "@babel/compat-data@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.3.tgz#cd502a6a0b6e37d7ad72ce7e71a7160a3ae36f7e"
+ integrity sha512-aNtko9OPOwVESUFp3MZfD8Uzxl7JzSeJpd7npIoxCasU37PFbAQRpKglkaKwlHOyeJdrREpo8TW8ldrkYWwvIQ==
-"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.16.0", "@babel/core@^7.20.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
- version "7.21.3"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.3.tgz#cf1c877284a469da5d1ce1d1e53665253fae712e"
- integrity sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==
+"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.14.0", "@babel/core@^7.16.0", "@babel/core@^7.20.0", "@babel/core@^7.20.2", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
+ version "7.22.1"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.1.tgz#5de51c5206f4c6f5533562838337a603c1033cfd"
+ integrity sha512-Hkqu7J4ynysSXxmAahpN1jjRwVJ+NdpraFLIWflgjpVob3KNyK3/tIUc7Q7szed8WMp0JNa7Qtd1E9Oo22F9gA==
dependencies:
"@ampproject/remapping" "^2.2.0"
- "@babel/code-frame" "^7.18.6"
- "@babel/generator" "^7.21.3"
- "@babel/helper-compilation-targets" "^7.20.7"
- "@babel/helper-module-transforms" "^7.21.2"
- "@babel/helpers" "^7.21.0"
- "@babel/parser" "^7.21.3"
- "@babel/template" "^7.20.7"
- "@babel/traverse" "^7.21.3"
- "@babel/types" "^7.21.3"
+ "@babel/code-frame" "^7.21.4"
+ "@babel/generator" "^7.22.0"
+ "@babel/helper-compilation-targets" "^7.22.1"
+ "@babel/helper-module-transforms" "^7.22.1"
+ "@babel/helpers" "^7.22.0"
+ "@babel/parser" "^7.22.0"
+ "@babel/template" "^7.21.9"
+ "@babel/traverse" "^7.22.1"
+ "@babel/types" "^7.22.0"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
@@ -241,20 +276,20 @@
semver "^6.3.0"
"@babel/eslint-parser@^7.16.3", "@babel/eslint-parser@^7.18.2":
- version "7.21.3"
- resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.21.3.tgz#d79e822050f2de65d7f368a076846e7184234af7"
- integrity sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==
+ version "7.21.8"
+ resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.21.8.tgz#59fb6fc4f3b017ab86987c076226ceef7b2b2ef2"
+ integrity sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ==
dependencies:
"@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1"
eslint-visitor-keys "^2.1.0"
semver "^6.3.0"
-"@babel/generator@^7.20.0", "@babel/generator@^7.21.3", "@babel/generator@^7.7.2":
- version "7.21.3"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.3.tgz#232359d0874b392df04045d72ce2fd9bb5045fce"
- integrity sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==
+"@babel/generator@^7.20.0", "@babel/generator@^7.20.4", "@babel/generator@^7.22.0", "@babel/generator@^7.22.3", "@babel/generator@^7.7.2":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.3.tgz#0ff675d2edb93d7596c5f6728b52615cfc0df01e"
+ integrity sha512-C17MW4wlk//ES/CJDL51kPNwl+qiBQyN7b9SKyVp11BLGFeSPoVaHrv+MNt8jwQFhQWowW88z1eeBx3pFz9v8A==
dependencies:
- "@babel/types" "^7.21.3"
+ "@babel/types" "^7.22.3"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
@@ -267,50 +302,51 @@
"@babel/types" "^7.18.6"
"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6":
- version "7.18.9"
- resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb"
- integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.3.tgz#c9b83d1ba74e163e023f008a3d3204588a7ceb60"
+ integrity sha512-ahEoxgqNoYXm0k22TvOke48i1PkavGu0qGCmcq9ugi6gnmvKNaMjKBSrZTnWUi1CFEeNAUiVba0Wtzm03aSkJg==
dependencies:
- "@babel/helper-explode-assignable-expression" "^7.18.6"
- "@babel/types" "^7.18.9"
+ "@babel/types" "^7.22.3"
-"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb"
- integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==
+"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.1":
+ version "7.22.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.1.tgz#bfcd6b7321ffebe33290d68550e2c9d7eb7c7a58"
+ integrity sha512-Rqx13UM3yVB5q0D/KwQ8+SPfX/+Rnsy1Lw1k/UwOC4KC6qrzIQoY3lYnBu5EHKBlEHHcj0M0W8ltPSkD8rqfsQ==
dependencies:
- "@babel/compat-data" "^7.20.5"
- "@babel/helper-validator-option" "^7.18.6"
+ "@babel/compat-data" "^7.22.0"
+ "@babel/helper-validator-option" "^7.21.0"
browserslist "^4.21.3"
lru-cache "^5.1.1"
semver "^6.3.0"
-"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz#64f49ecb0020532f19b1d014b03bccaa1ab85fb9"
- integrity sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==
+"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.22.1":
+ version "7.22.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.1.tgz#ae3de70586cc757082ae3eba57240d42f468c41b"
+ integrity sha512-SowrZ9BWzYFgzUMwUmowbPSGu6CXL5MSuuCkG3bejahSpSymioPmuLdhPxNOc9MjuNGjy7M/HaXvJ8G82Lywlw==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
- "@babel/helper-environment-visitor" "^7.18.9"
+ "@babel/helper-environment-visitor" "^7.22.1"
"@babel/helper-function-name" "^7.21.0"
- "@babel/helper-member-expression-to-functions" "^7.21.0"
+ "@babel/helper-member-expression-to-functions" "^7.22.0"
"@babel/helper-optimise-call-expression" "^7.18.6"
- "@babel/helper-replace-supers" "^7.20.7"
+ "@babel/helper-replace-supers" "^7.22.1"
"@babel/helper-skip-transparent-expression-wrappers" "^7.20.0"
"@babel/helper-split-export-declaration" "^7.18.6"
+ semver "^6.3.0"
-"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz#53ff78472e5ce10a52664272a239787107603ebb"
- integrity sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg==
+"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.1":
+ version "7.22.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.1.tgz#a7ed9a8488b45b467fca353cd1a44dc5f0cf5c70"
+ integrity sha512-WWjdnfR3LPIe+0EY8td7WmjhytxXtjKAEpnAxun/hkNiyOaPlvGK+NZaBFIdi9ndYV3Gav7BpFvtUwnaJlwi1w==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
regexpu-core "^5.3.1"
+ semver "^6.3.0"
-"@babel/helper-define-polyfill-provider@^0.3.3":
- version "0.3.3"
- resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a"
- integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==
+"@babel/helper-define-polyfill-provider@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz#487053f103110f25b9755c5980e031e93ced24d8"
+ integrity sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==
dependencies:
"@babel/helper-compilation-targets" "^7.17.7"
"@babel/helper-plugin-utils" "^7.16.7"
@@ -319,17 +355,10 @@
resolve "^1.14.2"
semver "^6.1.2"
-"@babel/helper-environment-visitor@^7.18.9":
- version "7.18.9"
- resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
- integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
-
-"@babel/helper-explode-assignable-expression@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
- integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==
- dependencies:
- "@babel/types" "^7.18.6"
+"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.22.1":
+ version "7.22.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.1.tgz#ac3a56dbada59ed969d712cf527bd8271fe3eba8"
+ integrity sha512-Z2tgopurB/kTbidvzeBrc2To3PUP/9i5MUe+fU6QJCQDyPwSH2oRapkLw3KGECDYSjhQZCNxEvNvZlLw8JjGwA==
"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0":
version "7.21.0"
@@ -346,33 +375,33 @@
dependencies:
"@babel/types" "^7.18.6"
-"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5"
- integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==
+"@babel/helper-member-expression-to-functions@^7.22.0":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.3.tgz#4b77a12c1b4b8e9e28736ed47d8b91f00976911f"
+ integrity sha512-Gl7sK04b/2WOb6OPVeNy9eFKeD3L6++CzL3ykPOWqTn08xgYYK0wz4TUh2feIImDXxcVW3/9WQ1NMKY66/jfZA==
dependencies:
- "@babel/types" "^7.21.0"
+ "@babel/types" "^7.22.3"
-"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
- integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
+"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.21.4":
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz#ac88b2f76093637489e718a90cec6cf8a9b029af"
+ integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==
dependencies:
- "@babel/types" "^7.18.6"
+ "@babel/types" "^7.21.4"
-"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2":
- version "7.21.2"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2"
- integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==
+"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.5", "@babel/helper-module-transforms@^7.22.1":
+ version "7.22.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.1.tgz#e0cad47fedcf3cae83c11021696376e2d5a50c63"
+ integrity sha512-dxAe9E7ySDGbQdCVOY/4+UcD8M9ZFqZcZhSPsPacvCG4M+9lwtDDQfI2EoaSvmf7W/8yCBkGU0m7Pvt1ru3UZw==
dependencies:
- "@babel/helper-environment-visitor" "^7.18.9"
- "@babel/helper-module-imports" "^7.18.6"
- "@babel/helper-simple-access" "^7.20.2"
+ "@babel/helper-environment-visitor" "^7.22.1"
+ "@babel/helper-module-imports" "^7.21.4"
+ "@babel/helper-simple-access" "^7.21.5"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/helper-validator-identifier" "^7.19.1"
- "@babel/template" "^7.20.7"
- "@babel/traverse" "^7.21.2"
- "@babel/types" "^7.21.2"
+ "@babel/template" "^7.21.9"
+ "@babel/traverse" "^7.22.1"
+ "@babel/types" "^7.22.0"
"@babel/helper-optimise-call-expression@^7.18.6":
version "7.18.6"
@@ -381,10 +410,10 @@
dependencies:
"@babel/types" "^7.18.6"
-"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
- version "7.20.2"
- resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629"
- integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.21.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56"
+ integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==
"@babel/helper-remap-async-to-generator@^7.18.9":
version "7.18.9"
@@ -396,24 +425,24 @@
"@babel/helper-wrap-function" "^7.18.9"
"@babel/types" "^7.18.9"
-"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331"
- integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==
+"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7", "@babel/helper-replace-supers@^7.22.1":
+ version "7.22.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.1.tgz#38cf6e56f7dc614af63a21b45565dd623f0fdc95"
+ integrity sha512-ut4qrkE4AuSfrwHSps51ekR1ZY/ygrP1tp0WFm8oVq6nzc/hvfV/22JylndIbsf2U2M9LOMwiSddr6y+78j+OQ==
dependencies:
- "@babel/helper-environment-visitor" "^7.18.9"
- "@babel/helper-member-expression-to-functions" "^7.20.7"
+ "@babel/helper-environment-visitor" "^7.22.1"
+ "@babel/helper-member-expression-to-functions" "^7.22.0"
"@babel/helper-optimise-call-expression" "^7.18.6"
- "@babel/template" "^7.20.7"
- "@babel/traverse" "^7.20.7"
- "@babel/types" "^7.20.7"
+ "@babel/template" "^7.21.9"
+ "@babel/traverse" "^7.22.1"
+ "@babel/types" "^7.22.0"
-"@babel/helper-simple-access@^7.20.2":
- version "7.20.2"
- resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9"
- integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==
+"@babel/helper-simple-access@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz#d697a7971a5c39eac32c7e63c0921c06c8a249ee"
+ integrity sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==
dependencies:
- "@babel/types" "^7.20.2"
+ "@babel/types" "^7.21.5"
"@babel/helper-skip-transparent-expression-wrappers@^7.20.0":
version "7.20.0"
@@ -429,17 +458,17 @@
dependencies:
"@babel/types" "^7.18.6"
-"@babel/helper-string-parser@^7.19.4":
- version "7.19.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63"
- integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
+"@babel/helper-string-parser@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd"
+ integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==
"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
-"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0":
+"@babel/helper-validator-option@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180"
integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==
@@ -454,14 +483,14 @@
"@babel/traverse" "^7.20.5"
"@babel/types" "^7.20.5"
-"@babel/helpers@^7.21.0":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e"
- integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==
+"@babel/helpers@^7.22.0":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.3.tgz#53b74351da9684ea2f694bf0877998da26dd830e"
+ integrity sha512-jBJ7jWblbgr7r6wYZHMdIqKc73ycaTcCaWRq4/2LpuPHcx7xMlZvpGQkOYc9HeSjn6rcx15CPlgVcBtZ4WZJ2w==
dependencies:
- "@babel/template" "^7.20.7"
- "@babel/traverse" "^7.21.0"
- "@babel/types" "^7.21.0"
+ "@babel/template" "^7.21.9"
+ "@babel/traverse" "^7.22.1"
+ "@babel/types" "^7.22.3"
"@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6":
version "7.18.6"
@@ -472,10 +501,10 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.3":
- version "7.21.3"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.3.tgz#1d285d67a19162ff9daa358d4cb41d50c06220b3"
- integrity sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==
+"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.9", "@babel/parser@^7.22.0", "@babel/parser@^7.22.4":
+ version "7.22.4"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.4.tgz#a770e98fd785c231af9d93f6459d36770993fb32"
+ integrity sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
@@ -484,16 +513,16 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz#d9c85589258539a22a901033853101a6198d4ef1"
- integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==
+"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.3.tgz#a75be1365c0c3188c51399a662168c1c98108659"
+ integrity sha512-6r4yRwEnorYByILoDRnEqxtojYKuiIv9FojW2E8GUKo9eWBwbKcd9IiZOZpdyXc64RmyGGyPu3/uAcrz/dq2kQ==
dependencies:
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-plugin-utils" "^7.21.5"
"@babel/helper-skip-transparent-expression-wrappers" "^7.20.0"
- "@babel/plugin-proposal-optional-chaining" "^7.20.7"
+ "@babel/plugin-transform-optional-chaining" "^7.22.3"
-"@babel/plugin-proposal-async-generator-functions@^7.0.0", "@babel/plugin-proposal-async-generator-functions@^7.20.1":
+"@babel/plugin-proposal-async-generator-functions@^7.0.0":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326"
integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==
@@ -503,7 +532,7 @@
"@babel/helper-remap-async-to-generator" "^7.18.9"
"@babel/plugin-syntax-async-generators" "^7.8.4"
-"@babel/plugin-proposal-class-properties@^7.0.0", "@babel/plugin-proposal-class-properties@^7.13.0", "@babel/plugin-proposal-class-properties@^7.16.0", "@babel/plugin-proposal-class-properties@^7.18.6":
+"@babel/plugin-proposal-class-properties@^7.0.0", "@babel/plugin-proposal-class-properties@^7.13.0", "@babel/plugin-proposal-class-properties@^7.16.0":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3"
integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==
@@ -511,33 +540,16 @@
"@babel/helper-create-class-features-plugin" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-proposal-class-static-block@^7.18.6":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz#77bdd66fb7b605f3a61302d224bdfacf5547977d"
- integrity sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==
- dependencies:
- "@babel/helper-create-class-features-plugin" "^7.21.0"
- "@babel/helper-plugin-utils" "^7.20.2"
- "@babel/plugin-syntax-class-static-block" "^7.14.5"
-
"@babel/plugin-proposal-decorators@^7.12.9", "@babel/plugin-proposal-decorators@^7.16.4":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.21.0.tgz#70e0c89fdcd7465c97593edb8f628ba6e4199d63"
- integrity sha512-MfgX49uRrFUTL/HvWtmx3zmpyzMMr4MTj3d527MLlr/4RTT9G/ytFFP7qet2uM2Ve03b+BkpWUpK+lRXnQ+v9w==
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.22.3.tgz#3502c0f8cfe0cdb79b62102c9c9b111309d942b7"
+ integrity sha512-XjTKH3sHr6pPqG+hR1NCdVupwiosfdKM2oSMyKQVQ5Bym9l/p7BuLAqT5U32zZzRCfPq/TPRPzMiiTE9bOXU4w==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.21.0"
- "@babel/helper-plugin-utils" "^7.20.2"
- "@babel/helper-replace-supers" "^7.20.7"
+ "@babel/helper-create-class-features-plugin" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/helper-replace-supers" "^7.22.1"
"@babel/helper-split-export-declaration" "^7.18.6"
- "@babel/plugin-syntax-decorators" "^7.21.0"
-
-"@babel/plugin-proposal-dynamic-import@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94"
- integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==
- dependencies:
- "@babel/helper-plugin-utils" "^7.18.6"
- "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+ "@babel/plugin-syntax-decorators" "^7.22.3"
"@babel/plugin-proposal-export-default-from@^7.0.0":
version "7.18.10"
@@ -555,23 +567,7 @@
"@babel/helper-plugin-utils" "^7.18.9"
"@babel/plugin-syntax-export-namespace-from" "^7.8.3"
-"@babel/plugin-proposal-json-strings@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b"
- integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==
- dependencies:
- "@babel/helper-plugin-utils" "^7.18.6"
- "@babel/plugin-syntax-json-strings" "^7.8.3"
-
-"@babel/plugin-proposal-logical-assignment-operators@^7.18.9":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83"
- integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==
- dependencies:
- "@babel/helper-plugin-utils" "^7.20.2"
- "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
-
-"@babel/plugin-proposal-nullish-coalescing-operator@^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8", "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6":
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8", "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1"
integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==
@@ -579,7 +575,7 @@
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
-"@babel/plugin-proposal-numeric-separator@^7.16.0", "@babel/plugin-proposal-numeric-separator@^7.18.6":
+"@babel/plugin-proposal-numeric-separator@^7.16.0":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75"
integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==
@@ -587,7 +583,7 @@
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-syntax-numeric-separator" "^7.10.4"
-"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.12.13", "@babel/plugin-proposal-object-rest-spread@^7.20.2":
+"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.12.13":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a"
integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==
@@ -598,7 +594,7 @@
"@babel/plugin-syntax-object-rest-spread" "^7.8.3"
"@babel/plugin-transform-parameters" "^7.20.7"
-"@babel/plugin-proposal-optional-catch-binding@^7.0.0", "@babel/plugin-proposal-optional-catch-binding@^7.18.6":
+"@babel/plugin-proposal-optional-catch-binding@^7.0.0":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb"
integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==
@@ -606,7 +602,7 @@
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
-"@babel/plugin-proposal-optional-chaining@^7.0.0", "@babel/plugin-proposal-optional-chaining@^7.13.12", "@babel/plugin-proposal-optional-chaining@^7.16.0", "@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7":
+"@babel/plugin-proposal-optional-chaining@^7.0.0", "@babel/plugin-proposal-optional-chaining@^7.13.12", "@babel/plugin-proposal-optional-chaining@^7.16.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea"
integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==
@@ -615,7 +611,7 @@
"@babel/helper-skip-transparent-expression-wrappers" "^7.20.0"
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
-"@babel/plugin-proposal-private-methods@^7.16.0", "@babel/plugin-proposal-private-methods@^7.18.6":
+"@babel/plugin-proposal-private-methods@^7.16.0":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea"
integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==
@@ -623,7 +619,7 @@
"@babel/helper-create-class-features-plugin" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-proposal-private-property-in-object@^7.18.6":
+"@babel/plugin-proposal-private-property-in-object@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz#19496bd9883dd83c23c7d7fc45dcd9ad02dfa1dc"
integrity sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==
@@ -633,7 +629,7 @@
"@babel/helper-plugin-utils" "^7.20.2"
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
-"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
+"@babel/plugin-proposal-unicode-property-regex@^7.4.4":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e"
integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==
@@ -669,12 +665,12 @@
dependencies:
"@babel/helper-plugin-utils" "^7.14.5"
-"@babel/plugin-syntax-decorators@^7.21.0":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.21.0.tgz#d2b3f31c3e86fa86e16bb540b7660c55bd7d0e78"
- integrity sha512-tIoPpGBR8UuM4++ccWN3gifhVvQu7ZizuR1fklhRJrd5ewgbkUS+0KVFeWWxELtn18NTLoW32XV7zyOgIAiz+w==
+"@babel/plugin-syntax-decorators@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.22.3.tgz#760f2d812d56c1d05970d01cdcd3c05e3d87d6ca"
+ integrity sha512-R16Zuge73+8/nLcDjkIpyhi5wIbN7i7fiuLJR8yQX7vPAa/ltUKtd3iLbb4AgP5nrLi91HnNUNosELIGUGH1bg==
dependencies:
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-plugin-utils" "^7.21.5"
"@babel/plugin-syntax-dynamic-import@^7.0.0", "@babel/plugin-syntax-dynamic-import@^7.8.3":
version "7.8.3"
@@ -698,11 +694,11 @@
"@babel/helper-plugin-utils" "^7.8.3"
"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.18.0", "@babel/plugin-syntax-flow@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz#774d825256f2379d06139be0c723c4dd444f3ca1"
- integrity sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.21.4.tgz#3e37fca4f06d93567c1cd9b75156422e90a67107"
+ integrity sha512-l9xd3N+XG4fZRxEP3vXdK6RW7vN1Uf5dxzRC/09wV86wqZ/YYQooBIGNsiRdfNR3/q2/5pPzV4B54J/9ctX5jw==
dependencies:
- "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.20.2"
"@babel/plugin-syntax-import-assertions@^7.20.0":
version "7.20.0"
@@ -711,7 +707,14 @@
dependencies:
"@babel/helper-plugin-utils" "^7.19.0"
-"@babel/plugin-syntax-import-meta@^7.8.3":
+"@babel/plugin-syntax-import-attributes@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.3.tgz#d7168f22b9b49a6cc1792cec78e06a18ad2e7b4b"
+ integrity sha512-i35jZJv6aO7hxEbIWQ41adVfOzjm9dcYDNeWlBMd8p0ZQRtNUCBrmGwZt+H5lb+oOC9a3svp956KP0oWGA1YsA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+
+"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
@@ -725,12 +728,12 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.0"
-"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.18.6", "@babel/plugin-syntax-jsx@^7.7.2":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
- integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
+"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.21.4", "@babel/plugin-syntax-jsx@^7.7.2":
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz#f264ed7bf40ffc9ec239edabc17a50c4f5b6fea2"
+ integrity sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==
dependencies:
- "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.20.2"
"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
version "7.10.4"
@@ -788,21 +791,39 @@
dependencies:
"@babel/helper-plugin-utils" "^7.14.5"
-"@babel/plugin-syntax-typescript@^7.20.0", "@babel/plugin-syntax-typescript@^7.7.2":
- version "7.20.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7"
- integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==
- dependencies:
- "@babel/helper-plugin-utils" "^7.19.0"
-
-"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.18.6":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551"
- integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==
+"@babel/plugin-syntax-typescript@^7.21.4", "@babel/plugin-syntax-typescript@^7.7.2":
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz#2751948e9b7c6d771a8efa59340c15d4a2891ff8"
+ integrity sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==
dependencies:
"@babel/helper-plugin-utils" "^7.20.2"
-"@babel/plugin-transform-async-to-generator@^7.0.0", "@babel/plugin-transform-async-to-generator@^7.18.6":
+"@babel/plugin-syntax-unicode-sets-regex@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357"
+ integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz#9bb42a53de447936a57ba256fbf537fc312b6929"
+ integrity sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+
+"@babel/plugin-transform-async-generator-functions@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.3.tgz#3ed99924c354fb9e80dabb2cc8d002c702e94527"
+ integrity sha512-36A4Aq48t66btydbZd5Fk0/xJqbpg/v4QWI4AH4cYHBXy9Mu42UOupZpebKFiCFNT9S9rJFcsld0gsv0ayLjtA==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/helper-remap-async-to-generator" "^7.18.9"
+ "@babel/plugin-syntax-async-generators" "^7.8.4"
+
+"@babel/plugin-transform-async-to-generator@^7.0.0", "@babel/plugin-transform-async-to-generator@^7.20.7":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz#dfee18623c8cb31deb796aa3ca84dda9cea94354"
integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==
@@ -818,14 +839,31 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.20.2":
+"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz#e737b91037e5186ee16b76e7ae093358a5634f02"
integrity sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==
dependencies:
"@babel/helper-plugin-utils" "^7.20.2"
-"@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.20.2":
+"@babel/plugin-transform-class-properties@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.3.tgz#3407145e513830df77f0cef828b8b231c166fe4c"
+ integrity sha512-mASLsd6rhOrLZ5F3WbCxkzl67mmOnqik0zrg5W6D/X0QMW7HtvnoL1dRARLKIbMP3vXwkwziuLesPqWVGIl6Bw==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+
+"@babel/plugin-transform-class-static-block@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.3.tgz#e352cf33567385c731a8f21192efeba760358773"
+ integrity sha512-5BirgNWNOx7cwbTJCOmKFJ1pZjwk5MUfMIwiBBvsirCJMZeQgs5pk6i1OlkVg+1Vef5LfBahFOrdCnAWvkVKMw==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-class-static-block" "^7.14.5"
+
+"@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz#f469d0b07a4c5a7dbb21afad9e27e57b47031665"
integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==
@@ -840,15 +878,15 @@
"@babel/helper-split-export-declaration" "^7.18.6"
globals "^11.1.0"
-"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.18.9":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa"
- integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==
+"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz#3a2d8bb771cd2ef1cd736435f6552fe502e11b44"
+ integrity sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q==
dependencies:
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-plugin-utils" "^7.21.5"
"@babel/template" "^7.20.7"
-"@babel/plugin-transform-destructuring@^7.0.0", "@babel/plugin-transform-destructuring@^7.20.2":
+"@babel/plugin-transform-destructuring@^7.0.0", "@babel/plugin-transform-destructuring@^7.21.3":
version "7.21.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz#73b46d0fd11cd6ef57dea8a381b1215f4959d401"
integrity sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==
@@ -870,6 +908,14 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.9"
+"@babel/plugin-transform-dynamic-import@^7.22.1":
+ version "7.22.1"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.1.tgz#6c56afaf896a07026330cf39714532abed8d9ed1"
+ integrity sha512-rlhWtONnVBPdmt+jeewS0qSnMz/3yLFrqAP8hHC6EDcrYRSyuz9f9yQhHvVn2Ad6+yO9fHXac5piudeYrInxwQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+
"@babel/plugin-transform-exponentiation-operator@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd"
@@ -878,7 +924,15 @@
"@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.16.0", "@babel/plugin-transform-flow-strip-types@^7.18.6":
+"@babel/plugin-transform-export-namespace-from@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.3.tgz#9b8700aa495007d3bebac8358d1c562434b680b9"
+ integrity sha512-5Ti1cHLTDnt3vX61P9KZ5IG09bFXp4cDVFJIAeCZuxu9OXXJJZp5iP0n/rzM2+iAutJY+KWEyyHcRaHlpQ/P5g==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
+
+"@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.16.0", "@babel/plugin-transform-flow-strip-types@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.21.0.tgz#6aeca0adcb81dc627c8986e770bfaa4d9812aff5"
integrity sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w==
@@ -886,12 +940,12 @@
"@babel/helper-plugin-utils" "^7.20.2"
"@babel/plugin-syntax-flow" "^7.18.6"
-"@babel/plugin-transform-for-of@^7.0.0", "@babel/plugin-transform-for-of@^7.18.8":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e"
- integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==
+"@babel/plugin-transform-for-of@^7.0.0", "@babel/plugin-transform-for-of@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz#e890032b535f5a2e237a18535f56a9fdaa7b83fc"
+ integrity sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ==
dependencies:
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-plugin-utils" "^7.21.5"
"@babel/plugin-transform-function-name@^7.0.0", "@babel/plugin-transform-function-name@^7.18.9":
version "7.18.9"
@@ -902,6 +956,14 @@
"@babel/helper-function-name" "^7.18.9"
"@babel/helper-plugin-utils" "^7.18.9"
+"@babel/plugin-transform-json-strings@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.3.tgz#a181b8679cf7c93e9d0e3baa5b1776d65be601a9"
+ integrity sha512-IuvOMdeOOY2X4hRNAT6kwbePtK21BUyrAEgLKviL8pL6AEEVUVcqtRdN/HJXBLGIbt9T3ETmXRnFedRRmQNTYw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-json-strings" "^7.8.3"
+
"@babel/plugin-transform-literals@^7.0.0", "@babel/plugin-transform-literals@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc"
@@ -909,6 +971,14 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.9"
+"@babel/plugin-transform-logical-assignment-operators@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.3.tgz#9e021455810f33b0baccb82fb759b194f5dc36f0"
+ integrity sha512-CbayIfOw4av2v/HYZEsH+Klks3NC2/MFIR3QR8gnpGNNPEaq2fdlVCRYG/paKs7/5hvBLQ+H70pGWOHtlNEWNA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+
"@babel/plugin-transform-member-expression-literals@^7.0.0", "@babel/plugin-transform-member-expression-literals@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e"
@@ -916,7 +986,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-transform-modules-amd@^7.19.6":
+"@babel/plugin-transform-modules-amd@^7.20.11":
version "7.20.11"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz#3daccca8e4cc309f03c3a0c4b41dc4b26f55214a"
integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==
@@ -924,23 +994,23 @@
"@babel/helper-module-transforms" "^7.20.11"
"@babel/helper-plugin-utils" "^7.20.2"
-"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.19.6":
- version "7.21.2"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz#6ff5070e71e3192ef2b7e39820a06fb78e3058e7"
- integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==
+"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.13.8", "@babel/plugin-transform-modules-commonjs@^7.19.6", "@babel/plugin-transform-modules-commonjs@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz#d69fb947eed51af91de82e4708f676864e5e47bc"
+ integrity sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==
dependencies:
- "@babel/helper-module-transforms" "^7.21.2"
- "@babel/helper-plugin-utils" "^7.20.2"
- "@babel/helper-simple-access" "^7.20.2"
+ "@babel/helper-module-transforms" "^7.21.5"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/helper-simple-access" "^7.21.5"
-"@babel/plugin-transform-modules-systemjs@^7.19.6":
- version "7.20.11"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz#467ec6bba6b6a50634eea61c9c232654d8a4696e"
- integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==
+"@babel/plugin-transform-modules-systemjs@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.3.tgz#cc507e03e88d87b016feaeb5dae941e6ef50d91e"
+ integrity sha512-V21W3bKLxO3ZjcBJZ8biSvo5gQ85uIXW2vJfh7JSWf/4SLUSr1tOoHX3ruN4+Oqa2m+BKfsxTR1I+PsvkIWvNw==
dependencies:
"@babel/helper-hoist-variables" "^7.18.6"
- "@babel/helper-module-transforms" "^7.20.11"
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-module-transforms" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
"@babel/helper-validator-identifier" "^7.19.1"
"@babel/plugin-transform-modules-umd@^7.18.6":
@@ -951,20 +1021,36 @@
"@babel/helper-module-transforms" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-transform-named-capturing-groups-regex@^7.0.0", "@babel/plugin-transform-named-capturing-groups-regex@^7.19.1":
- version "7.20.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8"
- integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==
+"@babel/plugin-transform-named-capturing-groups-regex@^7.0.0", "@babel/plugin-transform-named-capturing-groups-regex@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.3.tgz#db6fb77e6b3b53ec3b8d370246f0b7cf67d35ab4"
+ integrity sha512-c6HrD/LpUdNNJsISQZpds3TXvfYIAbo+efE9aWmY/PmSRD0agrJ9cPMt4BmArwUQ7ZymEWTFjTyp+yReLJZh0Q==
dependencies:
- "@babel/helper-create-regexp-features-plugin" "^7.20.5"
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-create-regexp-features-plugin" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
-"@babel/plugin-transform-new-target@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8"
- integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==
+"@babel/plugin-transform-new-target@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.3.tgz#deb0377d741cbee2f45305868b9026dcd6dd96e2"
+ integrity sha512-5RuJdSo89wKdkRTqtM9RVVJzHum9c2s0te9rB7vZC1zKKxcioWIy+xcu4OoIAjyFZhb/bp5KkunuLin1q7Ct+w==
dependencies:
- "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.21.5"
+
+"@babel/plugin-transform-nullish-coalescing-operator@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.3.tgz#8c519f8bf5af94a9ca6f65cf422a9d3396e542b9"
+ integrity sha512-CpaoNp16nX7ROtLONNuCyenYdY/l7ZsR6aoVa7rW7nMWisoNoQNIH5Iay/4LDyRjKMuElMqXiBoOQCDLTMGZiw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+
+"@babel/plugin-transform-numeric-separator@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.3.tgz#02493070ca6685884b0eee705363ee4da2132ab0"
+ integrity sha512-+AF88fPDJrnseMh5vD9+SH6wq4ZMvpiTMHh58uLs+giMEyASFVhcT3NkoyO+NebFCNnpHJEq5AXO2txV4AGPDQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-numeric-separator" "^7.10.4"
"@babel/plugin-transform-object-assign@^7.16.7":
version "7.18.6"
@@ -973,6 +1059,17 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
+"@babel/plugin-transform-object-rest-spread@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.3.tgz#da6fba693effb8c203d8c3bdf7bf4e2567e802e9"
+ integrity sha512-38bzTsqMMCI46/TQnJwPPpy33EjLCc1Gsm2hRTF6zTMWnKsN61vdrpuzIEGQyKEhDSYDKyZHrrd5FMj4gcUHhw==
+ dependencies:
+ "@babel/compat-data" "^7.22.3"
+ "@babel/helper-compilation-targets" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+ "@babel/plugin-transform-parameters" "^7.22.3"
+
"@babel/plugin-transform-object-super@^7.0.0", "@babel/plugin-transform-object-super@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c"
@@ -981,12 +1078,47 @@
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/helper-replace-supers" "^7.18.6"
-"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7":
- version "7.21.3"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz#18fc4e797cf6d6d972cb8c411dbe8a809fa157db"
- integrity sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==
+"@babel/plugin-transform-optional-catch-binding@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.3.tgz#e971a083fc7d209d9cd18253853af1db6d8dc42f"
+ integrity sha512-bnDFWXFzWY0BsOyqaoSXvMQ2F35zutQipugog/rqotL2S4ciFOKlRYUu9djt4iq09oh2/34hqfRR2k1dIvuu4g==
dependencies:
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+
+"@babel/plugin-transform-optional-chaining@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.3.tgz#5fd24a4a7843b76da6aeec23c7f551da5d365290"
+ integrity sha512-63v3/UFFxhPKT8j8u1jTTGVyITxl7/7AfOqK8C5gz1rHURPUGe3y5mvIf68eYKGoBNahtJnTxBKug4BQOnzeJg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0"
+ "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+
+"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.3.tgz#24477acfd2fd2bc901df906c9bf17fbcfeee900d"
+ integrity sha512-x7QHQJHPuD9VmfpzboyGJ5aHEr9r7DsAsdxdhJiTB3J3j8dyl+NFZ+rX5Q2RWFDCs61c06qBfS4ys2QYn8UkMw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.21.5"
+
+"@babel/plugin-transform-private-methods@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.3.tgz#adac38020bab5047482d3297107c1f58e9c574f6"
+ integrity sha512-fC7jtjBPFqhqpPAE+O4LKwnLq7gGkD3ZmC2E3i4qWH34mH3gOg2Xrq5YMHUq6DM30xhqM1DNftiRaSqVjEG+ug==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+
+"@babel/plugin-transform-private-property-in-object@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.3.tgz#031621b02c7b7d95389de1a3dba2fe9e8c548e56"
+ integrity sha512-C7MMl4qWLpgVCbXfj3UW8rR1xeCnisQ0cU7YJHV//8oNBS0aCIVg1vFnZXxOckHhEpQyqNNkWmvSEWnMLlc+Vw==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.18.6"
+ "@babel/helper-create-class-features-plugin" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
"@babel/plugin-transform-property-literals@^7.0.0", "@babel/plugin-transform-property-literals@^7.18.6":
version "7.18.6"
@@ -996,11 +1128,11 @@
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-transform-react-constant-elements@^7.12.1":
- version "7.21.3"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.21.3.tgz#b32a5556100d424b25e388dd689050d78396884d"
- integrity sha512-4DVcFeWe/yDYBLp0kBmOGFJ6N2UYg7coGid1gdxb4co62dy/xISDMaYBXBVXEDhfgMk7qkbcYiGtwd5Q/hwDDQ==
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.22.3.tgz#b87a436c3377f29b37409f9c02c99c9ce377909d"
+ integrity sha512-b5J6muxQYp4H7loAQv/c7GO5cPuRA6H5hx4gO+/Hn+Cu9MRQU0PNiUoWq1L//8sq6kFSNxGXFb2XTaUfa9y+Pg==
dependencies:
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-plugin-utils" "^7.21.5"
"@babel/plugin-transform-react-display-name@^7.0.0", "@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.18.6":
version "7.18.6"
@@ -1030,16 +1162,16 @@
dependencies:
"@babel/helper-plugin-utils" "^7.19.0"
-"@babel/plugin-transform-react-jsx@^7.0.0", "@babel/plugin-transform-react-jsx@^7.12.17", "@babel/plugin-transform-react-jsx@^7.18.6":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz#656b42c2fdea0a6d8762075d58ef9d4e3c4ab8a2"
- integrity sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==
+"@babel/plugin-transform-react-jsx@^7.0.0", "@babel/plugin-transform-react-jsx@^7.12.17", "@babel/plugin-transform-react-jsx@^7.18.6", "@babel/plugin-transform-react-jsx@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.3.tgz#5a1f380df3703ba92eb1a930a539c6d88836f690"
+ integrity sha512-JEulRWG2f04a7L8VWaOngWiK6p+JOSpB+DAtwfJgOaej1qdbNxqtK7MwTBHjUA10NeFcszlFNqCdbRcirzh2uQ==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
- "@babel/helper-module-imports" "^7.18.6"
- "@babel/helper-plugin-utils" "^7.20.2"
- "@babel/plugin-syntax-jsx" "^7.18.6"
- "@babel/types" "^7.21.0"
+ "@babel/helper-module-imports" "^7.21.4"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-jsx" "^7.21.4"
+ "@babel/types" "^7.22.3"
"@babel/plugin-transform-react-pure-annotations@^7.18.6":
version "7.18.6"
@@ -1049,12 +1181,12 @@
"@babel/helper-annotate-as-pure" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-transform-regenerator@^7.18.6":
- version "7.20.5"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d"
- integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==
+"@babel/plugin-transform-regenerator@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz#576c62f9923f94bcb1c855adc53561fd7913724e"
+ integrity sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w==
dependencies:
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-plugin-utils" "^7.21.5"
regenerator-transform "^0.15.1"
"@babel/plugin-transform-reserved-words@^7.18.6":
@@ -1065,15 +1197,15 @@
"@babel/helper-plugin-utils" "^7.18.6"
"@babel/plugin-transform-runtime@^7.0.0", "@babel/plugin-transform-runtime@^7.12.1", "@babel/plugin-transform-runtime@^7.16.4":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.0.tgz#2a884f29556d0a68cd3d152dcc9e6c71dfb6eee8"
- integrity sha512-ReY6pxwSzEU0b3r2/T/VhqMKg/AkceBT19X0UptA3/tYi5Pe2eXgEUH+NNMC5nok6c6XQz5tyVTUpuezRfSMSg==
+ version "7.22.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.4.tgz#f8353f313f18c3ce1315688631ec48657b97af42"
+ integrity sha512-Urkiz1m4zqiRo17klj+l3nXgiRTFQng91Bc1eiLF7BMQu1e7wE5Gcq9xSv062IF068NHjcutSbIMev60gXxAvA==
dependencies:
- "@babel/helper-module-imports" "^7.18.6"
- "@babel/helper-plugin-utils" "^7.20.2"
- babel-plugin-polyfill-corejs2 "^0.3.3"
- babel-plugin-polyfill-corejs3 "^0.6.0"
- babel-plugin-polyfill-regenerator "^0.4.1"
+ "@babel/helper-module-imports" "^7.21.4"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ babel-plugin-polyfill-corejs2 "^0.4.3"
+ babel-plugin-polyfill-corejs3 "^0.8.1"
+ babel-plugin-polyfill-regenerator "^0.5.0"
semver "^6.3.0"
"@babel/plugin-transform-shorthand-properties@^7.0.0", "@babel/plugin-transform-shorthand-properties@^7.18.6":
@@ -1083,7 +1215,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.19.0":
+"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.20.7":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz#c2d83e0b99d3bf83e07b11995ee24bf7ca09401e"
integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==
@@ -1112,22 +1244,30 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.9"
-"@babel/plugin-transform-typescript@^7.21.0", "@babel/plugin-transform-typescript@^7.5.0":
- version "7.21.3"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz#316c5be579856ea890a57ebc5116c5d064658f2b"
- integrity sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==
+"@babel/plugin-transform-typescript@^7.21.3", "@babel/plugin-transform-typescript@^7.5.0":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.3.tgz#8f662cec8ba88c873f1c7663c0c94e3f68592f09"
+ integrity sha512-pyjnCIniO5PNaEuGxT28h0HbMru3qCVrMqVgVOz/krComdIrY9W6FCLBq9NWHY8HDGaUlan+UhmZElDENIfCcw==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
- "@babel/helper-create-class-features-plugin" "^7.21.0"
- "@babel/helper-plugin-utils" "^7.20.2"
- "@babel/plugin-syntax-typescript" "^7.20.0"
+ "@babel/helper-create-class-features-plugin" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/plugin-syntax-typescript" "^7.21.4"
-"@babel/plugin-transform-unicode-escapes@^7.18.10":
- version "7.18.10"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246"
- integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==
+"@babel/plugin-transform-unicode-escapes@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz#1e55ed6195259b0e9061d81f5ef45a9b009fb7f2"
+ integrity sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg==
dependencies:
- "@babel/helper-plugin-utils" "^7.18.9"
+ "@babel/helper-plugin-utils" "^7.21.5"
+
+"@babel/plugin-transform-unicode-property-regex@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.3.tgz#597b6a614dc93eaae605ee293e674d79d32eb380"
+ integrity sha512-5ScJ+OmdX+O6HRuMGW4kv7RL9vIKdtdAj9wuWUKy1wbHY3jaM/UlyIiC1G7J6UJiiyMukjjK0QwL3P0vBd0yYg==
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
"@babel/plugin-transform-unicode-regex@^7.0.0", "@babel/plugin-transform-unicode-regex@^7.18.6":
version "7.18.6"
@@ -1137,38 +1277,34 @@
"@babel/helper-create-regexp-features-plugin" "^7.18.6"
"@babel/helper-plugin-utils" "^7.18.6"
-"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4", "@babel/preset-env@^7.20.0":
- version "7.20.2"
- resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506"
- integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==
+"@babel/plugin-transform-unicode-sets-regex@^7.22.3":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.3.tgz#7c14ee33fa69782b0101d0f7143d3fc73ce00700"
+ integrity sha512-hNufLdkF8vqywRp+P55j4FHXqAX2LRUccoZHH7AFn1pq5ZOO2ISKW9w13bFZVjBoTqeve2HOgoJCcaziJVhGNw==
dependencies:
- "@babel/compat-data" "^7.20.1"
- "@babel/helper-compilation-targets" "^7.20.0"
- "@babel/helper-plugin-utils" "^7.20.2"
- "@babel/helper-validator-option" "^7.18.6"
+ "@babel/helper-create-regexp-features-plugin" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+
+"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4", "@babel/preset-env@^7.20.0":
+ version "7.22.4"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.4.tgz#c86a82630f0e8c61d9bb9327b7b896732028cbed"
+ integrity sha512-c3lHOjbwBv0TkhYCr+XCR6wKcSZ1QbQTVdSkZUaVpLv8CVWotBMArWUi5UAJrcrQaEnleVkkvaV8F/pmc/STZQ==
+ dependencies:
+ "@babel/compat-data" "^7.22.3"
+ "@babel/helper-compilation-targets" "^7.22.1"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/helper-validator-option" "^7.21.0"
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6"
- "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9"
- "@babel/plugin-proposal-async-generator-functions" "^7.20.1"
- "@babel/plugin-proposal-class-properties" "^7.18.6"
- "@babel/plugin-proposal-class-static-block" "^7.18.6"
- "@babel/plugin-proposal-dynamic-import" "^7.18.6"
- "@babel/plugin-proposal-export-namespace-from" "^7.18.9"
- "@babel/plugin-proposal-json-strings" "^7.18.6"
- "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9"
- "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6"
- "@babel/plugin-proposal-numeric-separator" "^7.18.6"
- "@babel/plugin-proposal-object-rest-spread" "^7.20.2"
- "@babel/plugin-proposal-optional-catch-binding" "^7.18.6"
- "@babel/plugin-proposal-optional-chaining" "^7.18.9"
- "@babel/plugin-proposal-private-methods" "^7.18.6"
- "@babel/plugin-proposal-private-property-in-object" "^7.18.6"
- "@babel/plugin-proposal-unicode-property-regex" "^7.18.6"
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.3"
+ "@babel/plugin-proposal-private-property-in-object" "^7.21.0"
"@babel/plugin-syntax-async-generators" "^7.8.4"
"@babel/plugin-syntax-class-properties" "^7.12.13"
"@babel/plugin-syntax-class-static-block" "^7.14.5"
"@babel/plugin-syntax-dynamic-import" "^7.8.3"
"@babel/plugin-syntax-export-namespace-from" "^7.8.3"
"@babel/plugin-syntax-import-assertions" "^7.20.0"
+ "@babel/plugin-syntax-import-attributes" "^7.22.3"
+ "@babel/plugin-syntax-import-meta" "^7.10.4"
"@babel/plugin-syntax-json-strings" "^7.8.3"
"@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
"@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
@@ -1178,54 +1314,71 @@
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
"@babel/plugin-syntax-top-level-await" "^7.14.5"
- "@babel/plugin-transform-arrow-functions" "^7.18.6"
- "@babel/plugin-transform-async-to-generator" "^7.18.6"
+ "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6"
+ "@babel/plugin-transform-arrow-functions" "^7.21.5"
+ "@babel/plugin-transform-async-generator-functions" "^7.22.3"
+ "@babel/plugin-transform-async-to-generator" "^7.20.7"
"@babel/plugin-transform-block-scoped-functions" "^7.18.6"
- "@babel/plugin-transform-block-scoping" "^7.20.2"
- "@babel/plugin-transform-classes" "^7.20.2"
- "@babel/plugin-transform-computed-properties" "^7.18.9"
- "@babel/plugin-transform-destructuring" "^7.20.2"
+ "@babel/plugin-transform-block-scoping" "^7.21.0"
+ "@babel/plugin-transform-class-properties" "^7.22.3"
+ "@babel/plugin-transform-class-static-block" "^7.22.3"
+ "@babel/plugin-transform-classes" "^7.21.0"
+ "@babel/plugin-transform-computed-properties" "^7.21.5"
+ "@babel/plugin-transform-destructuring" "^7.21.3"
"@babel/plugin-transform-dotall-regex" "^7.18.6"
"@babel/plugin-transform-duplicate-keys" "^7.18.9"
+ "@babel/plugin-transform-dynamic-import" "^7.22.1"
"@babel/plugin-transform-exponentiation-operator" "^7.18.6"
- "@babel/plugin-transform-for-of" "^7.18.8"
+ "@babel/plugin-transform-export-namespace-from" "^7.22.3"
+ "@babel/plugin-transform-for-of" "^7.21.5"
"@babel/plugin-transform-function-name" "^7.18.9"
+ "@babel/plugin-transform-json-strings" "^7.22.3"
"@babel/plugin-transform-literals" "^7.18.9"
+ "@babel/plugin-transform-logical-assignment-operators" "^7.22.3"
"@babel/plugin-transform-member-expression-literals" "^7.18.6"
- "@babel/plugin-transform-modules-amd" "^7.19.6"
- "@babel/plugin-transform-modules-commonjs" "^7.19.6"
- "@babel/plugin-transform-modules-systemjs" "^7.19.6"
+ "@babel/plugin-transform-modules-amd" "^7.20.11"
+ "@babel/plugin-transform-modules-commonjs" "^7.21.5"
+ "@babel/plugin-transform-modules-systemjs" "^7.22.3"
"@babel/plugin-transform-modules-umd" "^7.18.6"
- "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1"
- "@babel/plugin-transform-new-target" "^7.18.6"
+ "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.3"
+ "@babel/plugin-transform-new-target" "^7.22.3"
+ "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.3"
+ "@babel/plugin-transform-numeric-separator" "^7.22.3"
+ "@babel/plugin-transform-object-rest-spread" "^7.22.3"
"@babel/plugin-transform-object-super" "^7.18.6"
- "@babel/plugin-transform-parameters" "^7.20.1"
+ "@babel/plugin-transform-optional-catch-binding" "^7.22.3"
+ "@babel/plugin-transform-optional-chaining" "^7.22.3"
+ "@babel/plugin-transform-parameters" "^7.22.3"
+ "@babel/plugin-transform-private-methods" "^7.22.3"
+ "@babel/plugin-transform-private-property-in-object" "^7.22.3"
"@babel/plugin-transform-property-literals" "^7.18.6"
- "@babel/plugin-transform-regenerator" "^7.18.6"
+ "@babel/plugin-transform-regenerator" "^7.21.5"
"@babel/plugin-transform-reserved-words" "^7.18.6"
"@babel/plugin-transform-shorthand-properties" "^7.18.6"
- "@babel/plugin-transform-spread" "^7.19.0"
+ "@babel/plugin-transform-spread" "^7.20.7"
"@babel/plugin-transform-sticky-regex" "^7.18.6"
"@babel/plugin-transform-template-literals" "^7.18.9"
"@babel/plugin-transform-typeof-symbol" "^7.18.9"
- "@babel/plugin-transform-unicode-escapes" "^7.18.10"
+ "@babel/plugin-transform-unicode-escapes" "^7.21.5"
+ "@babel/plugin-transform-unicode-property-regex" "^7.22.3"
"@babel/plugin-transform-unicode-regex" "^7.18.6"
+ "@babel/plugin-transform-unicode-sets-regex" "^7.22.3"
"@babel/preset-modules" "^0.1.5"
- "@babel/types" "^7.20.2"
- babel-plugin-polyfill-corejs2 "^0.3.3"
- babel-plugin-polyfill-corejs3 "^0.6.0"
- babel-plugin-polyfill-regenerator "^0.4.1"
- core-js-compat "^3.25.1"
+ "@babel/types" "^7.22.4"
+ babel-plugin-polyfill-corejs2 "^0.4.3"
+ babel-plugin-polyfill-corejs3 "^0.8.1"
+ babel-plugin-polyfill-regenerator "^0.5.0"
+ core-js-compat "^3.30.2"
semver "^6.3.0"
"@babel/preset-flow@^7.13.13":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.18.6.tgz#83f7602ba566e72a9918beefafef8ef16d2810cb"
- integrity sha512-E7BDhL64W6OUqpuyHnSroLnqyRTcG6ZdOBl1OKI/QK/HJfplqK/S3sq1Cckx7oTodJ5yOXyfw7rEADJ6UjoQDQ==
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.21.4.tgz#a5de2a1cafa61f0e0b3af9b30ff0295d38d3608f"
+ integrity sha512-F24cSq4DIBmhq4OzK3dE63NHagb27OPE3eWR+HLekt4Z3Y5MzIIUGF3LlLgV0gN8vzbDViSY7HnrReNVCJXTeA==
dependencies:
- "@babel/helper-plugin-utils" "^7.18.6"
- "@babel/helper-validator-option" "^7.18.6"
- "@babel/plugin-transform-flow-strip-types" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-validator-option" "^7.21.0"
+ "@babel/plugin-transform-flow-strip-types" "^7.21.0"
"@babel/preset-modules@^0.1.5":
version "0.1.5"
@@ -1239,25 +1392,27 @@
esutils "^2.0.2"
"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.16.0":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.18.6.tgz#979f76d6277048dc19094c217b507f3ad517dd2d"
- integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.22.3.tgz#2ec7f91d0c924fa2ea0c7cfbbf690bc62b79cd84"
+ integrity sha512-lxDz1mnZ9polqClBCVBjIVUypoB4qV3/tZUDb/IlYbW1kiiLaXaX+bInbRjl+lNQ/iUZraQ3+S8daEmoELMWug==
dependencies:
- "@babel/helper-plugin-utils" "^7.18.6"
- "@babel/helper-validator-option" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.21.5"
+ "@babel/helper-validator-option" "^7.21.0"
"@babel/plugin-transform-react-display-name" "^7.18.6"
- "@babel/plugin-transform-react-jsx" "^7.18.6"
+ "@babel/plugin-transform-react-jsx" "^7.22.3"
"@babel/plugin-transform-react-jsx-development" "^7.18.6"
"@babel/plugin-transform-react-pure-annotations" "^7.18.6"
-"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.0", "@babel/preset-typescript@^7.16.7":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.0.tgz#bcbbca513e8213691fe5d4b23d9251e01f00ebff"
- integrity sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg==
+"@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.16.0", "@babel/preset-typescript@^7.16.7", "@babel/preset-typescript@^7.17.12":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.21.5.tgz#68292c884b0e26070b4d66b202072d391358395f"
+ integrity sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==
dependencies:
- "@babel/helper-plugin-utils" "^7.20.2"
+ "@babel/helper-plugin-utils" "^7.21.5"
"@babel/helper-validator-option" "^7.21.0"
- "@babel/plugin-transform-typescript" "^7.21.0"
+ "@babel/plugin-syntax-jsx" "^7.21.4"
+ "@babel/plugin-transform-modules-commonjs" "^7.21.5"
+ "@babel/plugin-transform-typescript" "^7.21.3"
"@babel/register@^7.13.16":
version "7.21.0"
@@ -1275,44 +1430,44 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4":
- version "7.21.0"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
- integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb"
+ integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==
dependencies:
regenerator-runtime "^0.13.11"
-"@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
- integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==
+"@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.21.9", "@babel/template@^7.3.3":
+ version "7.21.9"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.21.9.tgz#bf8dad2859130ae46088a99c1f265394877446fb"
+ integrity sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ==
dependencies:
- "@babel/code-frame" "^7.18.6"
- "@babel/parser" "^7.20.7"
- "@babel/types" "^7.20.7"
+ "@babel/code-frame" "^7.21.4"
+ "@babel/parser" "^7.21.9"
+ "@babel/types" "^7.21.5"
-"@babel/traverse@^7.20.0", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.3", "@babel/traverse@^7.7.2", "@babel/traverse@^7.7.4":
- version "7.21.3"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.3.tgz#4747c5e7903d224be71f90788b06798331896f67"
- integrity sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==
+"@babel/traverse@^7.20.0", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.22.1", "@babel/traverse@^7.7.2", "@babel/traverse@^7.7.4":
+ version "7.22.4"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.4.tgz#c3cf96c5c290bd13b55e29d025274057727664c0"
+ integrity sha512-Tn1pDsjIcI+JcLKq1AVlZEr4226gpuAQTsLMorsYg9tuS/kG7nuwwJ4AB8jfQuEgb/COBwR/DqJxmoiYFu5/rQ==
dependencies:
- "@babel/code-frame" "^7.18.6"
- "@babel/generator" "^7.21.3"
- "@babel/helper-environment-visitor" "^7.18.9"
+ "@babel/code-frame" "^7.21.4"
+ "@babel/generator" "^7.22.3"
+ "@babel/helper-environment-visitor" "^7.22.1"
"@babel/helper-function-name" "^7.21.0"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
- "@babel/parser" "^7.21.3"
- "@babel/types" "^7.21.3"
+ "@babel/parser" "^7.22.4"
+ "@babel/types" "^7.22.4"
debug "^4.1.0"
globals "^11.1.0"
-"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.3", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
- version "7.21.3"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.3.tgz#4865a5357ce40f64e3400b0f3b737dc6d4f64d05"
- integrity sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==
+"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.22.0", "@babel/types@^7.22.3", "@babel/types@^7.22.4", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
+ version "7.22.4"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.4.tgz#56a2653ae7e7591365dabf20b76295410684c071"
+ integrity sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==
dependencies:
- "@babel/helper-string-parser" "^7.19.4"
+ "@babel/helper-string-parser" "^7.21.5"
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
@@ -1331,6 +1486,36 @@
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f"
integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==
+"@cbor-extract/cbor-extract-darwin-arm64@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz#5721f6dd3feae0b96d23122853ce977e0671b7a6"
+ integrity sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==
+
+"@cbor-extract/cbor-extract-darwin-x64@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz#c25e7d0133950d87d101d7b3afafea8d50d83f5f"
+ integrity sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw==
+
+"@cbor-extract/cbor-extract-linux-arm64@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz#48f78e7d8f0fcc84ed074b6bfa6d15dd83187c63"
+ integrity sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ==
+
+"@cbor-extract/cbor-extract-linux-arm@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz#7507d346389cb682e44fab8fae9534edd52e2e41"
+ integrity sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ==
+
+"@cbor-extract/cbor-extract-linux-x64@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz#b7c1d2be61c58ec18d58afbad52411ded63cd4cd"
+ integrity sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA==
+
+"@cbor-extract/cbor-extract-win32-x64@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7"
+ integrity sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw==
+
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
@@ -1445,11 +1630,24 @@
integrity sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==
"@csstools/selector-specificity@^2.0.0", "@csstools/selector-specificity@^2.0.2":
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz#c9c61d9fe5ca5ac664e1153bb0aa0eba1c6d6308"
- integrity sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016"
+ integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==
-"@did-plc/lib@*", "@did-plc/lib@^0.0.1":
+"@did-plc/lib@*":
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/@did-plc/lib/-/lib-0.0.4.tgz#be5400dc9464ec3088294bd089631e8a8aa98215"
+ integrity sha512-Omeawq3b8G/c/5CtkTtzovSOnWuvIuCI4GTJNrt1AmCskwEQV7zbX5d6km1mjJNbE0gHuQPTVqZxLVqetNbfwA==
+ dependencies:
+ "@atproto/common" "0.1.1"
+ "@atproto/crypto" "0.1.0"
+ "@ipld/dag-cbor" "^7.0.3"
+ axios "^1.3.4"
+ multiformats "^9.6.4"
+ uint8arrays "3.0.0"
+ zod "^3.14.2"
+
+"@did-plc/lib@^0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@did-plc/lib/-/lib-0.0.1.tgz#5fd78c71901168ac05c5650af3a376c76461991c"
integrity sha512-RkY5w9DbYMco3SjeepqIiMveqz35exjlVDipCs2gz9AXF4/cp9hvmrp9zUWEw2vny+FjV8vGEN7QpaXWaO6nhg==
@@ -1494,25 +1692,25 @@
"@types/hammerjs" "^2.0.36"
"@eslint-community/eslint-utils@^4.2.0":
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz#a556790523a351b4e47e9d385f47265eaaf9780a"
- integrity sha512-v3oplH6FYCULtFuCeqyuTd9D2WKO937Dxdq+GmHOLL72TTRriLxz2VLlNfkZRsvj6PKnOPAtuT6dwrs/pA5DvA==
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
+ integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
dependencies:
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.4.0":
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403"
- integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==
+ version "4.5.1"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884"
+ integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==
-"@eslint/eslintrc@^2.0.1":
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.1.tgz#7888fe7ec8f21bc26d646dbd2c11cd776e21192d"
- integrity sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==
+"@eslint/eslintrc@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331"
+ integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
- espree "^9.5.0"
+ espree "^9.5.2"
globals "^13.19.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
@@ -1520,10 +1718,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
-"@eslint/js@8.36.0":
- version "8.36.0"
- resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe"
- integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==
+"@eslint/js@8.41.0":
+ version "8.41.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3"
+ integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==
"@expo/bunyan@4.0.0", "@expo/bunyan@^4.0.0":
version "4.0.0"
@@ -1535,10 +1733,10 @@
mv "~2"
safe-json-stringify "~1"
-"@expo/cli@0.7.0":
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.7.0.tgz#2a16873ced05c1f3b7f3990d7b410e9853600f45"
- integrity sha512-9gjr3pRgwWzUDW/P7B4tA0QevKb+hCrvTmVc3Ce5w7CjdM3zNoBcro8vwviRHqkiB1IifG7zQh0PPStSbK+FRQ==
+"@expo/cli@0.7.3":
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.7.3.tgz#8d61490f4961d40c38af72b7184e3c7cab70773e"
+ integrity sha512-uMGHbAhApqXR2sd1KPhgvpbOhBBnspad8msEqHleT2PHXwKIwTUDzBGO9+jdOAWwCx2MJfw3+asYjzoD3DN9Bg==
dependencies:
"@babel/runtime" "^7.20.0"
"@expo/code-signing-certificates" "0.0.5"
@@ -1551,7 +1749,7 @@
"@expo/osascript" "^2.0.31"
"@expo/package-manager" "~1.0.0"
"@expo/plist" "^0.0.20"
- "@expo/prebuild-config" "6.0.0"
+ "@expo/prebuild-config" "6.0.1"
"@expo/rudder-sdk-node" "1.1.1"
"@expo/spawn-async" "1.5.0"
"@expo/xcpretty" "^4.2.1"
@@ -1610,14 +1808,14 @@
node-forge "^1.2.1"
nullthrows "^1.1.1"
-"@expo/config-plugins@4.1.1":
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-4.1.1.tgz#ffb20b3d2be4e2509e8bf846721641dc072718c7"
- integrity sha512-lo3tVxRhwM9jfxPHJcURsH5WvU26kX12h5EB3C7kjVhgdQPLkvT8Jk8Cx0KSL8MXKcry2xQvZ2uuwWLkMeplJw==
+"@expo/config-plugins@6.0.2":
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-6.0.2.tgz#cf07319515022ba94d9aa9fa30e0cff43a14256f"
+ integrity sha512-Cn01fXMHwjU042EgO9oO3Mna0o/UCrW91MQLMbJa4pXM41CYGjNgVy1EVXiuRRx/upegHhvltBw5D+JaUm8aZQ==
dependencies:
- "@expo/config-types" "^44.0.0"
- "@expo/json-file" "8.2.35"
- "@expo/plist" "0.0.18"
+ "@expo/config-types" "^48.0.0"
+ "@expo/json-file" "~8.2.37"
+ "@expo/plist" "^0.0.20"
"@expo/sdk-runtime-versions" "^1.0.0"
"@react-native/normalize-color" "^2.0.0"
chalk "^4.1.2"
@@ -1631,7 +1829,7 @@
xcode "^3.0.1"
xml2js "0.4.23"
-"@expo/config-plugins@6.0.1", "@expo/config-plugins@~6.0.0":
+"@expo/config-plugins@~6.0.0":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-6.0.1.tgz#827cb34c51f725d8825b0768df6550c1cf81d457"
integrity sha512-6mqZutxeibXFeqFfoZApFUEH2n1RxGXYMHCdJrDj4eXDBBFZ3aJ0XBoroZcHHHvfRieEsf54vNyJoWp7JZGj8g==
@@ -1652,33 +1850,11 @@
xcode "^3.0.1"
xml2js "0.4.23"
-"@expo/config-types@^44.0.0":
- version "44.0.0"
- resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-44.0.0.tgz#d3480fe2c99f9e895dae4ebba58b74ed72d03e26"
- integrity sha512-d+gpdKOAhqaD5RmcMzGgKzNtvE1w+GCqpFQNSXLliYlXjj+Tv0eL8EPeAdPtvke0vowpPFwd5McXLA90dgY6Jg==
-
"@expo/config-types@^48.0.0":
version "48.0.0"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-48.0.0.tgz#15a46921565ffeda3c3ba010701398f05193d5b3"
integrity sha512-DwyV4jTy/+cLzXGAo1xftS6mVlSiLIWZjl9DjTCLPFVgNYQxnh7htPilRv4rBhiNs7KaznWqKU70+4zQoKVT9A==
-"@expo/config@6.0.20":
- version "6.0.20"
- resolved "https://registry.yarnpkg.com/@expo/config/-/config-6.0.20.tgz#fb3b9fbcaf97f678714fe05603ba5d3aacfa0a25"
- integrity sha512-m2T1/hB4TyLkQElOUwOajn/7gBcPaGyfVwoVsuJMEh0yrNvNFtXP+nl87Cm53g5q+VyfwJUgbewPQ3j/UXkI6Q==
- dependencies:
- "@babel/code-frame" "~7.10.4"
- "@expo/config-plugins" "4.1.1"
- "@expo/config-types" "^44.0.0"
- "@expo/json-file" "8.2.35"
- 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.0.2", "@expo/config@~8.0.0":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-8.0.2.tgz#53ecfa9bafc97b990ff9e34e210205b0e3f05751"
@@ -1751,6 +1927,11 @@
tmp "^0.0.33"
tslib "^2.4.0"
+"@expo/html-elements@^0.4.2":
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/@expo/html-elements/-/html-elements-0.4.2.tgz#4e9f6b9250af8d0befe3242fd42704cca2421e0e"
+ integrity sha512-lNioCgdtOrCMMqzHY+PCTdyuWBTU4yMBlOzPSkS4YFIWt9bq0zexM2ZJkpybTXmowNdE3zHO93xxAmiA2yDi2w==
+
"@expo/image-utils@0.3.22":
version "0.3.22"
resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.22.tgz#3a45fb2e268d20fcc761c87bca3aca7fd8e24260"
@@ -1785,15 +1966,6 @@
semver "7.3.2"
tempy "0.3.0"
-"@expo/json-file@8.2.35":
- version "8.2.35"
- resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.35.tgz#d0f0c74cdde2ae26686708912cf23ff81a8d67ac"
- integrity sha512-cQFLGSNRRFbN9EIhVDpMCYuzXbrHUOmKEqitBR+nrU6surjKGsOsN9Ubyn/L/LAGlFvT293E4XY5zsOtJyiPZQ==
- dependencies:
- "@babel/code-frame" "~7.10.4"
- json5 "^1.0.1"
- write-file-atomic "^2.3.0"
-
"@expo/json-file@^8.2.37", "@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"
@@ -1841,15 +2013,6 @@
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.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.20.tgz#a6b3124438031c02b762bad5a47b70584d3c0072"
@@ -1859,10 +2022,10 @@
base64-js "^1.2.3"
xmlbuilder "^14.0.0"
-"@expo/prebuild-config@6.0.0":
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-6.0.0.tgz#c8e7f634f3ecf2272673f371c47d5d22950129a4"
- integrity sha512-UW0QKAoRelsalVMhAG1tmegwS+2tbefvUi6/0QiKPlMLg8GFDQ5ZnzsSmuljD0SzT5yGg8oSpKYhnrXJ6pRmIQ==
+"@expo/prebuild-config@6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-6.0.1.tgz#e3a5bbf5892859e71ac6a2408b1cc8ba6ca3f58f"
+ integrity sha512-WK3FDht1tdXZGCvtG5s7HSwzhsc7Tyu2DdqV9jVUsLtGD42oqUepk13mEWlU9LOTBgLsoEueKjoSK4EXOXFctw==
dependencies:
"@expo/config" "~8.0.0"
"@expo/config-plugins" "~6.0.0"
@@ -1900,7 +2063,7 @@
dependencies:
cross-spawn "^6.0.5"
-"@expo/spawn-async@^1.5.0":
+"@expo/spawn-async@^1.5.0", "@expo/spawn-async@^1.7.0":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.7.2.tgz#fcfe66c3e387245e72154b1a7eae8cada6a47f58"
integrity sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==
@@ -1913,19 +2076,18 @@
integrity sha512-TI+l71+5aSKnShYclFa14Kum+hQMZ86b95SH6tQUG3qZEmLTarvWpKwqtTwQKqvlJSJrpFiSFu3eCuZokY6zWA==
"@expo/webpack-config@^18.0.1":
- version "18.0.1"
- resolved "https://registry.yarnpkg.com/@expo/webpack-config/-/webpack-config-18.0.1.tgz#e657ae4490052a9ada6bf703cfd721324a5be741"
- integrity sha512-0C+wjmmQ0usySdhtzeRp0yYuf9zkUZ/kNgA6AHQ9N7eG4JIr0DM1c87g119smxcJTbd8N+//mv5znPxSJqBqmg==
+ version "18.1.0"
+ resolved "https://registry.yarnpkg.com/@expo/webpack-config/-/webpack-config-18.1.0.tgz#61f767531053afe14fd301bb6d88b3ca259665ff"
+ integrity sha512-P2P5MjbcIqSlepr8216eIy+rI8UK+K10r/3Y+eoV/pNABKXc/bjk/QSJICLayouxQSOp2YU6GipdfnwJRUsEUA==
dependencies:
- "@babel/core" "^7.16.0"
- "@expo/config" "6.0.20"
- babel-loader "^8.2.3"
+ "@babel/core" "^7.20.2"
+ babel-loader "^8.3.0"
chalk "^4.0.0"
clean-webpack-plugin "^4.0.0"
copy-webpack-plugin "^10.2.0"
css-loader "^6.5.1"
css-minimizer-webpack-plugin "^3.4.1"
- expo-pwa "0.0.124"
+ expo-pwa "0.0.125"
find-up "^5.0.0"
find-yarn-workspace-root "~2.0.0"
getenv "^1.0.0"
@@ -1951,31 +2113,31 @@
find-up "^5.0.0"
js-yaml "^4.1.0"
-"@fortawesome/fontawesome-common-types@6.3.0":
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz#51f734e64511dbc3674cd347044d02f4dd26e86b"
- integrity sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg==
+"@fortawesome/fontawesome-common-types@6.4.0":
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz#88da2b70d6ca18aaa6ed3687832e11f39e80624b"
+ integrity sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==
"@fortawesome/fontawesome-svg-core@^6.1.1":
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.3.0.tgz#b6a17d48d231ac1fad93e43fca7271676bf316cf"
- integrity sha512-uz9YifyKlixV6AcKlOX8WNdtF7l6nakGyLYxYaCa823bEBqyj/U2ssqtctO38itNEwXb8/lMzjdoJ+aaJuOdrw==
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz#3727552eff9179506e9203d72feb5b1063c11a21"
+ integrity sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==
dependencies:
- "@fortawesome/fontawesome-common-types" "6.3.0"
+ "@fortawesome/fontawesome-common-types" "6.4.0"
"@fortawesome/free-regular-svg-icons@^6.1.1":
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.3.0.tgz#286f87f777e6c96af59151e86647c81083029ee2"
- integrity sha512-cZnwiVHZ51SVzWHOaNCIA+u9wevZjCuAGSvSYpNlm6A4H4Vhwh8481Bf/5rwheIC3fFKlgXxLKaw8Xeroz8Ntg==
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz#cacc53bd8d832d46feead412d9ea9ce80a55e13a"
+ integrity sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==
dependencies:
- "@fortawesome/fontawesome-common-types" "6.3.0"
+ "@fortawesome/fontawesome-common-types" "6.4.0"
"@fortawesome/free-solid-svg-icons@^6.1.1":
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.3.0.tgz#d3bd33ae18bb15fdfc3ca136e2fea05f32768a65"
- integrity sha512-x5tMwzF2lTH8pyv8yeZRodItP2IVlzzmBuD1M7BjawWgg9XAvktqJJ91Qjgoaf8qJpHQ8FEU9VxRfOkLhh86QA==
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz#48c0e790847fa56299e2f26b82b39663b8ad7119"
+ integrity sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==
dependencies:
- "@fortawesome/fontawesome-common-types" "6.3.0"
+ "@fortawesome/fontawesome-common-types" "6.4.0"
"@fortawesome/react-native-fontawesome@^0.3.0":
version "0.3.0"
@@ -1990,10 +2152,10 @@
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
-"@gorhom/bottom-sheet@^4":
- version "4.4.5"
- resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-4.4.5.tgz#b9041b01ce1af9a936e7c0fc1d78f026d759eebe"
- integrity sha512-Z5Z20wshLUB8lIdtMKoJaRnjd64wBR/q8EeVPThrg+skrcBwBPHfUwZJ2srB0rEszA/01ejSJy/ixyd7Ra7vUA==
+"@gorhom/bottom-sheet@^4.4.7":
+ version "4.4.7"
+ resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-4.4.7.tgz#fc80b3f0b7ebab056ce226f3aa3a89b2db8660dd"
+ integrity sha512-ukTuTqDQi2heo68hAJsBpUQeEkdqP9REBcn47OpuvPKhdPuO1RBOOADjqXJNCnZZRcY+HqbnGPMSLFVc31zylQ==
dependencies:
"@gorhom/portal" "1.0.14"
invariant "^2.2.4"
@@ -2006,9 +2168,9 @@
nanoid "^3.3.1"
"@graphql-typed-document-node/core@^3.1.0":
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.2.tgz#6fc464307cbe3c8ca5064549b806360d84457b04"
- integrity sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA==
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
+ integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
"@hapi/hoek@^9.0.0":
version "9.3.0"
@@ -2494,46 +2656,48 @@
"@types/yargs" "^17.0.8"
chalk "^4.0.0"
-"@jridgewell/gen-mapping@^0.1.0":
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
- integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==
- dependencies:
- "@jridgewell/set-array" "^1.0.0"
- "@jridgewell/sourcemap-codec" "^1.4.10"
-
"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
- integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
+ integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
dependencies:
"@jridgewell/set-array" "^1.0.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
-"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3":
+"@jridgewell/resolve-uri@3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
-"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
+"@jridgewell/resolve-uri@^3.0.3":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
+ integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
+
+"@jridgewell/set-array@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
"@jridgewell/source-map@^0.3.2":
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
- integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda"
+ integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==
dependencies:
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
-"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
+"@jridgewell/sourcemap-codec@1.4.14":
version "1.4.14"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+"@jridgewell/sourcemap-codec@^1.4.10":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
@@ -2543,9 +2707,9 @@
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
- version "0.3.17"
- resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
- integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==
+ version "0.3.18"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6"
+ integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==
dependencies:
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
@@ -2562,10 +2726,45 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
-"@linaria/core@3.0.0-beta.13":
- version "3.0.0-beta.13"
- resolved "https://registry.yarnpkg.com/@linaria/core/-/core-3.0.0-beta.13.tgz#049c5be5faa67e341e413a0f6b641d5d78d91056"
- integrity sha512-3zEi5plBCOsEzUneRVuQb+2SAx3qaC1dj0FfFAI6zIJQoDWu0dlSwKijMRack7oO9tUWrchfj3OkKQAd1LBdVg==
+"@linaria/core@4.2.9":
+ version "4.2.9"
+ resolved "https://registry.yarnpkg.com/@linaria/core/-/core-4.2.9.tgz#4917bde18d064a29cff4fd86aa99621f953a2a2c"
+ integrity sha512-ELcu37VNVOT/PU0L6WDIN+aLzNFyJrqoBYT0CucGOCAmODbojUMCv8oJYRbWzA3N34w1t199dN4UFdfRWFG2rg==
+ dependencies:
+ "@linaria/logger" "^4.0.0"
+ "@linaria/tags" "^4.3.4"
+ "@linaria/utils" "^4.3.3"
+
+"@linaria/logger@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/@linaria/logger/-/logger-4.0.0.tgz#6f73eb3cc11d548967a7caf2e7997439e46fca0d"
+ integrity sha512-YnBq0JlDWMEkTOK+tMo5yEVR0f5V//6qMLToGcLhTyM9g9i+IDFn51Z+5q2hLk7RdG4NBPgbcCXYi2w4RKsPeg==
+ dependencies:
+ debug "^4.1.1"
+ picocolors "^1.0.0"
+
+"@linaria/tags@^4.3.4":
+ version "4.3.5"
+ resolved "https://registry.yarnpkg.com/@linaria/tags/-/tags-4.3.5.tgz#bf2e070d11179addf2f27a66cd29d8192e71ca89"
+ integrity sha512-PgaIi8Vv89YOjc6rpKL/uPg2w4k0rAwAYxcqeXqzKqsEAste5rgB8xp1/KUOG0oAOkPd3MRL6Duj+m0ZwJ3g+g==
+ dependencies:
+ "@babel/generator" "^7.20.4"
+ "@linaria/logger" "^4.0.0"
+ "@linaria/utils" "^4.3.4"
+
+"@linaria/utils@^4.3.3", "@linaria/utils@^4.3.4":
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/@linaria/utils/-/utils-4.3.4.tgz#860db9131e498b62510e49dc6fd4a8f0ed44bf4d"
+ integrity sha512-vt6WJG54n+KANaqxOfzIIU7aSfFHEWFbnGLsgxL7nASHqO0zezrNA2y2Rrp80zSeTW+wSpbmDM4uJyC9UW1qoA==
+ dependencies:
+ "@babel/core" "^7.20.2"
+ "@babel/plugin-proposal-export-namespace-from" "^7.18.9"
+ "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+ "@babel/plugin-transform-modules-commonjs" "^7.19.6"
+ "@babel/traverse" "^7.20.1"
+ "@babel/types" "^7.20.2"
+ "@linaria/logger" "^4.0.0"
+ babel-merge "^3.0.0"
"@lukeed/csprng@^1.1.0":
version "1.1.0"
@@ -2625,9 +2824,9 @@
fastq "^1.6.0"
"@notifee/react-native@^7.4.0":
- version "7.6.1"
- resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.6.1.tgz#e215428787396ec57ea424106cc88666f7efe70d"
- integrity sha512-OjhLPODh6FICYZmF9/0UZbcl2JPaPpcrWi1Cvs/OLFbPSJTIEwPZgXFrCHv/cA3wUX4YQCXreSqQGSVQgvNItQ==
+ version "7.7.1"
+ resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.7.1.tgz#ce3f982fb7354519406cb7716f8e861bab0056ce"
+ integrity sha512-E+W91ulI4dxdIrhK6YCyjWqXgrUsVNZYYCSn3gDADmveuR2Yd2uGvbbSW2vUIFU4N4gQQT/5HJdk9Jk83KHbVA==
"@npmcli/fs@^1.0.0":
version "1.1.1"
@@ -2661,21 +2860,21 @@
source-map "^0.7.3"
"@popperjs/core@^2.9.0":
- version "2.11.6"
- resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
- integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
+ version "2.11.8"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
+ integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@react-native-async-storage/async-storage@^1.15.15", "@react-native-async-storage/async-storage@^1.17.6":
- version "1.17.12"
- resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.17.12.tgz#a39e4df5b06795ce49b2ca5b7ca9b8faadf8e621"
- integrity sha512-BXg4OxFdjPTRt+8MvN6jz4muq0/2zII3s7HeT/11e4Zeh3WCgk/BleLzUcDfVqF3OzFHUqEkSrb76d6Ndjd/Nw==
+ version "1.18.1"
+ resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.18.1.tgz#b1aea4f07fb1dba3325b857b770671517ddab221"
+ integrity sha512-70aFW8fVCKl+oA1AKPFDpE6s4t9pulj2QeLX+MabEmzfT3urd/3cckv45WJvtocdoIH/oXA3Y+YcCRJCcNa8mA==
dependencies:
merge-options "^3.0.4"
"@react-native-camera-roll/camera-roll@^5.2.2":
- version "5.3.1"
- resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.3.1.tgz#0b6d363c0f6c83fc93ff033826f8fa96274a01a7"
- integrity sha512-2XKMkb/pLBC6vYkNh+bJ4UEj49V2ZSyWFHmaxsUJU9beLo1QbM3XJnySV6F1uv7aC+I2RBlDuAusCqNiTQiCOw==
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.5.0.tgz#3f7cc64e90e923eea8d956b09dd9a15ad0303670"
+ integrity sha512-oKj4J01auootXOAjh3CBJ+wYWxy536Ys1Ej0WzRmn/EbeDHiMQ3kAcAGvy0zsQkO4VeFXNISwCGfTQe3fTBh1w==
"@react-native-clipboard/clipboard@^1.10.0":
version "1.11.2"
@@ -2683,9 +2882,9 @@
integrity sha512-bHyZVW62TuleiZsXNHS1Pv16fWc0fh8O9WvBzl4h2fykqZRW9a+Pv/RGTH56E3X2PqzHP38K5go8zmCZUoIsoQ==
"@react-native-community/blur@^4.3.0":
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.0.tgz#e5018b3b0bd6de9632ac6cf34e9f8e0f1a9a28ec"
- integrity sha512-d6phh39kKcbZ4IluDftiVWqfeFOgjl1AbQWzN47x+hLKQ5GvQJ6QhRvgAuDZ+xbJksrbXgNpMjVYkjsbcVehxg==
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.2.tgz#185a2c7dd03ba168cc95069bc4742e9505fd6c6c"
+ integrity sha512-0ID+pyZKdC4RdgC7HePxUQ6JmsbNrgz03u+6SgqYpmBoK/rE+7JffqIw7IEsfoKitLEcRNLGekIBsfwCqiEkew==
"@react-native-community/cli-clean@^10.1.1":
version "10.1.1"
@@ -2849,6 +3048,13 @@
prompts "^2.4.0"
semver "^6.3.0"
+"@react-native-community/datetimepicker@6.7.3":
+ version "6.7.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/datetimepicker/-/datetimepicker-6.7.3.tgz#e6d75a42729265d8404d1d668c86926564abca2f"
+ integrity sha512-fXWbEdHMLW/e8cts3snEsbOTbnFXfUHeO2pkiDFX3fWpFoDtUrRWvn50xbY13IJUUKHDhoJ+mj24nMRVIXfX1A==
+ dependencies:
+ invariant "^2.2.4"
+
"@react-native-community/eslint-config@^3.0.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@react-native-community/eslint-config/-/eslint-config-3.2.0.tgz#42f677d5fff385bccf1be1d3b8faa8c086cf998d"
@@ -2948,40 +3154,40 @@
dependencies:
nanoid "^3.1.23"
-"@remirror/core-constants@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.0.tgz#a52f89059d93955e00810023cc76b4f7db9650bf"
- integrity sha512-vpePPMecHJllBqCWXl6+FIcZqS+tRUM2kSCCKFeEo1H3XUEv3ocijBIPhnlSAa7g6maX+12ATTgxrOsLpWVr2g==
- dependencies:
- "@babel/runtime" "^7.13.10"
-
-"@remirror/core-helpers@^2.0.1":
+"@remirror/core-constants@^2.0.1":
version "2.0.1"
- resolved "https://registry.yarnpkg.com/@remirror/core-helpers/-/core-helpers-2.0.1.tgz#6847666a009ada8c9b9f3a093c13a6d07a95d9bb"
- integrity sha512-s8M1pn33aBUhduvD1QR02uUQMegnFkGaTr4c1iBzxTTyg0rbQstzuQ7Q8TkL6n64JtgCdJS9jLz2dONb2meBKQ==
+ resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.1.tgz#19b4ae221880762cd98452f44288fcc66baaec0f"
+ integrity sha512-ZR4aihtnnT9lMbhh5DEbsriJRlukRXmLZe7HmM+6ufJNNUDoazc75UX26xbgQlNUqgAqMcUdGFAnPc1JwgAdLQ==
dependencies:
- "@babel/runtime" "^7.13.10"
- "@linaria/core" "3.0.0-beta.13"
- "@remirror/core-constants" "^2.0.0"
- "@remirror/types" "^1.0.0"
+ "@babel/runtime" "^7.21.0"
+
+"@remirror/core-helpers@^2.0.2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@remirror/core-helpers/-/core-helpers-2.0.3.tgz#fa4a0224a612016b9f16052ed0c5d817c69daa39"
+ integrity sha512-LqIPF4stGG69l9qu/FFicv9d9B+YaItzgDMC5A0CEvDQfKkGD3BfabLmfpnuWbsc06oKGdTduilgWcALLZoYLg==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+ "@linaria/core" "4.2.9"
+ "@remirror/core-constants" "^2.0.1"
+ "@remirror/types" "^1.0.1"
"@types/object.omit" "^3.0.0"
- "@types/object.pick" "^1.3.1"
+ "@types/object.pick" "^1.3.2"
"@types/throttle-debounce" "^2.1.0"
case-anything "^2.1.10"
dash-get "^1.0.2"
- deepmerge "^4.2.2"
+ deepmerge "^4.3.1"
fast-deep-equal "^3.1.3"
make-error "^1.3.6"
object.omit "^3.0.0"
object.pick "^1.3.0"
throttle-debounce "^3.0.1"
-"@remirror/types@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@remirror/types/-/types-1.0.0.tgz#cc8764440089a2ada71f149c409739575b73b12e"
- integrity sha512-7HQbW7k8VxrAtfzs9FxwO6XSDabn8tSFDi1wwzShOnU+cvaYpfxu0ygyTk3TpXsag1hgFKY3ZIlAfB4WVz2LkQ==
+"@remirror/types@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@remirror/types/-/types-1.0.1.tgz#768502497a0fbbc23338a1586b893f729310cf70"
+ integrity sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==
dependencies:
- type-fest "^2.0.0"
+ type-fest "^2.19.0"
"@rollup/plugin-babel@^5.2.0":
version "5.3.1"
@@ -3021,26 +3227,26 @@
picomatch "^2.2.2"
"@rushstack/eslint-patch@^1.1.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
- integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.0.tgz#f5635b36fc0dad96ef1e542a302cd914230188c0"
+ integrity sha512-IthPJsJR85GhOkp3Hvp8zFOPK5ynKn6STyHa/WZpioK7E1aYDiBzpqQPrngc14DszIUkIrdd3k9Iu0XSzlP/1w==
-"@segment/analytics-core@1.2.3":
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/@segment/analytics-core/-/analytics-core-1.2.3.tgz#729c5b72d6d940341ea8cba9d3ff3eef39baef7a"
- integrity sha512-/B4f4Hxmwd9WpEba/ChYkUwhILz5cPhG4Sto03IlLc8vbV7gAOCGH021EKvU3Wv70WlRK6EgJkuDLPnRl2a2aA==
+"@segment/analytics-core@1.2.5":
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/@segment/analytics-core/-/analytics-core-1.2.5.tgz#bd17034be9393aa245a684f137a7950debee240b"
+ integrity sha512-T+AyZe4eKAO08T138RcvCTqUCT609H07VQL6+kh58VJiIalowrnqSdWf9rc+AsUMvPnxUOsnyGBOqNdZ5KNqYw==
dependencies:
"@lukeed/uuid" "^2.0.0"
dset "^3.1.2"
tslib "^2.4.1"
"@segment/analytics-next@^1.51.3":
- version "1.51.3"
- resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.51.3.tgz#4720691d2bac43bb8390d4a7a15881e52dc9529f"
- integrity sha512-c22GDz6rrhliIsgtLQjEcRiZdqb70+0hEyfTI6YpRXZzEXBwdJybO5ZCD7NRlVFHf/qXp1qcjHuQ5xyOGr2lJg==
+ version "1.51.7"
+ resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.51.7.tgz#f0d3c9b06b2ff5e46802a0ec8e9f676b41bd3658"
+ integrity sha512-WXrgQPkB4MBa4Op4X1SKZL/WEBX3vM04jChHClXZK3QcMAezy4A0vblcHK9vtm8RGOuiickVuemU7ZN3/iaGPA==
dependencies:
"@lukeed/uuid" "^2.0.0"
- "@segment/analytics-core" "1.2.3"
+ "@segment/analytics-core" "1.2.5"
"@segment/analytics.js-video-plugins" "^0.2.1"
"@segment/facade" "^3.4.9"
"@segment/tsub" "1.0.1"
@@ -3052,9 +3258,9 @@
unfetch "^4.1.0"
"@segment/analytics-react-native@^2.10.1":
- version "2.13.4"
- resolved "https://registry.yarnpkg.com/@segment/analytics-react-native/-/analytics-react-native-2.13.4.tgz#52216972bf0a1f8722ddf18088340c9d4d90ca5a"
- integrity sha512-47z2TmODJpeA7Pf1P8kE5dNTiqmxJ7khQ/NgiFR3eoiSy/ir0QOpT49QFrwVMeG35fEl+wDGLXUoYWoAMvBy6w==
+ version "2.14.0"
+ resolved "https://registry.yarnpkg.com/@segment/analytics-react-native/-/analytics-react-native-2.14.0.tgz#24727b9fee5c559a2ce8e67df6a23dd92670273d"
+ integrity sha512-614qdb4pncYZJw09pvHd1cQURHYkbTQOWAPo262F98ouxiiR7SAHPQv0vRrkflhiDyHm9glez0PY168J+Mw0jg==
dependencies:
"@segment/sovran-react-native" "^1"
deepmerge "^4.2.2"
@@ -3116,13 +3322,13 @@
shell-quote "1.7.3"
"@segment/sovran-react-native@^1":
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-1.0.1.tgz#4311f0af2e2b606d2c17e535b293c096c6a3c2e8"
- integrity sha512-7VZrIa7/VP59d4QDvAs0ZOhiadlJ+2YC8K8dKOF0fGwiFC0UmQUZVs4IN9GZfbBavXsagVVMgL2GzjVGLLQdBw==
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-1.0.3.tgz#6dac4166ea51bd4fb08b5645546edec6c8cc922d"
+ integrity sha512-Xe00FPFeJE3Y85LQfArRQGdbmCdOzX1Ejws0at0PtaBHBADhrpZdOh7XxZzFvD3PUqMFyaSK91dKXNdACDx4Kg==
dependencies:
ansi-regex "5.0.1"
deepmerge "^4.2.2"
- shell-quote "1.7.3"
+ shell-quote "1.8.0"
"@segment/tsub@1.0.1":
version "1.0.1"
@@ -3134,6 +3340,147 @@
dset "^3.1.1"
tiny-hashes "^1.0.1"
+"@sentry/browser@7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.29.0.tgz#eb162b50adec33ac49ecd3dc930bdffbfda8098e"
+ integrity sha512-Af+dIcntaw405Wt7myDOMGDxiszfy4aBdshrEKYbGgcfHjgXBIdF3iKlNatvl6nrOm+IOVuKgSpCLOr2hiCwzw==
+ dependencies:
+ "@sentry/core" "7.29.0"
+ "@sentry/replay" "7.29.0"
+ "@sentry/types" "7.29.0"
+ "@sentry/utils" "7.29.0"
+ tslib "^1.9.3"
+
+"@sentry/cli@1.74.4":
+ version "1.74.4"
+ resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.4.tgz#7df82f68045a155e1885bfcbb5d303e5259eb18e"
+ integrity sha512-BMfzYiedbModsNBJlKeBOLVYUtwSi99LJ8gxxE4Bp5N8hyjNIN0WVrozAVZ27mqzAuy6151Za3dpmOLO86YlGw==
+ dependencies:
+ https-proxy-agent "^5.0.0"
+ mkdirp "^0.5.5"
+ node-fetch "^2.6.7"
+ npmlog "^4.1.2"
+ progress "^2.0.3"
+ proxy-from-env "^1.1.0"
+ which "^2.0.2"
+
+"@sentry/cli@^1.72.0":
+ version "1.75.2"
+ resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.75.2.tgz#2c38647b38300e52c9839612d42b7c23f8d6455b"
+ integrity sha512-CG0CKH4VCKWzEaegouWfCLQt9SFN+AieFESCatJ7zSuJmzF05ywpMusjxqRul6lMwfUhRKjGKOzcRJ1jLsfTBw==
+ dependencies:
+ https-proxy-agent "^5.0.0"
+ mkdirp "^0.5.5"
+ node-fetch "^2.6.7"
+ progress "^2.0.3"
+ proxy-from-env "^1.1.0"
+ which "^2.0.2"
+
+"@sentry/core@7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.29.0.tgz#bc4b54d56cf7652598d4430cf43ea97cc069f6fe"
+ integrity sha512-+e9aIp2ljtT4EJq3901z6TfEVEeqZd5cWzbKEuQzPn2UO6If9+Utd7kY2Y31eQYb4QnJgZfiIEz1HonuYY6zqQ==
+ dependencies:
+ "@sentry/types" "7.29.0"
+ "@sentry/utils" "7.29.0"
+ tslib "^1.9.3"
+
+"@sentry/hub@7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.29.0.tgz#916f818617b3c3993853737db3e752c21f8f8445"
+ integrity sha512-nIV2NtTn16VukTtWFhROHJ35NyUIXgEGtesG8a1i7D4iRSvkfLkLrQ9i6D0BAE2huqKqQemO3zGEPR00szqsiA==
+ dependencies:
+ "@sentry/core" "7.29.0"
+ "@sentry/types" "7.29.0"
+ "@sentry/utils" "7.29.0"
+ tslib "^1.9.3"
+
+"@sentry/integrations@7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.29.0.tgz#12595ac8d964b8006148618b8d5fad294e623c7f"
+ integrity sha512-BkZe3ALij320VtC5bNkeSz3OUhT9oxZsj2lf5rCuRFqcqw4tvVNADF/Y98mf0L4VCy582M9MlNXmwfewJjxGOA==
+ dependencies:
+ "@sentry/types" "7.29.0"
+ "@sentry/utils" "7.29.0"
+ localforage "^1.8.1"
+ tslib "^1.9.3"
+
+"@sentry/react-native@4.13.0":
+ version "4.13.0"
+ resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-4.13.0.tgz#d1b532f481080aed16532ac2778b20c1275391af"
+ integrity sha512-CxQd5jWPKEPgR1SH5ppf555h7DMhSBZMU3eSZ/VNT+BocgzxxBnf/tcJj92+gpwrzt2m7MiZ3uDfyfQOgyMc8Q==
+ dependencies:
+ "@sentry/browser" "7.29.0"
+ "@sentry/cli" "1.74.4"
+ "@sentry/core" "7.29.0"
+ "@sentry/hub" "7.29.0"
+ "@sentry/integrations" "7.29.0"
+ "@sentry/react" "7.29.0"
+ "@sentry/tracing" "7.29.0"
+ "@sentry/types" "7.29.0"
+ "@sentry/utils" "7.29.0"
+ "@sentry/wizard" "1.4.0"
+
+"@sentry/react@7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.29.0.tgz#a1c2ef522a4ccf1e948d77584e59e1254e09c92b"
+ integrity sha512-pJ138QTChfAiYzFrCgycBgXrAVARV6TdVvLB8z/HsqbHzPq17RhyF9M1xPE4ffeLDQAEuSudwED9CLOpJqKnAw==
+ dependencies:
+ "@sentry/browser" "7.29.0"
+ "@sentry/types" "7.29.0"
+ "@sentry/utils" "7.29.0"
+ hoist-non-react-statics "^3.3.2"
+ tslib "^1.9.3"
+
+"@sentry/replay@7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.29.0.tgz#75d5bb9df39e0a31994be245032c9998af62a304"
+ integrity sha512-Gw7HgviJQu6pX5RFQGVY38Av4qFn9otrZdwSSl/QK5hIyg6yhlh5h7U0ydZkrYYGiW6Z6SYYRpEWCJc/Wbh+ZQ==
+ dependencies:
+ "@sentry/core" "7.29.0"
+ "@sentry/types" "7.29.0"
+ "@sentry/utils" "7.29.0"
+
+"@sentry/tracing@7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.29.0.tgz#767f309cbff46ab12bec6ab3c266f7f03fec91fd"
+ integrity sha512-MAN/G6XROtRhzo/KDjddb6VJn/Q1TaPLwdyj9vvfkUkBNtlt5k16oXp+u7eHWX0uujER9wnZtj2ivXaPeqq0VA==
+ dependencies:
+ "@sentry/core" "7.29.0"
+ "@sentry/types" "7.29.0"
+ "@sentry/utils" "7.29.0"
+ tslib "^1.9.3"
+
+"@sentry/types@7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.29.0.tgz#ed829b6014ee19049035fec6af2b4fea44ff28b8"
+ integrity sha512-DmoEpoqHPty3VxqubS/5gxarwebHRlcBd/yuno+PS3xy++/i9YPjOWLZhU2jYs1cW68M9R6CcCOiC9f2ckJjdw==
+
+"@sentry/utils@7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.29.0.tgz#cbf8f87dd851b0fdc7870db9c68014c321c3bab8"
+ integrity sha512-ICcBwTiBGK8NQA8H2BJo0JcMN6yCeKLqNKNMVampRgS6wSfSk1edvcTdhRkW3bSktIGrIPZrKskBHyMwDGF2XQ==
+ dependencies:
+ "@sentry/types" "7.29.0"
+ tslib "^1.9.3"
+
+"@sentry/wizard@1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@sentry/wizard/-/wizard-1.4.0.tgz#9356ae2cb9e81ee6fa64418d15638607f1a957bd"
+ integrity sha512-Q/f9wJAAAr/YB6oWUzMQP/y5LIgx9la1SanMHNr3hMtVPKkMhvIZO5UWVn2G763yi85zARqSCLDx31/tZd4new==
+ dependencies:
+ "@sentry/cli" "^1.72.0"
+ chalk "^2.4.1"
+ glob "^7.1.3"
+ inquirer "^6.2.0"
+ lodash "^4.17.15"
+ opn "^5.4.0"
+ r2 "^2.0.1"
+ read-env "^1.3.0"
+ semver "^7.3.5"
+ xcode "3.0.1"
+ yargs "^16.2.0"
+
"@sideway/address@^4.1.3":
version "4.1.4"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"
@@ -3168,19 +3515,19 @@
dependencies:
type-detect "4.0.8"
-"@sinonjs/commons@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
- integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==
+"@sinonjs/commons@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72"
+ integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==
dependencies:
type-detect "4.0.8"
"@sinonjs/fake-timers@^10.0.2":
- version "10.0.2"
- resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c"
- integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==
+ version "10.2.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz#b3e322a34c5f26e3184e7f6115695f299c1b1194"
+ integrity sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==
dependencies:
- "@sinonjs/commons" "^2.0.0"
+ "@sinonjs/commons" "^3.0.0"
"@sinonjs/fake-timers@^8.0.1":
version "8.1.0"
@@ -4169,61 +4516,70 @@
pretty-format "^29.0.0"
"@tiptap/core@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.220.tgz#ced4b8f13ad6361f957275510bd0c005de29d18c"
- integrity sha512-F2Q666xJqijBU5o+GqekqseNgIEMTs6BhsLDaf9DwThhljGLS8RXKnSvQxrxLNrYEPpw39n/G3Qt8YAOk5qR6w==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.3.tgz#dfd55124b3e7b0482e5ccb8be46eb9c3189167e2"
+ integrity sha512-jLyVIWAdjjlNzrsRhSE2lVL/7N8228/1R1QtaVU85UlMIwHFAcdzhD8FeiKkqxpTnGpaDVaTy7VNEtEgaYdCyA==
-"@tiptap/extension-bubble-menu@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.220.tgz#3fea0c846f73a237f562fdce05671ef1fa025943"
- integrity sha512-wthyec7s0vZlTSEAAZEgoFfx/1Arwg1zxDUrrE+YAost/Yn+w4xQksz/ts5Bx90iOk2qsJ+jzzttLRV17Ku7lA==
+"@tiptap/extension-bubble-menu@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.3.tgz#44b3c4e35fd478c42467d8fb7dbc9532614e5b18"
+ integrity sha512-lPt1ELrYCuoQrQEUukqjp9xt38EwgPUwaKHI3wwt2Rbv+C6q1gmRsK1yeO/KqCNmFxNqF2p9ZF9srOnug/RZDQ==
dependencies:
- lodash "^4.17.21"
tippy.js "^6.3.7"
"@tiptap/extension-document@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.0-beta.220.tgz#15b4db7a92659eff7efc6d4d877dcf72e3fd61b6"
- integrity sha512-2sja4ZvOb4iynHrzinnclCSFgLyo6fJc1fBV5fIYaOgZOYcvz9KK8fgKiq+wIpG58sJEmQ5kcwwBlkXv+NTK+g==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.3.tgz#b58af5b4f71c0acea953a7ebe8b1d24341bfaf68"
+ integrity sha512-PsYeNQQBYIU9ayz1R11Kv/kKNPFNIV8tApJ9pxelXjzcAhkjncNUazPN/dyho60mzo+WpsmS3ceTj/gK3bCtWA==
-"@tiptap/extension-floating-menu@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.220.tgz#35eb154227533ada738c922be2f8cf18426fe4bf"
- integrity sha512-+WfcBEedm82ntaVIEQAGz0Om96Rpav7a+4f7e8N4PrLKm6nZ3gBaEkZVQ6vjJ6S/1htiWCv1XosYIwRboPBG0w==
+"@tiptap/extension-floating-menu@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.3.tgz#8d9943246aa3247442c1993f235617094fe705b5"
+ integrity sha512-zN1vRGRvyK3pO2aHRmQSOTpl4UJraXYwKYM009n6WviYKUNm0LPGo+VD4OAtdzUhPXyccnlsTv2p6LIqFty6Bg==
dependencies:
tippy.js "^6.3.7"
+"@tiptap/extension-hard-break@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.3.tgz#aa7805d825e5244bdccc508da18c781e231b2859"
+ integrity sha512-RCln6ARn16jvKTjhkcAD5KzYXYS0xRMc0/LrHeV8TKdCd4Yd0YYHe0PU4F9gAgAfPQn7Dgt4uTVJLN11ICl8sQ==
+
+"@tiptap/extension-history@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.3.tgz#8936c15aa46f2ddeada1c3d9abe2888d58d08c30"
+ integrity sha512-00KHIcJ8kivn2ARI6NQYphv2LfllVCXViHGm0EhzDW6NQxCrriJKE3tKDcTFCu7LlC5doMpq9Z6KXdljc4oVeQ==
+
"@tiptap/extension-link@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.0-beta.220.tgz#c9954613cd1e0a0f1527853b732ef50dff734eac"
- integrity sha512-vjEA8cE37ZZVVgPHSpttw3kbJoClb+ya/BVukDtJ1h6C7mIR1rqzNxTgpbnXJuA8xww0JOjpa5dpzEgcs294fA==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.3.tgz#4714a4c23d04032e75b5b8364a9c532f7a385aba"
+ integrity sha512-H72tXQ5rkVCkAhFaf08fbEU7EBUCK0uocsqOF+4th9sOlrhfgyJtc8Jv5EXPDpxNgG5jixSqWBo0zKXQm9s9eg==
dependencies:
linkifyjs "^4.1.0"
"@tiptap/extension-mention@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.0-beta.220.tgz#c3745895096157b09412bd49544f4ae741e8d0da"
- integrity sha512-mjFNBuLxLaZ48CaIp/AdyHB2X1UKptpv6NVG0JaP2vBxW22eUy709JmCbRnWjeYe8pHbJjW22WC4/M1C44SFWg==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.0.3.tgz#7ef0968c31543b806e431982ca697161439410ce"
+ integrity sha512-mT+tMJyf15gN3kW7UfZrP+J0jlhlBnR50SHj0PnDWqGnJ70qKSZTxcHfohrxU6On6yaOFsd+5Omn5seGK4XFWA==
"@tiptap/extension-paragraph@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.0-beta.220.tgz#d552dfdeeab9856e9eb8f0a7cf850f37d7cced69"
- integrity sha512-ZGCzNGFYV4wa3l1nXtDIaYp7O6f0DrGTSl3alKkDTQe3SOmzXS2HjgWl9yPw8VXpU9W5mMGhXd+nGn/jUk+f/A==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.3.tgz#88d332158c70622d36849256f90e43ca4d226dfe"
+ integrity sha512-a+tKtmj4bU3GVCH1NE8VHWnhVexxX5boTVxsHIr4yGG3UoKo1c5AO7YMaeX2W5xB5iIA+BQqOPCDPEAx34dd2A==
"@tiptap/extension-placeholder@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.0-beta.220.tgz#1d6057e5ae950d9a1ed43c03d26df60c08368f87"
- integrity sha512-Pq79BH/JqhjTNgxHkmbzcmwATsSJdRRSLHrnLx5upSmwEkQwCzqni9jL10rL2NM1ZyR+o25xC+r5loujx0aQ+Q==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.0.3.tgz#69575353f09fc7524c9cdbfbf16c04f73c29d154"
+ integrity sha512-Z42jo0termRAf0S0L8oxrts94IWX5waU4isS2CUw8xCUigYyCFslkhQXkWATO1qRbjNFLKN2C9qvCgGf4UeBrw==
"@tiptap/extension-text@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.0-beta.220.tgz#3f51d4aac11c16d79cf8ca22502898b67f5bc2f5"
- integrity sha512-3tnffc2YMjNyv7Lbad6fx9wYDE/Buz8vhx76M2AOSrjYbzmTJf7mLkgdlPM0VTy7FGZD5CGgHJAgYNt5HIqPkQ==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.3.tgz#12b6400a31ac6d35cbaf1822600f4c425457902f"
+ integrity sha512-LvzChcTCcPSMNLUjZe/A9SHXWGDHtvk73fR7CBqAeNU0MxhBPEBI03GFQ6RzW3xX0CmDmjpZoDxFMB+hDEtW1A==
"@tiptap/pm@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.0.0-beta.220.tgz#04e4c98e4d042ea8d67148ec6676f7078c6bac5a"
- integrity sha512-O9mGcmwUpEr630HY9RylIyZJKnpXi3xWINWNiAEfRJ1br5j5pHRoVRJQ1HzU+6+Z+i/8qp3zRHGLTBqihaZETA==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.0.3.tgz#e8bb47df765fc1b7acd52f2800c52d7ff945c5ec"
+ integrity sha512-I9dsInD89Agdm1QjFRO9dmJtU1ldVSILNPW0pEhv9wYqYVvl4HUj/JMtYNqu2jWrCHNXQcaX/WkdSdvGJtmg5g==
dependencies:
prosemirror-changeset "^2.2.0"
prosemirror-collab "^1.3.0"
@@ -4245,17 +4601,17 @@
prosemirror-view "^1.28.2"
"@tiptap/react@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.0-beta.220.tgz#c79df680ee2002061078704e4f35b232588a4a20"
- integrity sha512-AZWaCGjm2FcJWNl1dxRCHOjGYvUV8R39L7tAcnKxHGajOHdFk8JQHc0XbVZhdBi2YgwvwEr7Tw9G2lzi9e6/fg==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.3.tgz#4b7155ed4bfe3fa9cb691adbbcf3713173ca7a6c"
+ integrity sha512-fiAh8Lk+/NBPAR/PE4Kc/aLiBUbUYI/CpAopz8DI9eInNyV8h8LAGa9uFILJQF/TNu0tclJ4rV0sWc7Se0FZMw==
dependencies:
- "@tiptap/extension-bubble-menu" "^2.0.0-beta.220"
- "@tiptap/extension-floating-menu" "^2.0.0-beta.220"
+ "@tiptap/extension-bubble-menu" "^2.0.3"
+ "@tiptap/extension-floating-menu" "^2.0.3"
"@tiptap/suggestion@^2.0.0-beta.220":
- version "2.0.0-beta.220"
- resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.0-beta.220.tgz#2dc05f65e89006ffaad9f2b6a3468311a305e5ee"
- integrity sha512-lYb2HOAKJLjEBbTx5VXA32wRryQiMwaKkNfr3v6UhlwoNgD6NkCYID08UJbpMV7iM+iFQp9408D/vVWFwvOuKg==
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.3.tgz#3f25e20f50de6748f2b65a88e264d9b5887ca16a"
+ integrity sha512-1y3palQStGZq13UtHjouZ50k4sotM+N56cIlFeygIv3gqdai2zGPaPQtqV9FOVVQizXpUbQMTlPSDC5Ej4SPnQ==
"@tokenizer/token@^0.3.0":
version "0.3.0"
@@ -4293,9 +4649,9 @@
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
"@tsconfig/node16@^1.0.2":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
- integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
+ integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@tsconfig/react-native@^2.0.3":
version "2.0.3"
@@ -4303,9 +4659,9 @@
integrity sha512-jE58snEKBd9DXfyR4+ssZmYJ/W2mOSnNrvljR0aLyQJL9JKX6vlWELHkRjb3HBbcM9Uy0hZGijXbqEAjOERW2A==
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14":
- version "7.20.0"
- resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891"
- integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==
+ version "7.20.1"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.1.tgz#916ecea274b0c776fec721e333e55762d3a9614b"
+ integrity sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==
dependencies:
"@babel/parser" "^7.20.7"
"@babel/types" "^7.20.7"
@@ -4329,11 +4685,11 @@
"@babel/types" "^7.0.0"
"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6":
- version "7.18.3"
- resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d"
- integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==
+ version "7.20.0"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.0.tgz#4709d34d3eba3e1dad1950d40e80c6b5e0b81fc9"
+ integrity sha512-TBOjqAGf0hmaqRwpii5LLkJLg7c6OMm4nHLmpsUxwk9bBHtoTC6dAHdVWdGv4TBxj2CZOZY8Xfq8WmfoVi7n4Q==
dependencies:
- "@babel/types" "^7.3.0"
+ "@babel/types" "^7.20.7"
"@types/body-parser@*":
version "1.19.2"
@@ -4351,9 +4707,9 @@
"@types/node" "*"
"@types/connect-history-api-fallback@^1.3.5":
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae"
- integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#9fd20b3974bdc2bcd4ac6567e2e0f6885cb2cf41"
+ integrity sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==
dependencies:
"@types/express-serve-static-core" "*"
"@types/node" "*"
@@ -4374,36 +4730,32 @@
"@types/estree" "*"
"@types/eslint@*", "@types/eslint@^7.29.0 || ^8.4.1":
- version "8.21.3"
- resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.21.3.tgz#5794b3911f0f19e34e3a272c49cbdf48d6f543f2"
- integrity sha512-fa7GkppZVEByMWGbTtE5MbmXWJTVbrjjaS8K6uQj+XtuuUv1fsuPAxhygfqLmsb/Ufb3CV8deFCpiMfAgi00Sw==
+ version "8.40.0"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.40.0.tgz#ae73dc9ec5237f2794c4f79efd6a4c73b13daf23"
+ integrity sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
-"@types/estree@*":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
- integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
+"@types/estree@*", "@types/estree@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
+ integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/estree@0.0.39":
version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
-"@types/estree@^0.0.51":
- version "0.0.51"
- resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
- integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
-
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33":
- version "4.17.33"
- resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543"
- integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==
+ version "4.17.35"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f"
+ integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
+ "@types/send" "*"
"@types/express@*", "@types/express@^4.17.13":
version "4.17.17"
@@ -4446,9 +4798,9 @@
integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==
"@types/http-proxy@^1.17.8":
- version "1.17.10"
- resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.10.tgz#e576c8e4a0cc5c6a138819025a88e167ebb38d6c"
- integrity sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==
+ version "1.17.11"
+ resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.11.tgz#0ca21949a5588d55ac2b659b69035c84bd5da293"
+ integrity sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==
dependencies:
"@types/node" "*"
@@ -4472,9 +4824,9 @@
"@types/istanbul-lib-report" "*"
"@types/jest@^29.4.0":
- version "29.5.0"
- resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.0.tgz#337b90bbcfe42158f39c2fb5619ad044bbb518ac"
- integrity sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==
+ version "29.5.1"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.1.tgz#83c818aa9a87da27d6da85d3378e5a34d2f31a47"
+ integrity sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==
dependencies:
expect "^29.0.0"
pretty-format "^29.0.0"
@@ -4489,9 +4841,9 @@
parse5 "^7.0.0"
"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
- version "7.0.11"
- resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
- integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
+ version "7.0.12"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
+ integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
"@types/json5@^0.0.29":
version "0.0.29"
@@ -4505,13 +4857,6 @@
dependencies:
"@types/lodash" "*"
-"@types/lodash.clonedeep@^4.5.7":
- version "4.5.7"
- resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.7.tgz#0e119f582ed6f9e6b373c04a644651763214f197"
- integrity sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw==
- dependencies:
- "@types/lodash" "*"
-
"@types/lodash.debounce@^4.0.7":
version "4.0.7"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f"
@@ -4555,31 +4900,41 @@
"@types/lodash" "*"
"@types/lodash@*":
- version "4.14.191"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
- integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
+ version "4.14.195"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632"
+ integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==
"@types/mime@*":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
+"@types/mime@^1":
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
+ integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
+
"@types/minimatch@*":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
"@types/node@*":
- version "18.15.3"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014"
- integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==
+ version "20.2.5"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb"
+ integrity sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==
+
+"@types/node@^18.16.2":
+ version "18.16.16"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.16.tgz#3b64862856c7874ccf7439e6bab872d245c86d8e"
+ integrity sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==
"@types/object.omit@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/object.omit/-/object.omit-3.0.0.tgz#0d31e1208eac8fe2ad5c9499a1016a8273bbfafc"
integrity sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw==
-"@types/object.pick@^1.3.1":
+"@types/object.pick@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/object.pick/-/object.pick-1.3.2.tgz#9eb28118240ad8f658b9c9c6caf35359fdb37150"
integrity sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg==
@@ -4622,9 +4977,9 @@
"@types/react" "*"
"@types/react-native@^0.67.3":
- version "0.67.19"
- resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.67.19.tgz#8f2fb257bd9f7b56b07a98be488aab0d79f087fe"
- integrity sha512-tk3D4HtJ4KBmnoOMiPWY5og0m34cwavCPSlV75hMqut2WgcDF9SXvkqZU0RP6qddHwvEstYIJSvSfLMPOak5vQ==
+ version "0.67.21"
+ resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.67.21.tgz#d7a46cb1a1e5c9cdc8d83efefe9e54dc179d7126"
+ integrity sha512-p++6s9efGcIPjDJFxlfXS9zCb2ZVMhDM3eaEUqjmn9InVM1NhquyQlDABn6yZUAhBMqqoor62CXNKR0wC6sPKA==
dependencies:
"@types/react" "^17"
@@ -4643,9 +4998,9 @@
"@types/react" "^17"
"@types/react@*", "@types/react@^17":
- version "17.0.53"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.53.tgz#10d4d5999b8af3d6bc6a9369d7eb953da82442ab"
- integrity sha512-1yIpQR2zdYu1Z/dc1OxC+MA6GR240u3gcnP4l6mvj/PJiVaqHsQPmWttsvHsfnhfPbU2FuGmo0wSITPygjBmsw==
+ version "17.0.60"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.60.tgz#a4a97dcdbebad76612c188fc06440e4995fd8ad2"
+ integrity sha512-pCH7bqWIfzHs3D+PDs3O/COCQJka+Kcw3RnO9rFA2zalqoXg7cNjJDh6mZ7oRtY1wmY4LVwDdAbA1F7Z8tv3BQ==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -4664,14 +5019,22 @@
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
"@types/scheduler@*":
- version "0.16.2"
- resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
- integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
+ version "0.16.3"
+ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
+ integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
"@types/semver@^7.3.12":
- version "7.3.13"
- resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
- integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
+ version "7.5.0"
+ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
+ integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==
+
+"@types/send@*":
+ version "0.17.1"
+ resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301"
+ integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==
+ dependencies:
+ "@types/mime" "^1"
+ "@types/node" "*"
"@types/serve-index@^1.9.1":
version "1.9.1"
@@ -4742,21 +5105,21 @@
"@types/yargs-parser" "*"
"@types/yargs@^17.0.8":
- version "17.0.22"
- resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a"
- integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==
+ version "17.0.24"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902"
+ integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^5.30.5", "@typescript-eslint/eslint-plugin@^5.48.2", "@typescript-eslint/eslint-plugin@^5.5.0":
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz#bc2400c3a23305e8c9a9c04aa40933868aaaeb47"
- integrity sha512-IZGc50rtbjk+xp5YQoJvmMPmJEYoC53SiKPXyqWfv15XoD2Y5Kju6zN0DwlmaGJp1Iw33JsWJcQ7nw0lGCGjVg==
+ version "5.59.8"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.8.tgz#1e7a3e5318ece22251dfbc5c9c6feeb4793cc509"
+ integrity sha512-JDMOmhXteJ4WVKOiHXGCoB96ADWg9q7efPWHRViT/f09bA8XOMLAVHHju3l0MkZnG1izaWXYmgvQcUjTRcpShQ==
dependencies:
"@eslint-community/regexpp" "^4.4.0"
- "@typescript-eslint/scope-manager" "5.55.0"
- "@typescript-eslint/type-utils" "5.55.0"
- "@typescript-eslint/utils" "5.55.0"
+ "@typescript-eslint/scope-manager" "5.59.8"
+ "@typescript-eslint/type-utils" "5.59.8"
+ "@typescript-eslint/utils" "5.59.8"
debug "^4.3.4"
grapheme-splitter "^1.0.4"
ignore "^5.2.0"
@@ -4765,87 +5128,80 @@
tsutils "^3.21.0"
"@typescript-eslint/experimental-utils@^5.0.0":
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.55.0.tgz#ea2dd8737834af3a36b6a7be5bee57f57160c942"
- integrity sha512-3ZqXIZhdGyGQAIIGATeMtg7prA6VlyxGtcy5hYIR/3qUqp3t18pWWUYhL9mpsDm7y8F9mr3ISMt83TiqCt7OPQ==
+ version "5.59.8"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.59.8.tgz#21d20f3b657f8dbd237887d9bbb40bf1b5b38cd0"
+ integrity sha512-jAf+hihtd0G2RLB9x796+3i8D0L5T5xjftuPpJ82RLsPNHdzGXmbZNNftQ558h90ogc45DD8/W3OrxmdSO5Nng==
dependencies:
- "@typescript-eslint/utils" "5.55.0"
+ "@typescript-eslint/utils" "5.59.8"
"@typescript-eslint/parser@^5.30.5", "@typescript-eslint/parser@^5.48.2", "@typescript-eslint/parser@^5.5.0":
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.55.0.tgz#8c96a0b6529708ace1dcfa60f5e6aec0f5ed2262"
- integrity sha512-ppvmeF7hvdhUUZWSd2EEWfzcFkjJzgNQzVST22nzg958CR+sphy8A6K7LXQZd6V75m1VKjp+J4g/PCEfSCmzhw==
+ version "5.59.8"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.8.tgz#60cbb00671d86cf746044ab797900b1448188567"
+ integrity sha512-AnR19RjJcpjoeGojmwZtCwBX/RidqDZtzcbG3xHrmz0aHHoOcbWnpDllenRDmDvsV0RQ6+tbb09/kyc+UT9Orw==
dependencies:
- "@typescript-eslint/scope-manager" "5.55.0"
- "@typescript-eslint/types" "5.55.0"
- "@typescript-eslint/typescript-estree" "5.55.0"
+ "@typescript-eslint/scope-manager" "5.59.8"
+ "@typescript-eslint/types" "5.59.8"
+ "@typescript-eslint/typescript-estree" "5.59.8"
debug "^4.3.4"
-"@typescript-eslint/scope-manager@5.55.0":
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.55.0.tgz#e863bab4d4183ddce79967fe10ceb6c829791210"
- integrity sha512-OK+cIO1ZGhJYNCL//a3ROpsd83psf4dUJ4j7pdNVzd5DmIk+ffkuUIX2vcZQbEW/IR41DYsfJTB19tpCboxQuw==
+"@typescript-eslint/scope-manager@5.59.8":
+ version "5.59.8"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz#ff4ad4fec6433647b817c4a7d4b4165d18ea2fa8"
+ integrity sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==
dependencies:
- "@typescript-eslint/types" "5.55.0"
- "@typescript-eslint/visitor-keys" "5.55.0"
+ "@typescript-eslint/types" "5.59.8"
+ "@typescript-eslint/visitor-keys" "5.59.8"
-"@typescript-eslint/type-utils@5.55.0":
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.55.0.tgz#74bf0233523f874738677bb73cb58094210e01e9"
- integrity sha512-ObqxBgHIXj8rBNm0yh8oORFrICcJuZPZTqtAFh0oZQyr5DnAHZWfyw54RwpEEH+fD8suZaI0YxvWu5tYE/WswA==
+"@typescript-eslint/type-utils@5.59.8":
+ version "5.59.8"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.8.tgz#aa6c029a9d7706d26bbd25eb4666398781df6ea2"
+ integrity sha512-+5M518uEIHFBy3FnyqZUF3BMP+AXnYn4oyH8RF012+e7/msMY98FhGL5SrN29NQ9xDgvqCgYnsOiKp1VjZ/fpA==
dependencies:
- "@typescript-eslint/typescript-estree" "5.55.0"
- "@typescript-eslint/utils" "5.55.0"
+ "@typescript-eslint/typescript-estree" "5.59.8"
+ "@typescript-eslint/utils" "5.59.8"
debug "^4.3.4"
tsutils "^3.21.0"
-"@typescript-eslint/types@5.55.0":
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.55.0.tgz#9830f8d3bcbecf59d12f821e5bc6960baaed41fd"
- integrity sha512-M4iRh4AG1ChrOL6Y+mETEKGeDnT7Sparn6fhZ5LtVJF1909D5O4uqK+C5NPbLmpfZ0XIIxCdwzKiijpZUOvOug==
+"@typescript-eslint/types@5.59.8":
+ version "5.59.8"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.8.tgz#212e54414733618f5d0fd50b2da2717f630aebf8"
+ integrity sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==
-"@typescript-eslint/typescript-estree@5.55.0":
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.55.0.tgz#8db7c8e47ecc03d49b05362b8db6f1345ee7b575"
- integrity sha512-I7X4A9ovA8gdpWMpr7b1BN9eEbvlEtWhQvpxp/yogt48fy9Lj3iE3ild/1H3jKBBIYj5YYJmS2+9ystVhC7eaQ==
+"@typescript-eslint/typescript-estree@5.59.8":
+ version "5.59.8"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz#801a7b1766481629481b3b0878148bd7a1f345d7"
+ integrity sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==
dependencies:
- "@typescript-eslint/types" "5.55.0"
- "@typescript-eslint/visitor-keys" "5.55.0"
+ "@typescript-eslint/types" "5.59.8"
+ "@typescript-eslint/visitor-keys" "5.59.8"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
-"@typescript-eslint/utils@5.55.0", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.43.0":
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.55.0.tgz#34e97322e7ae5b901e7a870aabb01dad90023341"
- integrity sha512-FkW+i2pQKcpDC3AY6DU54yl8Lfl14FVGYDgBTyGKB75cCwV3KpkpTMFi9d9j2WAJ4271LR2HeC5SEWF/CZmmfw==
+"@typescript-eslint/utils@5.59.8", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.0":
+ version "5.59.8"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.8.tgz#34d129f35a2134c67fdaf024941e8f96050dca2b"
+ integrity sha512-Tr65630KysnNn9f9G7ROF3w1b5/7f6QVCJ+WK9nhIocWmx9F+TmCAcglF26Vm7z8KCTwoKcNEBZrhlklla3CKg==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@types/json-schema" "^7.0.9"
"@types/semver" "^7.3.12"
- "@typescript-eslint/scope-manager" "5.55.0"
- "@typescript-eslint/types" "5.55.0"
- "@typescript-eslint/typescript-estree" "5.55.0"
+ "@typescript-eslint/scope-manager" "5.59.8"
+ "@typescript-eslint/types" "5.59.8"
+ "@typescript-eslint/typescript-estree" "5.59.8"
eslint-scope "^5.1.1"
semver "^7.3.7"
-"@typescript-eslint/visitor-keys@5.55.0":
- version "5.55.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.55.0.tgz#01ad414fca8367706d76cdb94adf788dc5b664a2"
- integrity sha512-q2dlHHwWgirKh1D3acnuApXG+VNXpEY5/AwRxDVuEQpxWaB0jCDe0jFMVMALJ3ebSfuOVE8/rMS+9ZOYGg1GWw==
+"@typescript-eslint/visitor-keys@5.59.8":
+ version "5.59.8"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz#aa6a7ef862add919401470c09e1609392ef3cc40"
+ integrity sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==
dependencies:
- "@typescript-eslint/types" "5.55.0"
+ "@typescript-eslint/types" "5.59.8"
eslint-visitor-keys "^3.3.0"
-"@ucans/core@0.11.0":
- version "0.11.0"
- resolved "https://registry.yarnpkg.com/@ucans/core/-/core-0.11.0.tgz#8201680294d980f2b1f5edfaf77b42a86d3a5688"
- integrity sha512-SHX67e313kKBaur5Cp+6WFeOLC7aBhkf1i1jIFpFb9f0f1cvM/lC3mjzOyUBeDg3QwmcN5QSZzaogVFvuVvzvg==
- dependencies:
- uint8arrays "3.0.0"
-
"@urql/core@2.3.6":
version "2.3.6"
resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.3.6.tgz#ee0a6f8fde02251e9560c5f17dce5cd90f948552"
@@ -4855,11 +5211,12 @@
wonka "^4.0.14"
"@urql/core@>=2.3.1":
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/@urql/core/-/core-3.2.2.tgz#2a44015b536d72981822f715c96393d8e0ddc576"
- integrity sha512-i046Cz8cZ4xIzGMTyHZrbdgzcFMcKD7+yhCAH5FwWBRjcKrc+RjEOuR9X5AMuBvr8c6IAaE92xAqa4wmlGfWTQ==
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/@urql/core/-/core-4.0.7.tgz#8918a956f8e2ffbaeb3aae58190d728813de5841"
+ integrity sha512-UtZ9oSbSFODXzFydgLCXpAQz26KGT1d6uEfcylKphiRWNXSWZi8k7vhJXNceNm/Dn0MiZ+kaaJHKcnGY1jvHRQ==
dependencies:
- wonka "^6.1.2"
+ "@0no-co/graphql.web" "^1.0.1"
+ wonka "^6.3.2"
"@urql/exchange-retry@0.3.0":
version "0.3.0"
@@ -4869,146 +5226,146 @@
"@urql/core" ">=2.3.1"
wonka "^4.0.14"
-"@webassemblyjs/ast@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
- integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==
+"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
+ integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==
dependencies:
- "@webassemblyjs/helper-numbers" "1.11.1"
- "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+ "@webassemblyjs/helper-numbers" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
-"@webassemblyjs/floating-point-hex-parser@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f"
- integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==
+"@webassemblyjs/floating-point-hex-parser@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431"
+ integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==
-"@webassemblyjs/helper-api-error@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16"
- integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==
+"@webassemblyjs/helper-api-error@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768"
+ integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==
-"@webassemblyjs/helper-buffer@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5"
- integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==
+"@webassemblyjs/helper-buffer@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093"
+ integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==
-"@webassemblyjs/helper-numbers@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae"
- integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==
+"@webassemblyjs/helper-numbers@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5"
+ integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==
dependencies:
- "@webassemblyjs/floating-point-hex-parser" "1.11.1"
- "@webassemblyjs/helper-api-error" "1.11.1"
+ "@webassemblyjs/floating-point-hex-parser" "1.11.6"
+ "@webassemblyjs/helper-api-error" "1.11.6"
"@xtuc/long" "4.2.2"
-"@webassemblyjs/helper-wasm-bytecode@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1"
- integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==
+"@webassemblyjs/helper-wasm-bytecode@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9"
+ integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==
-"@webassemblyjs/helper-wasm-section@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a"
- integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==
+"@webassemblyjs/helper-wasm-section@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577"
+ integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==
dependencies:
- "@webassemblyjs/ast" "1.11.1"
- "@webassemblyjs/helper-buffer" "1.11.1"
- "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
- "@webassemblyjs/wasm-gen" "1.11.1"
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-buffer" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.11.6"
-"@webassemblyjs/ieee754@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614"
- integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==
+"@webassemblyjs/ieee754@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a"
+ integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==
dependencies:
"@xtuc/ieee754" "^1.2.0"
-"@webassemblyjs/leb128@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5"
- integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==
+"@webassemblyjs/leb128@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7"
+ integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==
dependencies:
"@xtuc/long" "4.2.2"
-"@webassemblyjs/utf8@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff"
- integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==
+"@webassemblyjs/utf8@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a"
+ integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==
-"@webassemblyjs/wasm-edit@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6"
- integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==
+"@webassemblyjs/wasm-edit@^1.11.5":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab"
+ integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==
dependencies:
- "@webassemblyjs/ast" "1.11.1"
- "@webassemblyjs/helper-buffer" "1.11.1"
- "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
- "@webassemblyjs/helper-wasm-section" "1.11.1"
- "@webassemblyjs/wasm-gen" "1.11.1"
- "@webassemblyjs/wasm-opt" "1.11.1"
- "@webassemblyjs/wasm-parser" "1.11.1"
- "@webassemblyjs/wast-printer" "1.11.1"
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-buffer" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/helper-wasm-section" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.11.6"
+ "@webassemblyjs/wasm-opt" "1.11.6"
+ "@webassemblyjs/wasm-parser" "1.11.6"
+ "@webassemblyjs/wast-printer" "1.11.6"
-"@webassemblyjs/wasm-gen@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76"
- integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==
+"@webassemblyjs/wasm-gen@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268"
+ integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==
dependencies:
- "@webassemblyjs/ast" "1.11.1"
- "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
- "@webassemblyjs/ieee754" "1.11.1"
- "@webassemblyjs/leb128" "1.11.1"
- "@webassemblyjs/utf8" "1.11.1"
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/ieee754" "1.11.6"
+ "@webassemblyjs/leb128" "1.11.6"
+ "@webassemblyjs/utf8" "1.11.6"
-"@webassemblyjs/wasm-opt@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2"
- integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==
+"@webassemblyjs/wasm-opt@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2"
+ integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==
dependencies:
- "@webassemblyjs/ast" "1.11.1"
- "@webassemblyjs/helper-buffer" "1.11.1"
- "@webassemblyjs/wasm-gen" "1.11.1"
- "@webassemblyjs/wasm-parser" "1.11.1"
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-buffer" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.11.6"
+ "@webassemblyjs/wasm-parser" "1.11.6"
-"@webassemblyjs/wasm-parser@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199"
- integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==
+"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1"
+ integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==
dependencies:
- "@webassemblyjs/ast" "1.11.1"
- "@webassemblyjs/helper-api-error" "1.11.1"
- "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
- "@webassemblyjs/ieee754" "1.11.1"
- "@webassemblyjs/leb128" "1.11.1"
- "@webassemblyjs/utf8" "1.11.1"
+ "@webassemblyjs/ast" "1.11.6"
+ "@webassemblyjs/helper-api-error" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/ieee754" "1.11.6"
+ "@webassemblyjs/leb128" "1.11.6"
+ "@webassemblyjs/utf8" "1.11.6"
-"@webassemblyjs/wast-printer@1.11.1":
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0"
- integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==
+"@webassemblyjs/wast-printer@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20"
+ integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==
dependencies:
- "@webassemblyjs/ast" "1.11.1"
+ "@webassemblyjs/ast" "1.11.6"
"@xtuc/long" "4.2.2"
-"@webpack-cli/configtest@^2.0.1":
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz#a69720f6c9bad6aef54a8fa6ba9c3533e7ef4c7f"
- integrity sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==
+"@webpack-cli/configtest@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.0.tgz#b59b33377b1b896a9a7357cfc643b39c1524b1e6"
+ integrity sha512-K/vuv72vpfSEZoo5KIU0a2FsEoYdW0DUMtMpB5X3LlUwshetMZRZRxB7sCsVji/lFaSxtQQ3aM9O4eMolXkU9w==
"@webpack-cli/info@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.1.tgz#eed745799c910d20081e06e5177c2b2569f166c0"
integrity sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==
-"@webpack-cli/serve@^2.0.1":
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.1.tgz#34bdc31727a1889198855913db2f270ace6d7bf8"
- integrity sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw==
+"@webpack-cli/serve@^2.0.4":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.4.tgz#3982ee6f8b42845437fc4d391e93ac5d9da52f0f"
+ integrity sha512-0xRgjgDLdz6G7+vvDLlaRpFatJaJ69uTalZLRSMX5B3VUrDmXcrVA3+6fXXQgmYz7bY9AAgs348XQdmtLsK41A==
-"@xmldom/xmldom@~0.7.0", "@xmldom/xmldom@~0.7.7":
- version "0.7.9"
- resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.9.tgz#7f9278a50e737920e21b297b8a35286e9942c056"
- integrity sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA==
+"@xmldom/xmldom@~0.7.7":
+ version "0.7.11"
+ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.11.tgz#adecc134521274711d071d5b0200907cc83b38ee"
+ integrity sha512-UDi3g6Jss/W5FnSzO9jCtQwEpfymt0M+sPPlmLhDH6h2TJ8j4ESE/LpmNPBij15J5NKkk4/cg/qoVMdWI3vnlQ==
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
@@ -5071,26 +5428,17 @@ acorn-globals@^7.0.0:
acorn "^8.1.0"
acorn-walk "^8.0.2"
-acorn-import-assertions@^1.7.6:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
- integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
+acorn-import-assertions@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac"
+ integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
-acorn-node@^1.8.2:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
- integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
- dependencies:
- acorn "^7.0.0"
- acorn-walk "^7.0.0"
- xtend "^4.0.2"
-
-acorn-walk@^7.0.0, acorn-walk@^7.1.1:
+acorn-walk@^7.1.1:
version "7.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
@@ -5100,7 +5448,7 @@ acorn-walk@^8.0.2, acorn-walk@^8.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
-acorn@^7.0.0, acorn@^7.1.1:
+acorn@^7.1.1:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
@@ -5150,7 +5498,7 @@ ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
-ajv-keywords@^5.0.0:
+ajv-keywords@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16"
integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==
@@ -5167,7 +5515,7 @@ ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-ajv@^8.0.0, ajv@^8.11.0, ajv@^8.6.0, ajv@^8.6.3, ajv@^8.8.0:
+ajv@^8.0.0, ajv@^8.11.0, ajv@^8.6.0, ajv@^8.6.3, ajv@^8.9.0:
version "8.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
@@ -5182,7 +5530,7 @@ anser@^1.4.9:
resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b"
integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==
-ansi-escapes@^3.1.0:
+ansi-escapes@^3.1.0, ansi-escapes@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
@@ -5195,9 +5543,9 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.1:
type-fest "^0.21.3"
ansi-escapes@^6.0.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.1.0.tgz#f2912cdaa10785f3f51f4b562a2497b885aadc5e"
- integrity sha512-bQyg9bzRntwR/8b89DOEhGwctcwCrbWW/TuqTQnpqpy5Fz3aovcOTj5i8NJV6AHc8OGNdMaqdxAWww8pz2kiKg==
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.0.tgz#8a13ce75286f417f1963487d86ba9f90dccf9947"
+ integrity sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==
dependencies:
type-fest "^3.0.0"
@@ -5220,6 +5568,16 @@ ansi-regex@5.0.1, ansi-regex@^5.0.0, ansi-regex@^5.0.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+ integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==
+
+ansi-regex@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1"
+ integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==
+
ansi-regex@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed"
@@ -5272,6 +5630,19 @@ application-config-path@^0.1.0:
resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.1.tgz#8b5ac64ff6afdd9bd70ce69f6f64b6998f5f756e"
integrity sha512-zy9cHePtMP0YhwG+CfHm0bgwdnga2X3gZexpdCwEj//dpb+TKajtiC8REEUJUSq6Ab4f9cgNy2l8ObXzCXFkEw==
+aproba@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+are-we-there-yet@~1.1.2:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146"
+ integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
arg@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0"
@@ -5499,21 +5870,22 @@ await-lock@^2.2.2:
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
axe-core@^4.6.2:
- version "4.6.3"
- resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece"
- integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==
+ version "4.7.2"
+ resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0"
+ integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==
-axios@^0.24.0:
- version "0.24.0"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
- integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
+axios@^0.27.2:
+ version "0.27.2"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
+ integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
dependencies:
- follow-redirects "^1.14.4"
+ follow-redirects "^1.14.9"
+ form-data "^4.0.0"
axios@^1.3.4:
- version "1.3.4"
- resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024"
- integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
+ integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
@@ -5558,7 +5930,7 @@ babel-jest@^29.2.1, babel-jest@^29.4.2, babel-jest@^29.5.0:
graceful-fs "^4.2.9"
slash "^3.0.0"
-babel-loader@^8.2.3:
+babel-loader@^8.2.3, babel-loader@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8"
integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==
@@ -5576,6 +5948,14 @@ babel-loader@^9.1.2:
find-cache-dir "^3.3.2"
schema-utils "^4.0.0"
+babel-merge@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/babel-merge/-/babel-merge-3.0.0.tgz#9bd368d48116dab18b8f3e8022835479d80f3b50"
+ integrity sha512-eBOBtHnzt9xvnjpYNI5HmaPp/b2vMveE5XggzqHnQeHJ8mFIBrBv6WZEVIj5jJ2uwTItkqKo9gWzEEcBxEq0yw==
+ dependencies:
+ deepmerge "^2.2.1"
+ object.omit "^3.0.0"
+
babel-plugin-istanbul@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73"
@@ -5643,29 +6023,29 @@ babel-plugin-named-asset-import@^0.3.8:
resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz#6b7fa43c59229685368683c28bc9734f24524cc2"
integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==
-babel-plugin-polyfill-corejs2@^0.3.3:
- version "0.3.3"
- resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122"
- integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==
+babel-plugin-polyfill-corejs2@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz#75044d90ba5043a5fb559ac98496f62f3eb668fd"
+ integrity sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==
dependencies:
"@babel/compat-data" "^7.17.7"
- "@babel/helper-define-polyfill-provider" "^0.3.3"
+ "@babel/helper-define-polyfill-provider" "^0.4.0"
semver "^6.1.1"
-babel-plugin-polyfill-corejs3@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a"
- integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==
+babel-plugin-polyfill-corejs3@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz#39248263c38191f0d226f928d666e6db1b4b3a8a"
+ integrity sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==
dependencies:
- "@babel/helper-define-polyfill-provider" "^0.3.3"
- core-js-compat "^3.25.1"
+ "@babel/helper-define-polyfill-provider" "^0.4.0"
+ core-js-compat "^3.30.1"
-babel-plugin-polyfill-regenerator@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747"
- integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==
+babel-plugin-polyfill-regenerator@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz#e7344d88d9ef18a3c47ded99362ae4a757609380"
+ integrity sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==
dependencies:
- "@babel/helper-define-polyfill-provider" "^0.3.3"
+ "@babel/helper-define-polyfill-provider" "^0.4.0"
babel-plugin-react-native-web@^0.18.12, babel-plugin-react-native-web@~0.18.10:
version "0.18.12"
@@ -5920,9 +6300,9 @@ body-parser@^1.20.1:
unpipe "1.0.0"
bonjour-service@^1.0.11:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.0.tgz#424170268d68af26ff83a5c640b95def01803a13"
- integrity sha512-LVRinRB3k1/K0XzZ2p58COnWvkQknIY6sf0zF2rpErvcJXpMBttEPQSxK+HEXSS9VmpZlDoDnQWv8ftJT20B0Q==
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.1.tgz#960948fa0e0153f5d26743ab15baf8e33752c135"
+ integrity sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==
dependencies:
array-flatten "^2.1.2"
dns-equal "^1.0.0"
@@ -6004,14 +6384,14 @@ browser-process-hrtime@^1.0.0:
integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5:
- version "4.21.5"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7"
- integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==
+ version "4.21.7"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.7.tgz#e2b420947e5fb0a58e8f4668ae6e23488127e551"
+ integrity sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==
dependencies:
- caniuse-lite "^1.0.30001449"
- electron-to-chromium "^1.4.284"
- node-releases "^2.0.8"
- update-browserslist-db "^1.0.10"
+ caniuse-lite "^1.0.30001489"
+ electron-to-chromium "^1.4.411"
+ node-releases "^2.0.12"
+ update-browserslist-db "^1.0.11"
bser@2.1.1:
version "2.1.1"
@@ -6195,6 +6575,11 @@ camelcase-css@^2.0.1:
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+camelcase@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
+ integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
+
camelcase@^5.0.0, camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@@ -6215,25 +6600,51 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464:
- version "1.0.30001468"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001468.tgz#0101837c6a4e38e6331104c33dcfb3bdf367a4b7"
- integrity sha512-zgAo8D5kbOyUcRAgSmgyuvBkjrGk5CGYG5TYgFdpQv+ywcyEpo1LOWoG8YmoflGnh+V+UsNuKYedsoYs0hzV5A==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001489:
+ version "1.0.30001491"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001491.tgz#eab0e0f392de6f7411751d148de9b5bd6b203e46"
+ integrity sha512-17EYIi4TLnPiTzVKMveIxU5ETlxbSO3B6iPvMbprqnKh4qJsQGk5Nh1Lp4jIMAE0XfrujsJuWZAM3oJdMHaKBA==
case-anything@^2.1.10:
- version "2.1.10"
- resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.10.tgz#d18a6ca968d54ec3421df71e3e190f3bced23410"
- integrity sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==
+ version "2.1.13"
+ resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.13.tgz#0cdc16278cb29a7fcdeb072400da3f342ba329e9"
+ integrity sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==
case-sensitive-paths-webpack-plugin@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==
+caseless@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+ integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
+
+cbor-extract@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42"
+ integrity sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA==
+ dependencies:
+ node-gyp-build-optional-packages "5.0.3"
+ optionalDependencies:
+ "@cbor-extract/cbor-extract-darwin-arm64" "2.1.1"
+ "@cbor-extract/cbor-extract-darwin-x64" "2.1.1"
+ "@cbor-extract/cbor-extract-linux-arm" "2.1.1"
+ "@cbor-extract/cbor-extract-linux-arm64" "2.1.1"
+ "@cbor-extract/cbor-extract-linux-x64" "2.1.1"
+ "@cbor-extract/cbor-extract-win32-x64" "2.1.1"
+
+cbor-x@^1.5.1:
+ version "1.5.3"
+ resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.3.tgz#f8252fec7cab86b66c500e0c991788618e6638de"
+ integrity sha512-adrN0S67C7jY2hgqeGcw+Uj6iEGLQa5D/p6/9YNl5AaVIYJaJz/bARfWsP8UikBZWbhS27LN0DJK4531vo9ODw==
+ optionalDependencies:
+ cbor-extract "^2.1.1"
+
cborg@^1.6.0:
- version "1.10.1"
- resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.1.tgz#24cfe52c69ec0f66f95e23dc57f2086954c8d718"
- integrity sha512-et6Qm8MOUY2kCWa5GKk2MlBVoPjHv0hQBmlzI/Z7+5V3VJCeIkGehIB3vWknNsm2kOkAIs6wEKJFJo8luWQQ/w==
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1"
+ integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
@@ -6270,6 +6681,11 @@ char-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e"
integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==
+chardet@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
+ integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+
charenc@0.0.2, charenc@~0.0.1:
version "0.0.2"
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
@@ -6378,9 +6794,14 @@ cli-cursor@^3.1.0:
restore-cursor "^3.1.0"
cli-spinners@^2.0.0, cli-spinners@^2.5.0:
- version "2.7.0"
- resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a"
- integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.0.tgz#5881d0ad96381e117bbe07ad91f2008fe6ffd8db"
+ integrity sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==
+
+cli-width@^2.0.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
+ integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
cliui@^6.0.0:
version "6.0.0"
@@ -6442,6 +6863,11 @@ coa@^2.0.2:
chalk "^2.4.1"
q "^1.1.2"
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+ integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==
+
collect-v8-coverage@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
@@ -6474,7 +6900,7 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
-color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
+color-name@^1.0.0, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
@@ -6506,9 +6932,9 @@ colorette@^1.0.7:
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
colorette@^2.0.10, colorette@^2.0.14:
- version "2.0.19"
- resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
- integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
+ version "2.0.20"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
+ integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
combined-stream@^1.0.8:
version "1.0.8"
@@ -6527,6 +6953,11 @@ commander@2.20.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
+commander@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
+ integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
+
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -6637,6 +7068,11 @@ connect@^3.6.5, connect@^3.7.0:
parseurl "~1.3.3"
utils-merge "1.0.1"
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+ integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
+
content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
@@ -6686,22 +7122,22 @@ copy-webpack-plugin@^10.2.0:
schema-utils "^4.0.0"
serialize-javascript "^6.0.0"
-core-js-compat@^3.25.1:
- version "3.29.1"
- resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.29.1.tgz#15c0fb812ea27c973c18d425099afa50b934b41b"
- integrity sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA==
+core-js-compat@^3.30.1, core-js-compat@^3.30.2:
+ version "3.30.2"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.30.2.tgz#83f136e375babdb8c80ad3c22d67c69098c1dd8b"
+ integrity sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA==
dependencies:
browserslist "^4.21.5"
core-js-pure@^3.23.3:
- version "3.29.1"
- resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.29.1.tgz#1be6ca2b8772f6b4df7fc4621743286e676c6162"
- integrity sha512-4En6zYVi0i0XlXHVz/bi6l1XDjCqkKRq765NXuX+SnaIatlE96Odt5lMLjdxUiNI1v9OXI5DSLWYPlmTfkTktg==
+ version "3.30.2"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.30.2.tgz#005a82551f4af3250dcfb46ed360fad32ced114e"
+ integrity sha512-p/npFUJXXBkCCTIlEGBdghofn00jWG6ZOtdoIXSJmAu2QBvN0IqpZXWweOytcwE6cfx8ZvVUy1vw8zxhe4Y2vg==
core-js@^3.19.2:
- version "3.29.1"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.29.1.tgz#40ff3b41588b091aaed19ca1aa5cb111803fa9a6"
- integrity sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==
+ version "3.30.2"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.2.tgz#6528abfda65e5ad728143ea23f7a14f0dcf503fc"
+ integrity sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==
core-util-is@~1.0.0:
version "1.0.3"
@@ -6762,16 +7198,16 @@ create-require@^1.1.0:
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
crelt@^1.0.0:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94"
- integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
+ integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
cross-fetch@^3.1.5:
- version "3.1.5"
- resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
- integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
+ version "3.1.6"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c"
+ integrity sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==
dependencies:
- node-fetch "2.6.7"
+ node-fetch "^2.6.11"
cross-spawn@^4.0.2:
version "4.0.2"
@@ -6824,9 +7260,9 @@ css-blank-pseudo@^3.0.3:
postcss-selector-parser "^6.0.9"
css-declaration-sorter@^6.3.1:
- version "6.3.1"
- resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec"
- integrity sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz#630618adc21724484b3e9505bce812def44000ad"
+ integrity sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==
css-has-pseudo@^3.0.4:
version "3.0.4"
@@ -6843,14 +7279,14 @@ css-in-js-utils@^3.1.0:
hyphenate-style-name "^1.0.3"
css-loader@^6.5.1:
- version "6.7.3"
- resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd"
- integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==
+ version "6.8.1"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88"
+ integrity sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==
dependencies:
icss-utils "^5.1.0"
- postcss "^8.4.19"
+ postcss "^8.4.21"
postcss-modules-extract-imports "^3.0.0"
- postcss-modules-local-by-default "^4.0.0"
+ postcss-modules-local-by-default "^4.0.3"
postcss-modules-scope "^3.0.0"
postcss-modules-values "^4.0.0"
postcss-value-parser "^4.2.0"
@@ -6942,9 +7378,9 @@ css-what@^6.0.1, css-what@^6.1.0:
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
cssdb@^7.1.0:
- version "7.4.1"
- resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.4.1.tgz#61d55c0173126689922a219e15e131e4b5caf422"
- integrity sha512-0Q8NOMpXJ3iTDDbUv9grcmQAfdDx4qz+fN/+Md2FGbevT+6+bJNQ2LjB2YIUlLbpBTM32idU1Sb+tb/uGt6/XQ==
+ version "7.6.0"
+ resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.6.0.tgz#beac8f7a5f676db62d3c33da517ef4c9eb008f8b"
+ integrity sha512-Nna7rph8V0jC6+JBY4Vk4ndErUmfJfV6NJCaZdurL0omggabiy+QB2HCQtu5c/ACLZ0I7REv7A4QyPIoYzZx0w==
cssesc@^3.0.0:
version "3.0.0"
@@ -7030,9 +7466,9 @@ cssstyle@^2.3.0:
cssom "~0.3.6"
csstype@^3.0.2:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
- integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
+ integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
dag-map@~1.0.0:
version "1.0.2"
@@ -7126,15 +7562,16 @@ dedent@^0.7.0:
integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==
deep-equal@^2.0.5:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6"
- integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.1.tgz#c72ab22f3a7d3503a4ca87dde976fe9978816739"
+ integrity sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==
dependencies:
+ array-buffer-byte-length "^1.0.0"
call-bind "^1.0.2"
- es-get-iterator "^1.1.2"
- get-intrinsic "^1.1.3"
+ es-get-iterator "^1.1.3"
+ get-intrinsic "^1.2.0"
is-arguments "^1.1.1"
- is-array-buffer "^3.0.1"
+ is-array-buffer "^3.0.2"
is-date-object "^1.0.5"
is-regex "^1.1.4"
is-shared-array-buffer "^1.0.2"
@@ -7142,7 +7579,7 @@ deep-equal@^2.0.5:
object-is "^1.1.5"
object-keys "^1.1.1"
object.assign "^4.1.4"
- regexp.prototype.flags "^1.4.3"
+ regexp.prototype.flags "^1.5.0"
side-channel "^1.0.4"
which-boxed-primitive "^1.0.2"
which-collection "^1.0.1"
@@ -7158,12 +7595,17 @@ deep-is@^0.1.3, deep-is@~0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+deepmerge@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
+ integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
+
deepmerge@^3.2.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.3.0.tgz#d3c47fd6f3a93d517b14426b0628a17b0125f5f7"
integrity sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==
-deepmerge@^4.2.2:
+deepmerge@^4.2.2, deepmerge@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
@@ -7195,7 +7637,7 @@ define-lazy-prop@^2.0.0:
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
-define-properties@^1.1.3, define-properties@^1.1.4:
+define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5"
integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==
@@ -7225,11 +7667,6 @@ define-property@^2.0.2:
is-descriptor "^1.0.2"
isobject "^3.0.1"
-defined@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.1.tgz#c0b9db27bfaffd95d6f61399419b893df0f91ebf"
- integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==
-
del@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4"
@@ -7267,6 +7704,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+ integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
+
denodeify@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631"
@@ -7324,29 +7766,20 @@ detect-port-alt@^1.1.6:
address "^1.0.1"
debug "^2.6.0"
-detective@^5.2.1:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034"
- integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==
- dependencies:
- acorn-node "^1.8.2"
- defined "^1.0.0"
- minimist "^1.2.6"
-
detox@^20.1.2:
- version "20.5.0"
- resolved "https://registry.yarnpkg.com/detox/-/detox-20.5.0.tgz#70f1aa7ed4a2b652b5787a806e680fafaab2fcb3"
- integrity sha512-iFDqU5UZ5f1usgRowyiauO83ffMqvN7qFdF5+TVJelfcHTIVHRbZwI/D4MjtAnpuowljfwBhN0tYhTSEOMjCmg==
+ version "20.9.1"
+ resolved "https://registry.yarnpkg.com/detox/-/detox-20.9.1.tgz#78c351c6d5f140d29151ebfe986362276e156d08"
+ integrity sha512-o7x9fHhOoVDZK1069RgefqIxY0B53eAnk7N5/3D8qEa8N0YmvylqzAqeYVtnzHYkveZb1pkcruzKC9jomWTEnw==
dependencies:
ajv "^8.6.3"
bunyan "^1.8.12"
bunyan-debug-stream "^3.1.0"
caf "^15.0.1"
- chalk "^2.4.2"
+ chalk "^4.0.0"
child-process-promise "^2.2.0"
execa "^5.1.1"
- find-up "^4.1.0"
- fs-extra "^4.0.2"
+ find-up "^5.0.0"
+ fs-extra "^11.0.0"
funpermaproxy "^1.1.0"
glob "^8.0.3"
ini "^1.3.4"
@@ -7354,7 +7787,7 @@ detox@^20.1.2:
lodash "^4.17.11"
multi-sort-stream "^1.0.3"
multipipe "^4.0.0"
- node-ipc "^9.2.1"
+ node-ipc "9.2.1"
proper-lockfile "^3.0.2"
resolve-from "^5.0.0"
sanitize-filename "^1.6.1"
@@ -7369,15 +7802,10 @@ detox@^20.1.2:
trace-event-lib "^1.3.1"
which "^1.3.1"
ws "^7.0.0"
- yargs "^16.0.3"
- yargs-parser "^20.2.9"
+ yargs "^17.0.0"
+ yargs-parser "^21.0.0"
yargs-unparser "^2.0.0"
-did-resolver@^4.0.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/did-resolver/-/did-resolver-4.1.0.tgz#740852083c4fd5bf9729d528eca5d105aff45eb6"
- integrity sha512-S6fWHvCXkZg2IhS4RcVHxwuyVejPR7c+a4Go0xbQ9ps5kILa8viiYQgrM4gfTyeTjJ0ekgJH9gk/BawTpmkbZA==
-
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -7416,9 +7844,9 @@ dns-equal@^1.0.0:
integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==
dns-packet@^5.2.2:
- version "5.4.0"
- resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b"
- integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.0.tgz#2202c947845c7a63c23ece58f2f70ff6ab4c2f7d"
+ integrity sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==
dependencies:
"@leichtgewicht/ip-codec" "^2.0.1"
@@ -7500,7 +7928,7 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
dependencies:
domelementtype "^2.2.0"
-domhandler@^5.0.1, domhandler@^5.0.2:
+domhandler@^5.0.2, domhandler@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
@@ -7525,13 +7953,13 @@ domutils@^2.5.2, domutils@^2.8.0:
domhandler "^4.2.0"
domutils@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c"
- integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
+ integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
dependencies:
dom-serializer "^2.0.0"
domelementtype "^2.3.0"
- domhandler "^5.0.1"
+ domhandler "^5.0.3"
dot-case@^3.0.4:
version "3.0.4"
@@ -7552,9 +7980,9 @@ dotenv@^10.0.0:
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
dotenv@^16.0.0, dotenv@^16.0.3:
- version "16.0.3"
- resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
- integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
+ version "16.1.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.1.0.tgz#beb4232166ea080520a42245b8007227ad84b052"
+ integrity sha512-XiwP/4cqatBNLEnKe169vPZCrovUmYngyVA4DgZ3uIVLJfZaBgr4uT0EF2TrEQqgWDDlekGo0muEYme5SR78Ww==
dset@^3.1.1, dset@^3.1.2:
version "3.1.2"
@@ -7604,10 +8032,10 @@ ejs@^3.1.6:
dependencies:
jake "^10.8.5"
-electron-to-chromium@^1.4.284:
- version "1.4.333"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.333.tgz#ebb21f860f8a29923717b06ec0cb54e77ed34c04"
- integrity sha512-YyE8+GKyGtPEP1/kpvqsdhD6rA/TP1DUFDN4uiU/YI52NzDxmwHkEb3qjId8hLBa5siJvG0sfC3O66501jMruQ==
+electron-to-chromium@^1.4.411:
+ version "1.4.414"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.414.tgz#f9eedb6fb01b50439d8228d8ee3a6fa5e0108437"
+ integrity sha512-RRuCvP6ekngVh2SAJaOKT/hxqc9JAsK+Pe0hP5tGQIfonU2Zy9gMGdJ+mBdyl/vNucMG6gkXYtuM4H/1giws5w==
email-validator@^2.0.4:
version "2.0.4"
@@ -7656,10 +8084,10 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
dependencies:
once "^1.4.0"
-enhanced-resolve@^5.10.0:
- version "5.12.0"
- resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
- integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
+enhanced-resolve@^5.14.1:
+ version "5.14.1"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz#de684b6803724477a4af5d74ccae5de52c25f6b3"
+ integrity sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -7670,9 +8098,9 @@ entities@^2.0.0:
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
entities@^4.2.0, entities@^4.4.0:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174"
- integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+ integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
entities@~3.0.1:
version "3.0.1"
@@ -7716,7 +8144,7 @@ errorhandler@^1.5.0:
accepts "~1.3.7"
escape-html "~1.0.3"
-es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.20.4:
+es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2:
version "1.21.2"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff"
integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==
@@ -7761,7 +8189,7 @@ es-array-method-boxes-properly@^1.0.0:
resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==
-es-get-iterator@^1.1.2:
+es-get-iterator@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"
integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==
@@ -7776,10 +8204,10 @@ es-get-iterator@^1.1.2:
isarray "^2.0.5"
stop-iteration-iterator "^1.0.0"
-es-module-lexer@^0.9.0:
- version "0.9.3"
- resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19"
- integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==
+es-module-lexer@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527"
+ integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==
es-set-tostringtag@^2.0.1:
version "2.0.1"
@@ -7844,9 +8272,9 @@ escodegen@^2.0.0:
source-map "~0.6.1"
eslint-config-prettier@^8.5.0:
- version "8.7.0"
- resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.7.0.tgz#f1cc58a8afebc50980bd53475451df146c13182d"
- integrity sha512-HHVXLSlVUhMSmyW4ZzEuvjpwqamgmlfkutD53cYXLikh4pt/modINRcCIApJ84czDxM4GZInwUrromsDdTImTA==
+ version "8.8.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
+ integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
eslint-config-react-app@^7.0.1:
version "7.0.1"
@@ -7878,9 +8306,9 @@ eslint-import-resolver-node@^0.3.7:
resolve "^1.22.1"
eslint-module-utils@^2.7.4:
- version "2.7.4"
- resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974"
- integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49"
+ integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==
dependencies:
debug "^3.2.7"
@@ -7984,6 +8412,15 @@ eslint-plugin-react-hooks@^4.3.0, eslint-plugin-react-hooks@^4.6.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==
+eslint-plugin-react-native-a11y@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react-native-a11y/-/eslint-plugin-react-native-a11y-3.3.0.tgz#0485a8f18474bf54ec68d004b50167f75ffbf201"
+ integrity sha512-21bIs/0yROcMq7KtAG+OVNDWAh8M+6scII0iXcO3i9NYHe2xZ443yPs5KSUMSvQJeRLLjuKB7V5saqNjoMWDHA==
+ dependencies:
+ "@babel/runtime" "^7.15.4"
+ ast-types-flow "^0.0.7"
+ jsx-ast-utils "^3.2.1"
+
eslint-plugin-react-native-globals@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz#ee1348bc2ceb912303ce6bdbd22e2f045ea86ea2"
@@ -8019,11 +8456,11 @@ eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.30.1:
string.prototype.matchall "^4.0.8"
eslint-plugin-testing-library@^5.0.1:
- version "5.10.2"
- resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.2.tgz#12f231ad9b52b6aef45c801fd00aa129a932e0c2"
- integrity sha512-f1DmDWcz5SDM+IpCkEX0lbFqrrTs8HRsEElzDEqN/EBI0hpRj8Cns5+IVANXswE8/LeybIJqPAOQIFu2j5Y5sw==
+ version "5.11.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.0.tgz#0bad7668e216e20dd12f8c3652ca353009163121"
+ integrity sha512-ELY7Gefo+61OfXKlQeXNIDVVLPcvKTeiQOoMZG9TeuWa7Ln4dUNRv8JdRWBQI9Mbb427XGlVB1aa1QPZxBJM8Q==
dependencies:
- "@typescript-eslint/utils" "^5.43.0"
+ "@typescript-eslint/utils" "^5.58.0"
eslint-scope@5.1.1, eslint-scope@^5.1.1:
version "5.1.1"
@@ -8033,10 +8470,10 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
-eslint-scope@^7.1.1:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642"
- integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==
+eslint-scope@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
+ integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
@@ -8046,10 +8483,10 @@ eslint-visitor-keys@^2.1.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
-eslint-visitor-keys@^3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
- integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
+ integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
eslint-webpack-plugin@^3.1.1:
version "3.2.0"
@@ -8063,14 +8500,14 @@ eslint-webpack-plugin@^3.1.1:
schema-utils "^4.0.0"
eslint@^8.19.0, eslint@^8.3.0:
- version "8.36.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf"
- integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==
+ version "8.41.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c"
+ integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.4.0"
- "@eslint/eslintrc" "^2.0.1"
- "@eslint/js" "8.36.0"
+ "@eslint/eslintrc" "^2.0.3"
+ "@eslint/js" "8.41.0"
"@humanwhocodes/config-array" "^0.11.8"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
@@ -8080,9 +8517,9 @@ eslint@^8.19.0, eslint@^8.3.0:
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
- eslint-scope "^7.1.1"
- eslint-visitor-keys "^3.3.0"
- espree "^9.5.0"
+ eslint-scope "^7.2.0"
+ eslint-visitor-keys "^3.4.1"
+ espree "^9.5.2"
esquery "^1.4.2"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
@@ -8090,13 +8527,12 @@ eslint@^8.19.0, eslint@^8.3.0:
find-up "^5.0.0"
glob-parent "^6.0.2"
globals "^13.19.0"
- grapheme-splitter "^1.0.4"
+ graphemer "^1.4.0"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
- js-sdsl "^4.1.4"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
@@ -8108,14 +8544,14 @@ eslint@^8.19.0, eslint@^8.3.0:
strip-json-comments "^3.1.0"
text-table "^0.2.0"
-espree@^9.5.0:
- version "9.5.0"
- resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.0.tgz#3646d4e3f58907464edba852fa047e6a27bdf113"
- integrity sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==
+espree@^9.5.2:
+ version "9.5.2"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b"
+ integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==
dependencies:
acorn "^8.8.0"
acorn-jsx "^5.3.2"
- eslint-visitor-keys "^3.3.0"
+ eslint-visitor-keys "^3.4.1"
esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0:
version "4.0.1"
@@ -8301,22 +8737,22 @@ expo-constants@~14.2.0, expo-constants@~14.2.1:
uuid "^3.3.2"
expo-dev-client@~2.1.1:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-2.1.5.tgz#a0f0a7e319c09813a001c9df1935adef4eb378d5"
- integrity sha512-Xcz+4cQhuUgbQ3krEGqjeC6rwVIZsCnOWLHQyuHuiKGtJLJ6CfKHyuCPY53b7c0DI7ThWafKMD3vc78E7ux3TQ==
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-2.1.6.tgz#b5f614dfcdd2793afda3d57e7fcadc7507ab8158"
+ integrity sha512-6XJS+giOUBA1onRFsT4rtaTkG96cw0tBrnn8LEW5lAM96mN/bl1IZsmyUmLgKfpE40lqvc9ZuYN3Uv2EwTGS/Q==
dependencies:
- expo-dev-launcher "2.1.5"
- expo-dev-menu "2.1.3"
+ expo-dev-launcher "2.1.6"
+ expo-dev-menu "2.1.4"
expo-dev-menu-interface "1.1.1"
expo-manifests "~0.5.0"
expo-updates-interface "~0.9.0"
-expo-dev-launcher@2.1.5:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-2.1.5.tgz#1ed3a407ac8a8f83cd92b0c06e7dcfdfc2dcaf1f"
- integrity sha512-zwQ21JBEpL1FCTlJrPv3cOaDH9UN7MDPPx8k1j9i4ZxRMdLLYIDmgGiP/oz5dcLf4Z1yi3Ofur42eDYDkKgRlQ==
+expo-dev-launcher@2.1.6:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-2.1.6.tgz#4be192cfae397b2024947a437c5b65d154270c1b"
+ integrity sha512-fk2Vb7sJgk++CFfwxuL5A8yZXUghqTOZy0fXqpYBJlskSq2sQr8LPoOrqxEQhnA06/CEzS2OC6FTFo+aY9UkBQ==
dependencies:
- expo-dev-menu "2.1.3"
+ expo-dev-menu "2.1.4"
resolve-from "^5.0.0"
semver "^7.3.5"
@@ -8325,14 +8761,21 @@ expo-dev-menu-interface@1.1.1:
resolved "https://registry.yarnpkg.com/expo-dev-menu-interface/-/expo-dev-menu-interface-1.1.1.tgz#8a0d979f62d9a192696f66a77f75d8fab79e604b"
integrity sha512-doT+7WrSBnxCcTGZw9QIEZoL+43U4RywbG8XZwbhkcsFWGsh9scp0y/bv3ieFHxRtIdImxbxOoYh7fy1O6g28w==
-expo-dev-menu@2.1.3:
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-2.1.3.tgz#e349d157b284e68c3eebec924c9bdc2f22174dbf"
- integrity sha512-meQ3irhGNGyx6jKEpHy18WDS7on0iAJSmDnhT3+Jx55Ya+hdIvebF+aHDd4TrE/C5/Hlsn9/Fpm8bFAgmC1xpw==
+expo-dev-menu@2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-2.1.4.tgz#8bf8ae605d75199a72b603d7ac246e853b8404ca"
+ integrity sha512-T9YPrfo3M+tf4kH61wp36QI2XU2FxeG7EMYg1bcF4BjYx4fUs6i/QvxJ32o5eB+96fXraG2bhiv0Q2QlYWU8Tg==
dependencies:
expo-dev-menu-interface "1.1.1"
semver "^7.3.5"
+expo-device@~5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-5.2.1.tgz#2962abdb9682e5b991a82836667f2e7d7103d9ef"
+ integrity sha512-ZWGph+fGQPxo9v2e0YygPb45Hl+ZR3mh4tpLY5AOYK/sNjQy+Lu3T/sLGIdi2TOcYNL2oZwzZ6eGvwVYmdIfLg==
+ dependencies:
+ ua-parser-js "^0.7.33"
+
expo-eas-client@~0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/expo-eas-client/-/expo-eas-client-0.5.1.tgz#3ef80dbbde13abe35be4e2a2e29b73d2f7fdf27a"
@@ -8357,17 +8800,24 @@ expo-image-loader@~4.1.0:
resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-4.1.1.tgz#efadbb17de1861106864820194900f336dd641b6"
integrity sha512-ciEHVokU0f6w0eTxdRxLCio6tskMsjxWIoV92+/ZD37qePUJYMfEphPhu1sruyvMBNR8/j5iyOvPFVGTfO8oxA==
-expo-image-picker@~14.1.1:
+expo-image-manipulator@^11.1.1:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-11.1.1.tgz#bb54df80e98abc9798876e3f70596a5b880168c9"
+ integrity sha512-W9LfJK/IL7EhhkkC1JQnEX/1S9B09rcGasJiQjXc2s1bEsrQnqXvXEv7shUW8b/L8rE+ynf+XvvDE+YIDL7oFg==
+ dependencies:
+ expo-image-loader "~4.1.0"
+
+expo-image-picker@^14.1.1:
version "14.1.1"
resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-14.1.1.tgz#181f1348ba6a43df7b87cee4a601d45c79b7c2d7"
integrity sha512-SvWtnkLW7jp5Ntvk3lVcRQmhFYja8psmiR7O6P/+7S6f4llt3vaFwb4I3+pUXqJxxpi7BHc2+95qOLf0SFOIag==
dependencies:
expo-image-loader "~4.1.0"
-expo-image@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.0.0.tgz#a3670d20815d99e2527307a33761c9b0088823b1"
- integrity sha512-A1amVExKhBa/eRXuceauYtPkf9izeje5AbxEWL09tgK91rf3GSIZXM5PSDGlIM0s7dpCV+Iet2jhwcFUfWaZrw==
+expo-image@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.2.3.tgz#f3582d725ffb7437f8ce946ad44fe33f0aa0603d"
+ integrity sha512-+Mnx6rcneWSUGfHkUDV3cQ3R4lVwoIDFs/tcXVqnlxyNJdNxpW2cge9pS2Hpj3UDoZVhZPLR8LHS8E9wEaC0NA==
expo-json-utils@~0.5.0:
version "0.5.1"
@@ -8409,31 +8859,36 @@ expo-modules-autolinking@1.2.0:
find-up "^5.0.0"
fs-extra "^9.1.0"
-expo-modules-core@1.2.6:
- version "1.2.6"
- resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.2.6.tgz#921abc8031fe0e5474ee48905071902b9627d051"
- integrity sha512-vyleKepkP8F6L+D55B/E4FbZ8x9pdy3yw/mdbGBkDkrmo2gmeMjOM1mKLSszOkLIqet05O7Wy8m0FZHZTo0VBg==
+expo-modules-core@1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.2.7.tgz#c80627b13a8f1c94ae9da8eea41e1ef1df5788c8"
+ integrity sha512-sulqn2M8+tIdxi6QFkKppDEzbePAscgE2LEHocYoQOgHxJpeT7axE0Hkzc+81EeviQilZzGeFZMtNMGh3c9yJg==
dependencies:
compare-versions "^3.4.0"
invariant "^2.2.4"
-expo-pwa@0.0.124:
- version "0.0.124"
- resolved "https://registry.yarnpkg.com/expo-pwa/-/expo-pwa-0.0.124.tgz#684e68aea6c7f95864a8cde17a57e223ed017199"
- integrity sha512-hYvQQhxATNTivWSRc9nrd1WVYJJnBG8P/SVrJ4PPu0pmsS7ZIvWt981IXYG461y9UWnTbXdZEG4UOt0Thak1Gg==
+expo-pwa@0.0.125:
+ version "0.0.125"
+ resolved "https://registry.yarnpkg.com/expo-pwa/-/expo-pwa-0.0.125.tgz#fb5a66f21e7c9a51cdfa76d692b48bd116e6e002"
+ integrity sha512-A40Man5vMO1WWHwVDJr/7Y2N6vwHCQDX4gQ1LM9GngEFHRMK2lxx/tMVV2v+UF2g1lr84RVRGzMvO/tV9LYiaA==
dependencies:
"@expo/image-utils" "0.3.23"
chalk "^4.0.0"
commander "2.20.0"
update-check "1.5.3"
-expo-splash-screen@~0.18.1:
- version "0.18.1"
- resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.18.1.tgz#e090b045a7f8c5d9597b7a96910caa4eae1fcf3b"
- integrity sha512-1di1kuh14likGUs3fyVZWAqEMxhmdAjpmf9T8Qk5OzUa5oPEMEDYB2e2VprddWnJNBVVe/ojBDSCY8w56/LS0Q==
+expo-sharing@~11.2.2:
+ version "11.2.2"
+ resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-11.2.2.tgz#7d9e387f1a902e6dd6838c22d9599dae9e7432cf"
+ integrity sha512-4Lhm1eS/CFIzX+JPuxMUTWBt9rv/WdvJvpQ9y+71bL/9w9dhvsdt9tv0SsNZATz4hk0tbrYD8ZEUsgiHiT1KkQ==
+
+expo-splash-screen@~0.18.2:
+ version "0.18.2"
+ resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.18.2.tgz#dde246204da875785ba40c7143a70013cdefdbb6"
+ integrity sha512-fsiKmyn/lbJtV6Uor6wSvl21fScOidFzmB/HHShQJJOu2TBN/vqMvhPu/r0bF5NVk8Wi64r98hiWY1EEsbW03w==
dependencies:
"@expo/configure-splash-screen" "^0.6.0"
- "@expo/prebuild-config" "6.0.0"
+ "@expo/prebuild-config" "6.0.1"
expo-status-bar@~1.4.4:
version "1.4.4"
@@ -8475,15 +8930,15 @@ expo-updates@~0.16.4:
fbemitter "^3.0.0"
resolve-from "^5.0.0"
-expo@~48.0.11:
- version "48.0.11"
- resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.11.tgz#afd43c7a5ddce3d02a3f27263c95f8d01e1fb84d"
- integrity sha512-KX1RCHhdhdT4DjCeRqYJpZXhdCTuqxHHdNIRoFkmCgkUARYlZbB+Y1U8/KMz8fBAlFoEq99cF/KyRr87VAxRCw==
+expo@~48.0.18:
+ version "48.0.19"
+ resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.19.tgz#0f13be65d3cac99922666e5939388fc22b147e6a"
+ integrity sha512-Pmz2HEwcDdjWPq5fM3vF++je0hjZIBX9aTZEkm6sBv09Vfhe4+CuiuKDq3iE+N6G9l2+eFYoRCApDwLqcRMiPA==
dependencies:
"@babel/runtime" "^7.20.0"
- "@expo/cli" "0.7.0"
+ "@expo/cli" "0.7.3"
"@expo/config" "8.0.2"
- "@expo/config-plugins" "6.0.1"
+ "@expo/config-plugins" "6.0.2"
"@expo/vector-icons" "^13.0.0"
babel-preset-expo "~9.3.2"
cross-spawn "^6.0.5"
@@ -8494,7 +8949,7 @@ expo@~48.0.11:
expo-font "~11.1.1"
expo-keep-awake "~12.0.1"
expo-modules-autolinking "1.2.0"
- expo-modules-core "1.2.6"
+ expo-modules-core "1.2.7"
fbemitter "^3.0.0"
getenv "^1.0.0"
invariant "^2.2.4"
@@ -8560,6 +9015,15 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
+external-editor@^3.0.3:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
+ integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
+ dependencies:
+ chardet "^0.7.0"
+ iconv-lite "^0.4.24"
+ tmp "^0.0.33"
+
extglob@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
@@ -8585,9 +9049,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.1.2:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
- integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
+ integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
fast-glob@^3.2.12, fast-glob@^3.2.5, fast-glob@^3.2.7, fast-glob@^3.2.9:
version "3.2.12"
@@ -8633,9 +9097,9 @@ fast-printf@^1.6.9:
boolean "^3.1.4"
fast-redact@^3.1.1:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
- integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.2.0.tgz#b1e2d39bc731376d28bde844454fa23e26919987"
+ integrity sha512-zaTadChr+NekyzallAMXATXLOR8MNx3zqpZ0MUF2aGf4EathnG0f32VLODNlY8IuGY3HoRO2L6/6fSzNsLaHIw==
fast-text-encoding@^1.0.6:
version "1.0.6"
@@ -8643,9 +9107,9 @@ fast-text-encoding@^1.0.6:
integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==
fast-xml-parser@^4.0.12:
- version "4.1.3"
- resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.1.3.tgz#0254ad0d4d27f07e6b48254b068c0c137488dd97"
- integrity sha512-LsNDahCiCcJPe8NO7HijcnukHB24tKbfDDA5IILx9dmW3Frb52lhbeX6MPNUSvyGNfav2VTYpJ/OqkRoVLrh2Q==
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.2.tgz#cb7310d1e9cf42d22c687b0fae41f3c926629368"
+ integrity sha512-DLzIPtQqmvmdq3VUKR7T6omPK/VCRNqgFlGtbESfyhcH2R4I8EzK1/K6E8PkRCK2EabWrUHK32NjYRbEFnnz0Q==
dependencies:
strnum "^1.0.5"
@@ -8705,6 +9169,13 @@ fetch-retry@^4.1.1:
resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-4.1.1.tgz#fafe0bb22b54f4d0a9c788dff6dd7f8673ca63f3"
integrity sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ integrity sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@@ -8734,7 +9205,7 @@ file-uri-to-path@1.0.0:
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
-filelist@^1.0.1:
+filelist@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==
@@ -8877,16 +9348,16 @@ flatted@^3.1.0:
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
flow-parser@0.*:
- version "0.202.0"
- resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.202.0.tgz#534178266d3ceec5368415e59990db97eece5bd0"
- integrity sha512-ZiXxSIXK3zPmY3zrzCofFonM2T+/3Jz5QZKJyPVtUERQEJUnYkXBQ+0H3FzyqiyJs+VXqb/UNU6/K6sziVYdxw==
+ version "0.207.0"
+ resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.207.0.tgz#376975f6b88991bf0ef9496fa3bffd5eb3120046"
+ integrity sha512-s90OlXqzWj1xc4yUtqD1Gr8pGVx0/5rk9gsqPrOYF1kBAPMH4opkmzdWgQ8aNe3Pckqtwr8DlYGbfE2GnW+zsg==
flow-parser@^0.185.0:
version "0.185.2"
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.185.2.tgz#cb7ee57f77377d6c5d69a469e980f6332a15e492"
integrity sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ==
-follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.15.0:
+follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@@ -8996,14 +9467,14 @@ fs-extra@^10.0.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
-fs-extra@^4.0.2:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
- integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
+fs-extra@^11.0.0:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
+ integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
dependencies:
- graceful-fs "^4.1.2"
- jsonfile "^4.0.0"
- universalify "^0.1.0"
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
fs-extra@^8.1.0, fs-extra@~8.1.0:
version "8.1.0"
@@ -9061,7 +9532,7 @@ function.prototype.name@^1.1.5:
es-abstract "^1.19.0"
functions-have-names "^1.2.2"
-functions-have-names@^1.2.2:
+functions-have-names@^1.2.2, functions-have-names@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
@@ -9071,6 +9542,20 @@ funpermaproxy@^1.1.0:
resolved "https://registry.yarnpkg.com/funpermaproxy/-/funpermaproxy-1.1.0.tgz#39cb0b8bea908051e4608d8a414f1d87b55bf557"
integrity sha512-2Sp1hWuO8m5fqeFDusyhKqYPT+7rGLw34N3qonDcdRP8+n7M7Gl/yKp/q7oCxnnJ6pWCectOmLFJpsMU/++KrQ==
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ integrity sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -9082,12 +9567,13 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
- integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
+ integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
+ has-proto "^1.0.1"
has-symbols "^1.0.3"
get-own-enumerable-property-symbols@^3.0.0:
@@ -9389,6 +9875,11 @@ has-tostringtag@^1.0.0:
dependencies:
has-symbols "^1.0.2"
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+ integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==
+
has-value@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@@ -9458,7 +9949,7 @@ history@^5.3.0:
dependencies:
"@babel/runtime" "^7.7.6"
-hoist-non-react-statics@^3.3.0:
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -9535,9 +10026,9 @@ html-to-text@7.1.1:
minimist "^1.2.5"
html-webpack-plugin@^5.5.0:
- version "5.5.0"
- resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50"
- integrity sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==
+ version "5.5.1"
+ resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz#826838e31b427f5f7f30971f8d8fa2422dfa6763"
+ integrity sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA==
dependencies:
"@types/html-minifier-terser" "^6.0.0"
html-minifier-terser "^6.0.2"
@@ -9657,7 +10148,7 @@ hyphenate-style-name@^1.0.0, hyphenate-style-name@^1.0.3:
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
-iconv-lite@0.4.24:
+iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -9703,10 +10194,15 @@ image-size@^0.6.0:
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.6.3.tgz#e7e5c65bb534bd7cdcedd6cb5166272a85f75fb2"
integrity sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==
+immediate@~3.0.5:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+ integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
+
immer@^9.0.7:
- version "9.0.19"
- resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.19.tgz#67fb97310555690b5f9cd8380d38fc0aabb6b38b"
- integrity sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==
+ version "9.0.21"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
+ integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==
import-fresh@^2.0.0:
version "2.0.0"
@@ -9778,6 +10274,25 @@ inline-style-prefixer@^6.0.1:
css-in-js-utils "^3.1.0"
fast-loops "^1.1.3"
+inquirer@^6.2.0:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca"
+ integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==
+ dependencies:
+ ansi-escapes "^3.2.0"
+ chalk "^2.4.2"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^3.0.3"
+ figures "^2.0.0"
+ lodash "^4.17.12"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rxjs "^6.4.0"
+ string-width "^2.1.0"
+ strip-ansi "^5.1.0"
+ through "^2.3.6"
+
internal-ip@4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907"
@@ -9823,9 +10338,9 @@ ipaddr.js@1.9.1, ipaddr.js@^1.9.0:
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
ipaddr.js@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
- integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f"
+ integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==
is-accessor-descriptor@^0.1.6:
version "0.1.6"
@@ -9908,9 +10423,9 @@ is-ci@^2.0.0:
ci-info "^2.0.0"
is-core-module@^2.11.0, is-core-module@^2.9.0:
- version "2.11.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
- integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
+ version "2.12.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd"
+ integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==
dependencies:
has "^1.0.3"
@@ -9985,6 +10500,13 @@ is-extglob@^2.1.1:
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==
+ dependencies:
+ number-is-nan "^1.0.0"
+
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
@@ -10305,14 +10827,14 @@ istanbul-reports@^3.1.3:
istanbul-lib-report "^3.0.0"
jake@^10.8.5:
- version "10.8.5"
- resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
- integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==
+ version "10.8.7"
+ resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f"
+ integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==
dependencies:
async "^3.2.3"
chalk "^4.0.2"
- filelist "^1.0.1"
- minimatch "^3.0.4"
+ filelist "^1.0.4"
+ minimatch "^3.1.2"
jest-changed-files@^27.5.1:
version "27.5.1"
@@ -11236,10 +11758,15 @@ jimp-compact@0.16.1:
resolved "https://registry.yarnpkg.com/jimp-compact/-/jimp-compact-0.16.1.tgz#9582aea06548a2c1e04dd148d7c3ab92075aefa3"
integrity sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==
+jiti@^1.18.2:
+ version "1.18.2"
+ resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd"
+ integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==
+
joi@^17.2.1:
- version "17.8.4"
- resolved "https://registry.yarnpkg.com/joi/-/joi-17.8.4.tgz#f2d91ab8acd3cca4079ba70669c65891739234aa"
- integrity sha512-jjdRHb5WtL+KgSHvOULQEPPv4kcl+ixd1ybOFQq3rWLgEEqc03QMmilodL0GVJE14U/SQDXkUhQUSZANGDH/AA==
+ version "17.9.2"
+ resolved "https://registry.yarnpkg.com/joi/-/joi-17.9.2.tgz#8b2e4724188369f55451aebd1d0b1d9482470690"
+ integrity sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==
dependencies:
"@hapi/hoek" "^9.0.0"
"@hapi/topo" "^5.0.0"
@@ -11274,11 +11801,6 @@ js-queue@2.0.2:
dependencies:
easy-stack "^1.0.1"
-js-sdsl@^4.1.4:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711"
- integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==
-
js-sha256@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
@@ -11410,9 +11932,9 @@ jsesc@~0.5.0:
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
json-cycle@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/json-cycle/-/json-cycle-1.3.0.tgz#c4f6f7d926c2979012cba173b06f9cae9e866d3f"
- integrity sha512-FD/SedD78LCdSvJaOUQAXseT8oQBb5z6IVYaQaCrVUlu9zOAr1BDdKyVYQaSD/GDsAMrXpKcOyBD4LIl8nfjHw==
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/json-cycle/-/json-cycle-1.5.0.tgz#b1f1d976eee16cef51d5f3d3b3caece3e90ba23a"
+ integrity sha512-GOehvd5PO2FeZ5T4c+RxobeT5a1PiGpF4u9/3+UvrMU4bhnVqzJY7hm39wg8PDCqkU91fWGH8qjWR4bn+wgq9w==
json-parse-better-errors@^1.0.1:
version "1.0.2"
@@ -11463,7 +11985,7 @@ json5@^0.5.1:
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
integrity sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==
-json5@^1.0.1, json5@^1.0.2:
+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==
@@ -11512,7 +12034,7 @@ jsonwebtoken@^8.5.1:
ms "^2.1.1"
semver "^5.6.0"
-"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3:
+"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1, jsx-ast-utils@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea"
integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==
@@ -11636,7 +12158,14 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
-lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.0.6:
+lie@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
+ integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==
+ dependencies:
+ immediate "~3.0.5"
+
+lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
@@ -11654,9 +12183,9 @@ linkify-it@^4.0.1:
uc.micro "^1.0.1"
linkifyjs@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.0.tgz#0460bfcc37d3348fa80e078d92e7bbc82588db15"
- integrity sha512-Ffv8VoY3+ixI1b3aZ3O+jM6x17cOsgwfB1Wq7pkytbo1WlyRp6ZO0YDMqiWT/gQPY/CmtiGuKfzDIVqxh1aCTA==
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.1.tgz#73d427e3bbaaf4ca8e71c589ad4ffda11a9a5fde"
+ integrity sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==
loader-runner@^4.2.0:
version "4.3.0"
@@ -11677,6 +12206,13 @@ loader-utils@^3.2.0:
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576"
integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==
+localforage@^1.8.1:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4"
+ integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
+ dependencies:
+ lie "3.1.1"
+
locate-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
@@ -11704,11 +12240,6 @@ lodash.chunk@^4.2.0:
resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc"
integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==
-lodash.clonedeep@^4.5.0:
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
- integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
-
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@@ -11799,7 +12330,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
-lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0:
+lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -11984,9 +12515,9 @@ media-typer@0.3.0:
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
memfs@^3.1.2, memfs@^3.4.3:
- version "3.4.13"
- resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.13.tgz#248a8bd239b3c240175cd5ec548de5227fc4f345"
- integrity sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.5.1.tgz#f0cd1e2bfaef58f6fe09bfb9c2288f07fea099ec"
+ integrity sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA==
dependencies:
fs-monkey "^1.0.3"
@@ -12120,7 +12651,7 @@ metro-minify-uglify@0.73.9:
dependencies:
uglify-es "^3.1.9"
-metro-react-native-babel-preset@0.73.9:
+metro-react-native-babel-preset@0.73.9, metro-react-native-babel-preset@^0.73.7:
version "0.73.9"
resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.9.tgz#ef54637dd20f025197beb49e71309a9c539e73e2"
integrity sha512-AoD7v132iYDV4K78yN2OLgTPwtAKn0XlD2pOhzyBxiI8PeXzozhbKyPV7zUOJUPETj+pcEVfuYj5ZN/8+bhbCw==
@@ -12164,50 +12695,6 @@ metro-react-native-babel-preset@0.73.9:
"@babel/template" "^7.0.0"
react-refresh "^0.4.0"
-metro-react-native-babel-preset@^0.73.7:
- version "0.73.8"
- resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.8.tgz#04908f264f5d99c944ae20b5b11f659431328431"
- integrity sha512-spNrcQJTbQntEIqJnCA6yL4S+dzV9fXCk7U+Rm7yJasZ4o4Frn7jP23isu7FlZIp1Azx1+6SbP7SgQM+IP5JgQ==
- dependencies:
- "@babel/core" "^7.20.0"
- "@babel/plugin-proposal-async-generator-functions" "^7.0.0"
- "@babel/plugin-proposal-class-properties" "^7.0.0"
- "@babel/plugin-proposal-export-default-from" "^7.0.0"
- "@babel/plugin-proposal-nullish-coalescing-operator" "^7.0.0"
- "@babel/plugin-proposal-object-rest-spread" "^7.0.0"
- "@babel/plugin-proposal-optional-catch-binding" "^7.0.0"
- "@babel/plugin-proposal-optional-chaining" "^7.0.0"
- "@babel/plugin-syntax-dynamic-import" "^7.0.0"
- "@babel/plugin-syntax-export-default-from" "^7.0.0"
- "@babel/plugin-syntax-flow" "^7.18.0"
- "@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0"
- "@babel/plugin-syntax-optional-chaining" "^7.0.0"
- "@babel/plugin-transform-arrow-functions" "^7.0.0"
- "@babel/plugin-transform-async-to-generator" "^7.0.0"
- "@babel/plugin-transform-block-scoping" "^7.0.0"
- "@babel/plugin-transform-classes" "^7.0.0"
- "@babel/plugin-transform-computed-properties" "^7.0.0"
- "@babel/plugin-transform-destructuring" "^7.0.0"
- "@babel/plugin-transform-flow-strip-types" "^7.0.0"
- "@babel/plugin-transform-function-name" "^7.0.0"
- "@babel/plugin-transform-literals" "^7.0.0"
- "@babel/plugin-transform-modules-commonjs" "^7.0.0"
- "@babel/plugin-transform-named-capturing-groups-regex" "^7.0.0"
- "@babel/plugin-transform-parameters" "^7.0.0"
- "@babel/plugin-transform-react-display-name" "^7.0.0"
- "@babel/plugin-transform-react-jsx" "^7.0.0"
- "@babel/plugin-transform-react-jsx-self" "^7.0.0"
- "@babel/plugin-transform-react-jsx-source" "^7.0.0"
- "@babel/plugin-transform-runtime" "^7.0.0"
- "@babel/plugin-transform-shorthand-properties" "^7.0.0"
- "@babel/plugin-transform-spread" "^7.0.0"
- "@babel/plugin-transform-sticky-regex" "^7.0.0"
- "@babel/plugin-transform-template-literals" "^7.0.0"
- "@babel/plugin-transform-typescript" "^7.5.0"
- "@babel/plugin-transform-unicode-regex" "^7.0.0"
- "@babel/template" "^7.0.0"
- react-refresh "^0.4.0"
-
metro-react-native-babel-transformer@0.73.9:
version "0.73.9"
resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.9.tgz#4f4f0cfa5119bab8b53e722fabaf90687d0cbff0"
@@ -12418,9 +12905,9 @@ min-indent@^1.0.0:
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
mini-css-extract-plugin@^2.4.5, mini-css-extract-plugin@^2.5.2:
- version "2.7.5"
- resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz#afbb344977659ec0f1f6e050c7aea456b121cfc5"
- integrity sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ==
+ version "2.7.6"
+ resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz#282a3d38863fddcd2e0c220aaed5b90bc156564d"
+ integrity sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==
dependencies:
schema-utils "^4.0.0"
@@ -12483,10 +12970,10 @@ minipass@^3.0.0, minipass@^3.1.1:
dependencies:
yallist "^4.0.0"
-minipass@^4.0.0:
- version "4.2.5"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.5.tgz#9e0e5256f1e3513f8c34691dd68549e85b2c8ceb"
- integrity sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==
+minipass@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
+ integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
minizlib@^2.1.1:
version "2.1.2"
@@ -12509,7 +12996,7 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
-mkdirp@^0.5.1, mkdirp@~0.5.1:
+mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
@@ -12532,9 +13019,9 @@ mobx-utils@^6.0.6:
integrity sha512-lzJtxOWgj3Dp2HeXviInV3ZRY4YhThzRHXuy90oKXDH2g+ymJGIts4bdjb7NQuSi34V25cMZoQX7TkHJQuKLOQ==
mobx@^6.6.1:
- version "6.8.0"
- resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.8.0.tgz#59051755fdb5c8a9f3f2e0a9b6abaf86bab7f843"
- integrity sha512-+o/DrHa4zykFMSKfS8Z+CPSEg5LW9tSNGTuN8o6MF1GKxlfkSHSeJn5UtgxvPkGgaouplnrLXCF+duAsmm6FHQ==
+ version "6.9.0"
+ resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.9.0.tgz#8a894c26417c05bed2cf7499322e589ee9787397"
+ integrity sha512-HdKewQEREEJgsWnErClfbFoVebze6rGazxFLU/XUyrII8dORfVszN1V0BMRnQSzcgsNNtkX8DHj3nC6cdWE9YQ==
moment@^2.19.3:
version "2.29.4"
@@ -12582,6 +13069,11 @@ multipipe@^4.0.0:
duplexer2 "^0.1.2"
object-assign "^4.1.0"
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+ integrity sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==
+
mv@~2:
version "2.1.1"
resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2"
@@ -12605,10 +13097,10 @@ nan@^2.14.0:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
-nanoid@^3.1.23, nanoid@^3.3.1, nanoid@^3.3.4:
- version "3.3.4"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
- integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+nanoid@^3.1.23, nanoid@^3.3.1, nanoid@^3.3.6:
+ version "3.3.6"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
+ integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanomatch@^1.2.9:
version "1.2.13"
@@ -12688,9 +13180,9 @@ nocache@^3.0.1:
integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==
node-abi@^3.3.0:
- version "3.33.0"
- resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.33.0.tgz#8b23a0cec84e1c5f5411836de6a9b84bccf26e7f"
- integrity sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog==
+ version "3.40.0"
+ resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.40.0.tgz#51d8ed44534f70ff1357dfbc3a89717b1ceac1b4"
+ integrity sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==
dependencies:
semver "^7.3.5"
@@ -12706,17 +13198,10 @@ node-dir@^0.1.17:
dependencies:
minimatch "^3.0.2"
-node-fetch@2.6.7:
- version "2.6.7"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
- integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
- dependencies:
- whatwg-url "^5.0.0"
-
-node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7:
- version "2.6.9"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
- integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==
+node-fetch@^2.0.0-alpha.8, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.11, node-fetch@^2.6.7:
+ version "2.6.11"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25"
+ integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==
dependencies:
whatwg-url "^5.0.0"
@@ -12725,6 +13210,11 @@ node-forge@^1, node-forge@^1.2.1, node-forge@^1.3.1:
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
+node-gyp-build-optional-packages@5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17"
+ integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==
+
node-html-parser@^5.2.0:
version "5.4.2"
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a"
@@ -12738,7 +13228,7 @@ node-int64@^0.4.0:
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
-node-ipc@^9.2.1:
+node-ipc@9.2.1:
version "9.2.1"
resolved "https://registry.yarnpkg.com/node-ipc/-/node-ipc-9.2.1.tgz#b32f66115f9d6ce841dc4ec2009d6a733f98bb6b"
integrity sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==
@@ -12747,10 +13237,10 @@ node-ipc@^9.2.1:
js-message "1.0.7"
js-queue "2.0.2"
-node-releases@^2.0.8:
- version "2.0.10"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
- integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
+node-releases@^2.0.12:
+ version "2.0.12"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039"
+ integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==
node-stream-zip@^1.9.1:
version "1.15.0"
@@ -12770,9 +13260,9 @@ nodemailer-html-to-text@^3.2.0:
html-to-text "7.1.1"
nodemailer@^6.8.0:
- version "6.9.1"
- resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.1.tgz#8249d928a43ed85fec17b13d2870c8f758a126ed"
- integrity sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==
+ version "6.9.3"
+ resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.3.tgz#e4425b85f05d83c43c5cd81bf84ab968f8ef5cbe"
+ integrity sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==
normalize-css-color@^1.0.2:
version "1.0.2"
@@ -12823,6 +13313,16 @@ npm-run-path@^4.0.1:
dependencies:
path-key "^3.0.0"
+npmlog@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
nth-check@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
@@ -12842,10 +13342,15 @@ nullthrows@^1.1.1:
resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+ integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==
+
nwsapi@^2.2.0, nwsapi@^2.2.2:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0"
- integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==
+ version "2.2.5"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.5.tgz#a52744c61b3889dd44b0a158687add39b8d935e2"
+ integrity sha512-6xpotnECFy/og7tKSBVmUNft7J3jyXAka4XvG6AUhFWRz+Q/Ljus7znJAA3bxColfQLdS+XsjoodtJfCgeTEFQ==
ob1@0.73.9:
version "0.73.9"
@@ -12930,14 +13435,15 @@ object.fromentries@^2.0.6:
es-abstract "^1.20.4"
object.getownpropertydescriptors@^2.1.0:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz#db5a9002489b64eef903df81d6623c07e5b4b4d3"
- integrity sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz#5e5c384dd209fa4efffead39e3a0512770ccc312"
+ integrity sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==
dependencies:
array.prototype.reduce "^1.0.5"
call-bind "^1.0.2"
- define-properties "^1.1.4"
- es-abstract "^1.20.4"
+ define-properties "^1.2.0"
+ es-abstract "^1.21.2"
+ safe-array-concat "^1.0.0"
object.hasown@^1.1.2:
version "1.1.2"
@@ -13054,6 +13560,13 @@ opencollective-postinstall@^2.0.3:
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
+opn@^5.4.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
+ integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==
+ dependencies:
+ is-wsl "^1.1.0"
+
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@@ -13106,9 +13619,9 @@ ora@^5.4.1:
wcwidth "^1.0.1"
orderedmap@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.0.tgz#819457082fa3a06abd316d83a281a1ca467437cd"
- integrity sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA==
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
+ integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
os-homedir@^1.0.0:
version "1.0.2"
@@ -13378,10 +13891,15 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
-pg-connection-string@^2.5.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
- integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
+pg-cloudflare@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz#833d70870d610d14bf9df7afb40e1cba310c17a0"
+ integrity sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==
+
+pg-connection-string@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.0.tgz#12a36cc4627df19c25cc1b9b736cc39ee1f73ae8"
+ integrity sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg==
pg-int8@1.0.1:
version "1.0.1"
@@ -13409,18 +13927,20 @@ pg-types@^2.1.0:
postgres-date "~1.0.4"
postgres-interval "^1.1.0"
-pg@^8.8.0, pg@^8.9.0:
- version "8.10.0"
- resolved "https://registry.yarnpkg.com/pg/-/pg-8.10.0.tgz#5b8379c9b4a36451d110fc8cd98fc325fe62ad24"
- integrity sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==
+pg@^8.10.0, pg@^8.9.0:
+ version "8.11.0"
+ resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.0.tgz#a37e534e94b57a7ed811e926f23a7c56385f55d9"
+ integrity sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==
dependencies:
buffer-writer "2.0.0"
packet-reader "1.0.0"
- pg-connection-string "^2.5.0"
+ pg-connection-string "^2.6.0"
pg-pool "^3.6.0"
pg-protocol "^1.6.0"
pg-types "^2.1.0"
pgpass "1.x"
+ optionalDependencies:
+ pg-cloudflare "^1.1.0"
pgpass@1.x:
version "1.0.5"
@@ -13485,14 +14005,14 @@ pino-http@^8.2.1, pino-http@^8.3.3:
process-warning "^2.0.0"
pino-std-serializers@^6.0.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz#307490fd426eefc95e06067e85d8558603e8e844"
- integrity sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.1.tgz#369f4ae2a19eb6d769ddf2c88a2164b76879a284"
+ integrity sha512-wHuWB+CvSVb2XqXM0W/WOYUkVSPbiJb9S5fNB7TBhd8s892Xq910bRxwHtC4l71hgztObTjXL6ZheZXFjhDrDQ==
pino@^8.0.0, pino@^8.11.0, pino@^8.6.1:
- version "8.11.0"
- resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498"
- integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==
+ version "8.14.1"
+ resolved "https://registry.yarnpkg.com/pino/-/pino-8.14.1.tgz#bb38dcda8b500dd90c1193b6c9171eb777a47ac8"
+ integrity sha512-8LYNv7BKWXSfS+k6oEc6occy5La+q2sPwU3q2ljTX5AZk7v+5kND2o5W794FyRaqha6DJajmkNRsWtPpFyMUdw==
dependencies:
atomic-sleep "^1.0.0"
fast-redact "^3.1.1"
@@ -13720,10 +14240,10 @@ postcss-image-set-function@^4.0.7:
dependencies:
postcss-value-parser "^4.2.0"
-postcss-import@^14.1.0:
- version "14.1.0"
- resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
- integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
+postcss-import@^15.1.0:
+ version "15.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70"
+ integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==
dependencies:
postcss-value-parser "^4.0.0"
read-cache "^1.0.0"
@@ -13734,7 +14254,7 @@ postcss-initial@^4.0.1:
resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42"
integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==
-postcss-js@^4.0.0:
+postcss-js@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2"
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
@@ -13749,13 +14269,13 @@ postcss-lab-function@^4.2.1:
"@csstools/postcss-progressive-custom-properties" "^1.1.0"
postcss-value-parser "^4.2.0"
-postcss-load-config@^3.1.4:
- version "3.1.4"
- resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
- integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
+postcss-load-config@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd"
+ integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==
dependencies:
lilconfig "^2.0.5"
- yaml "^1.10.2"
+ yaml "^2.1.1"
postcss-loader@^6.2.1:
version "6.2.1"
@@ -13831,10 +14351,10 @@ postcss-modules-extract-imports@^3.0.0:
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
-postcss-modules-local-by-default@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
- integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
+postcss-modules-local-by-default@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524"
+ integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==
dependencies:
icss-utils "^5.0.0"
postcss-selector-parser "^6.0.2"
@@ -13854,12 +14374,12 @@ postcss-modules-values@^4.0.0:
dependencies:
icss-utils "^5.0.0"
-postcss-nested@6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.0.tgz#1572f1984736578f360cffc7eb7dca69e30d1735"
- integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==
+postcss-nested@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c"
+ integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==
dependencies:
- postcss-selector-parser "^6.0.10"
+ postcss-selector-parser "^6.0.11"
postcss-nesting@^10.2.0:
version "10.2.0"
@@ -14063,9 +14583,9 @@ postcss-selector-not@^6.0.1:
postcss-selector-parser "^6.0.10"
postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9:
- version "6.0.11"
- resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc"
- integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==
+ version "6.0.13"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b"
+ integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
@@ -14098,12 +14618,12 @@ postcss@^7.0.35:
picocolors "^0.2.1"
source-map "^0.6.1"
-postcss@^8.0.9, postcss@^8.3.5, postcss@^8.4.19, postcss@^8.4.4:
- version "8.4.21"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
- integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
+postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.4:
+ version "8.4.24"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.24.tgz#f714dba9b2284be3cc07dbd2fc57ee4dc972d2df"
+ integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==
dependencies:
- nanoid "^3.3.4"
+ nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
@@ -14170,9 +14690,9 @@ prettier-linter-helpers@^1.0.0:
fast-diff "^1.1.2"
prettier@^2.8.3:
- version "2.8.4"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3"
- integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==
+ version "2.8.8"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
+ integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
pretty-bytes@5.6.0, pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
version "5.6.0"
@@ -14231,16 +14751,16 @@ process-nextick-args@~2.0.0:
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process-warning@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.1.0.tgz#1e60e3bfe8183033bbc1e702c2da74f099422d1a"
- integrity sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg==
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.2.0.tgz#008ec76b579820a8e5c35d81960525ca64feb626"
+ integrity sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
-progress@2.0.3:
+progress@2.0.3, progress@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@@ -14296,41 +14816,41 @@ proper-lockfile@^3.0.2:
signal-exit "^3.0.2"
prosemirror-changeset@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.0.tgz#22c05da271a118be40d3e339fa2cace789b1254b"
- integrity sha512-QM7ohGtkpVpwVGmFb8wqVhaz9+6IUXcIQBGZ81YNAKYuHiFJ1ShvSzab4pKqTinJhwciZbrtBEk/2WsqSt2PYg==
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz#dae94b63aec618fac7bb9061648e6e2a79988383"
+ integrity sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==
dependencies:
prosemirror-transform "^1.0.0"
prosemirror-collab@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.0.tgz#601d33473bf72e6c43041a54b860c84c60b37769"
- integrity sha512-+S/IJ69G2cUu2IM5b3PBekuxs94HO1CxJIWOFrLQXUaUDKL/JfBx+QcH31ldBlBXyDEUl+k3Vltfi1E1MKp2mA==
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
+ integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-commands@^1.0.0, prosemirror-commands@^1.3.1:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.1.tgz#89ddfa14e144dcc7fb0938aa0e2568c7fdde306f"
- integrity sha512-ga1ga/RkbzxfAvb6iEXYmrEpekn5NCwTb8w1dr/gmhSoaGcQ0VPuCzOn5qDEpC45ql2oDkKoKQbRxLJwKLpMTQ==
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz#e94aeea52286f658cd984270de9b4c3fff580852"
+ integrity sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-dropcursor@^1.5.0:
- version "1.7.1"
- resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.7.1.tgz#b6921ef866ca95b6f6c8b197767f60dc39598416"
- integrity sha512-GmWk9bAwhfHwA8xmJhBFjPcebxUG9zAPYtqpIr7NTDigWZZEJCgUYyUQeqgyscLr8ZHoh9aeprX9kW7BihUT+w==
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d"
+ integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
prosemirror-view "^1.1.0"
prosemirror-gapcursor@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.1.tgz#8cfd874592e4504d63720e14ed680c7866e64554"
- integrity sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4"
+ integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==
dependencies:
prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0"
@@ -14338,42 +14858,43 @@ prosemirror-gapcursor@^1.3.1:
prosemirror-view "^1.0.0"
prosemirror-history@^1.0.0, prosemirror-history@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.0.tgz#bf5a1ff7759aca759ddf0c722c2fa5b14fb0ddc1"
- integrity sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA==
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.2.tgz#ce6ad7ab9db83e761aee716f3040d74738311b15"
+ integrity sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==
dependencies:
prosemirror-state "^1.2.2"
prosemirror-transform "^1.0.0"
+ prosemirror-view "^1.31.0"
rope-sequence "^1.3.0"
prosemirror-inputrules@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz#476dde2dc244050b3aca00cf58a82adfad6749e7"
- integrity sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ==
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.2.1.tgz#8faf3d78c16150aedac71d326a3e3947417ce557"
+ integrity sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.0:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.1.tgz#3839e7db66cecddae7451f4246e73bdd8489be1d"
- integrity sha512-kVK6WGC+83LZwuSJnuCb9PsADQnFZllt94qPP3Rx/vLcOUV65+IbBeH2nS5cFggPyEVJhGkGrgYFRrG250WhHQ==
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e"
+ integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==
dependencies:
prosemirror-state "^1.0.0"
w3c-keyname "^2.2.0"
prosemirror-markdown@^1.10.1:
- version "1.10.1"
- resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.10.1.tgz#e20468201cda1916a6182686159398b242bb78ab"
- integrity sha512-s7iaTLiX+qO5z8kF2NcMmy2T7mIlxzkS4Sp3vTKSYChPtbMpg6YxFkU0Y06rUg2WtKlvBu7v1bXzlGBkfjUWAA==
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.11.0.tgz#75f2d6f14655762b4b8a247436b87ed81e22c7ee"
+ integrity sha512-yP9mZqPRstjZhhf3yykCQNE3AijxARrHe4e7esV9A+gp4cnGOH4QvrKYPpXLHspNWyvJJ+0URH+iIvV5qP1I2Q==
dependencies:
markdown-it "^13.0.1"
prosemirror-model "^1.0.0"
prosemirror-menu@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.1.tgz#94d99a8547b7ba5680c20e9c497ce19846ce3b2c"
- integrity sha512-sBirXxVfHalZO4f1ZS63WzewINK4182+7dOmoMeBkqYO8wqMBvBS7wQuwVOHnkMWPEh0+N0LJ856KYUN+vFkmQ==
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.2.tgz#c545a2de0b8cb79babc07682b1d93de0f273aa33"
+ integrity sha512-437HIWTq4F9cTX+kPfqZWWm+luJm95Aut/mLUy+9OMrOml0bmWDS26ceC6SNfb2/S94et1sZ186vLO7pDHzxSw==
dependencies:
crelt "^1.0.0"
prosemirror-commands "^1.0.0"
@@ -14381,32 +14902,32 @@ prosemirror-menu@^1.2.1:
prosemirror-state "^1.0.0"
prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1:
- version "1.19.0"
- resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.0.tgz#d7ad9a65ada0bb12196f64fe0dd4fc392c841c29"
- integrity sha512-/CvFGJnwc41EJSfDkQLly1cAJJJmBpZwwUJtwZPTjY2RqZJfM8HVbCreOY/jti8wTRbVyjagcylyGoeJH/g/3w==
+ version "1.19.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.2.tgz#297c9ecfb103154e605f0dbaf3cc72ee32ca0ad5"
+ integrity sha512-RXl0Waiss4YtJAUY3NzKH0xkJmsZupCIccqcIFoLTIKFlKNbIvFDRl27/kQy1FP8iUAxrjRRfIVvOebnnXJgqQ==
dependencies:
orderedmap "^2.0.0"
prosemirror-schema-basic@^1.2.0:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.1.tgz#a5a137a6399d1a829873332117d2fe8131d291d0"
- integrity sha512-vYBdIHsYKSDIqYmPBC7lnwk9DsKn8PnVqK97pMYP5MLEDFqWIX75JiaJTzndBii4bRuNqhC2UfDOfM3FKhlBHg==
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7"
+ integrity sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==
dependencies:
prosemirror-model "^1.19.0"
prosemirror-schema-list@^1.2.2:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.2.tgz#bafda37b72367d39accdcaf6ddf8fb654a16e8e5"
- integrity sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.3.tgz#12e3d70cb17780980a3c28588ed7c888121d5e8d"
+ integrity sha512-HD8yjDOusz7JB3oBFCaMOpEN9Z9DZttLr6tcASjnvKMc0qTyX5xgAN8YiMFFEcwyhF7WZrZ2YQkAwzsn8ICVbQ==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.1:
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.2.tgz#f93bd8a33a4454efab917ba9b738259d828db7e5"
- integrity sha512-puuzLD2mz/oTdfgd8msFbe0A42j5eNudKAAPDB0+QJRw8cO1ygjLmhLrg9RvDpf87Dkd6D4t93qdef00KKNacQ==
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080"
+ integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
@@ -14424,26 +14945,26 @@ prosemirror-tables@^1.3.0:
prosemirror-view "^1.13.3"
prosemirror-trailing-node@^2.0.2:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.3.tgz#213fc0e545a434ff3c37b5218a0de69561bf3892"
- integrity sha512-lGrjMrn97KWkjQSW/FjdvnhJmqFACmQIyr6lKYApvHitDnKsCoZz6XzrHB7RZYHni/0NxQmZ01p/2vyK2SkvaA==
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.4.tgz#60febdeb947550ee93a224f2e56dbd5cb2cdd607"
+ integrity sha512-0Yl9w7IdHkaCdqR+NE3FOucePME4OmiGcybnF1iasarEILP5U8+4xTnl53yafULjmwcg1SrSG65Hg7Zk2H2v3g==
dependencies:
- "@babel/runtime" "^7.13.10"
- "@remirror/core-constants" "^2.0.0"
- "@remirror/core-helpers" "^2.0.1"
+ "@babel/runtime" "^7.21.0"
+ "@remirror/core-constants" "^2.0.1"
+ "@remirror/core-helpers" "^2.0.2"
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.0:
- version "1.7.1"
- resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.7.1.tgz#b516e818c3add0bdf960f4ca8ccb9d057a3ba21b"
- integrity sha512-VteoifAfpt46z0yEt6Fc73A5OID9t/y2QIeR5MgxEwTuitadEunD/V0c9jQW8ziT8pbFM54uTzRLJ/nLuQjMxg==
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.7.2.tgz#f3e57d8424afa6ab7c2b2319cc0ac58e75f7160b"
+ integrity sha512-b94lVUdA9NyaYRb2WuGSgb5YANiITa05dtew9eSK+KkYu64BCnU27WhJPE95gAWAnhV57CM3FabWXM23gri8Kg==
dependencies:
prosemirror-model "^1.0.0"
-prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.28.2:
- version "1.30.2"
- resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.30.2.tgz#57a9d15c5baa454f0d0f4a3028ddbd9be1e8ed9b"
- integrity sha512-nTNzZvalQf9kHeEyO407LiV6DoOs/pXsid88UqW9Vvybo4ozJW2PJhkfZUxCUF1hR/9vJLdhxX84wuw9P9HsXA==
+prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.28.2, prosemirror-view@^1.31.0:
+ version "1.31.3"
+ resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.31.3.tgz#cfe171c4e50a577526d0235d9ec757cdddf6017d"
+ integrity sha512-UYDa8WxRFZm0xQLXiPJUVTl6H08Fn0IUVDootA7ZlQwzooqVWnBOXLovJyyTKgws1nprfsPhhlvWgt2jo4ZA6g==
dependencies:
prosemirror-model "^1.16.0"
prosemirror-state "^1.0.0"
@@ -14486,9 +15007,9 @@ punycode@^2.1.0, punycode@^2.1.1:
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
pure-rand@^6.0.0:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.1.tgz#31207dddd15d43f299fdcdb2f572df65030c19af"
- integrity sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306"
+ integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==
q@^1.1.2:
version "1.5.1"
@@ -14532,10 +15053,14 @@ quick-format-unescaped@^4.0.3:
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
-quick-lru@^5.1.1:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
- integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+r2@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/r2/-/r2-2.0.1.tgz#94cd802ecfce9a622549c8182032d8e4a2b2e612"
+ integrity sha512-EEmxoxYCe3LHzAUhRIRxdCKERpeRNmlLj6KLUSORqnK6dWl/K5ShmDGZqM2lRZQeqJgF+wyqk0s1M7SWUveNOQ==
+ dependencies:
+ caseless "^0.12.0"
+ node-fetch "^2.0.0-alpha.8"
+ typedarray-to-buffer "^3.1.2"
raf@^3.4.1:
version "3.4.1"
@@ -14643,9 +15168,9 @@ react-dev-utils@^12.0.1:
text-table "^0.2.0"
react-devtools-core@^4.26.1:
- version "4.27.2"
- resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.2.tgz#d20fc57e258c656eedabafc2c851d38b33583148"
- integrity sha512-8SzmIkpO87alD7Xr6gWIEa1jHkMjawOZ+6egjazlnjB4UUcbnzGDf/vBJ4BzGuWWEM+pzrxuzsPpcMqlQkYK2g==
+ version "4.27.8"
+ resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.8.tgz#b7b387b079c14ae9a214d4846a402da2b6efd164"
+ integrity sha512-KwoH8/wN/+m5wTItLnsgVraGNmFrcTWR3k1VimP1HjtMMw4CNF+F5vg4S/0tzTEKIdpCi2R7mPNTC+/dswZMgw==
dependencies:
shell-quote "^1.6.1"
ws "^7"
@@ -14689,9 +15214,9 @@ react-native-appstate-hook@^1.0.6:
integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ==
react-native-background-fetch@^4.1.8:
- version "4.1.9"
- resolved "https://registry.yarnpkg.com/react-native-background-fetch/-/react-native-background-fetch-4.1.9.tgz#10ebff9ca45a8868f1a72b2aa6cea40d499ece6e"
- integrity sha512-sk4MCXRhGghBXu9ReabuT8U0WRzjsMt2i/nqCwR9eHi0hux+4kUh5ubpLKLByw5G8WifUv1sp6qsA7uvQERrrQ==
+ version "4.1.10"
+ resolved "https://registry.yarnpkg.com/react-native-background-fetch/-/react-native-background-fetch-4.1.10.tgz#12c7e85140af67fb05edb7cd9960e4f09a457797"
+ integrity sha512-Ug54vTctZuD/c06ZLk/VyvFdhw/hCVVOHYR5heyMqc6FlT/m9fVhFWyl4uH3JmPCzmWDVR3fO28CzrGpKOrusw==
react-native-codegen@^0.71.5:
version "0.71.5"
@@ -14710,6 +15235,13 @@ react-native-dotenv@^3.3.1:
dependencies:
dotenv "^16.0.3"
+react-native-draggable-flatlist@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/react-native-draggable-flatlist/-/react-native-draggable-flatlist-4.0.1.tgz#2f027d387ba4b8f3eb0907340e32cb85e6460df2"
+ integrity sha512-ZO1QUTNx64KZfXGXeXcBfql67l38X7kBcJ3rxUVZzPHt5r035GnGzIC0F8rqSXp6zgnwgUYMfB6zQc5PKmPL9Q==
+ dependencies:
+ "@babel/preset-typescript" "^7.17.12"
+
react-native-drawer-layout@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-3.2.0.tgz#1ab05d0bed6bb684353c17c96e1d3e6c1a4e225d"
@@ -14737,16 +15269,16 @@ react-native-gesture-handler@~2.9.0:
prop-types "^15.7.2"
react-native-get-random-values@^1.8.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.8.0.tgz#1cb4bd4bd3966a356e59697b8f372999fe97cb16"
- integrity sha512-H/zghhun0T+UIJLmig3+ZuBCvF66rdbiWUfRSNS6kv5oDSpa1ZiVyvRWtuPesQpT8dXj+Bv7WJRQOUP+5TB1sA==
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.9.0.tgz#6cb30511c406922e75fe73833dc1812a85bfb37e"
+ integrity sha512-+29IR2oxzxNVeaRwCqGZ9ABadzMI8SLTBidrIDXPOkKnm5+kEmLt34QKM4JV+d2usPErvKyS85le0OmGTHnyWQ==
dependencies:
fast-base64-decode "^1.0.0"
-react-native-gradle-plugin@^0.71.17:
- version "0.71.17"
- resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.17.tgz#cf780a27270f0a32dca8184eff91555d7627dd00"
- integrity sha512-OXXYgpISEqERwjSlaCiaQY6cTY5CH6j73gdkWpK0hedxtiWMWgH+i5TOi4hIGYitm9kQBeyDu+wim9fA8ROFJA==
+react-native-gradle-plugin@^0.71.18:
+ version "0.71.19"
+ resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.19.tgz#3379e28341fcd189bc1f4691cefc84c1a4d7d232"
+ integrity sha512-1dVk9NwhoyKHCSxcrM6vY6cxmojeATsBobDicX0ZKr7DgUF2cBQRTKsimQFvzH8XhOVXyH8p4HyDSZNIFI8OlQ==
react-native-haptic-feedback@^1.14.0:
version "1.14.0"
@@ -14782,18 +15314,15 @@ react-native-progress@bluesky-social/react-native-progress:
dependencies:
prop-types "^15.7.2"
-react-native-reanimated@~2.14.4:
- version "2.14.4"
- resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.14.4.tgz#3fa3da4e7b99f5dfb28f86bcf24d9d1024d38836"
- integrity sha512-DquSbl7P8j4SAmc+kRdd75Ianm8G+IYQ9T4AQ6lrpLVeDkhZmjWI0wkutKWnp6L7c5XNVUrFDUf69dwETLCItQ==
+react-native-reanimated@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.3.0.tgz#80f9d58e28fddf62fe4c1bc792337b8ab57936ab"
+ integrity sha512-LzfpPZ1qXBGy5BcUHqw3pBC0qSd22qXS3t8hWSbozXNrBkzMhhOrcILE/nEg/PHpNNp1xvGOW8NwpAMF006roQ==
dependencies:
"@babel/plugin-transform-object-assign" "^7.16.7"
"@babel/preset-typescript" "^7.16.7"
- convert-source-map "^1.7.0"
+ convert-source-map "^2.0.0"
invariant "^2.2.4"
- lodash.isequal "^4.5.0"
- setimmediate "^1.0.5"
- string-hash-64 "^1.0.3"
react-native-root-siblings@^4.1.1:
version "4.1.1"
@@ -14801,9 +15330,9 @@ react-native-root-siblings@^4.1.1:
integrity sha512-sdmLElNs5PDWqmZmj4/aNH4anyxreaPm61c4ZkRiR8SO/GzLg6KjAbb0e17RmMdnBdD0AIQbS38h/l55YKN4ZA==
react-native-safe-area-context@^4.4.1:
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.5.0.tgz#9208313236e8f49e1920ac1e2a2c975f03aed284"
- integrity sha512-0WORnk9SkREGUg2V7jHZbuN5x4vcxj/1B0QOcXJjdYWrzZHgLcUzYWWIUecUPJh747Mwjt/42RZDOaFn3L8kPQ==
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.5.3.tgz#e98eb1a73a6b3846d296545fe74760754dbaaa69"
+ integrity sha512-ihYeGDEBSkYH+1aWnadNhVtclhppVgd/c0tm4mj0+HV11FoiWJ8N6ocnnZnRLvM5Fxc+hUqxR9bm5AXU3rXiyA==
react-native-screens@^3.13.1:
version "3.20.0"
@@ -14861,10 +15390,10 @@ react-native-web@^0.18.11:
postcss-value-parser "^4.2.0"
styleq "^0.1.2"
-react-native@0.71.6:
- version "0.71.6"
- resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.6.tgz#e8f07baf55abd1015eaa7040ceaa4aa632c2c04f"
- integrity sha512-gHrDj7qaAaiE41JwaFCh3AtvOqOLuRgZtHKzNiwxakG/wvPAYmG73ECfWHGxjxIx/QT17Hp37Da3ipCei/CayQ==
+react-native@0.71.8:
+ version "0.71.8"
+ resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.8.tgz#4314145341c49448cf7465b93ced52a433a5e191"
+ integrity sha512-ftMAuhpgTkbHU9brrqsEyxcNrpYvXKeATY+if22Nfhhg1zW+6wn95w9otwTnA3xHkljPCbng8mUhmmERjGEl7g==
dependencies:
"@jest/create-cache-key-function" "^29.2.1"
"@react-native-community/cli" "10.2.2"
@@ -14891,7 +15420,7 @@ react-native@0.71.6:
promise "^8.3.0"
react-devtools-core "^4.26.1"
react-native-codegen "^0.71.5"
- react-native-gradle-plugin "^0.71.17"
+ react-native-gradle-plugin "^0.71.18"
react-refresh "^0.4.0"
react-shallow-renderer "^16.15.0"
regenerator-runtime "^0.13.2"
@@ -15007,7 +15536,14 @@ read-cache@^1.0.0:
dependencies:
pify "^2.3.0"
-readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@~2.3.6:
+read-env@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/read-env/-/read-env-1.3.0.tgz#e26e1e446992b3216e9a3c6f6ac51064fe91fdff"
+ integrity sha512-DbCgZ8oHwZreK/E2E27RGk3EUPapMhYGSGIt02k9sX6R3tCFc4u4tkltKvkCvzEQ3SOLUaiYHAnGb+TdsnPp0A==
+ dependencies:
+ camelcase "5.0.0"
+
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@~2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
@@ -15030,9 +15566,9 @@ readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable
util-deprecate "^1.0.1"
readable-stream@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.3.0.tgz#0914d0c72db03b316c9733bb3461d64a3cc50cba"
- integrity sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.4.0.tgz#55ce132d60a988c460d75c631e9ccf6a7229b468"
+ integrity sha512-kDMOq0qLtxV9f/SQv522h8cxZBqNZXuXNyjyezmfAAuribMyVXziljpQ/uQhfE1XLg2/TLTW2DsnoE4VAi/krg==
dependencies:
abort-controller "^3.0.0"
buffer "^6.0.3"
@@ -15132,14 +15668,14 @@ regex-parser@^2.2.11:
resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58"
integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==
-regexp.prototype.flags@^1.4.3:
- version "1.4.3"
- resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
- integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
+regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb"
+ integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
- functions-have-names "^1.2.2"
+ define-properties "^1.2.0"
+ functions-have-names "^1.2.3"
regexpu-core@^5.3.1:
version "5.3.2"
@@ -15241,9 +15777,9 @@ requires-port@^1.0.0:
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
reselect@^4.0.0, reselect@^4.1.7:
- version "4.1.7"
- resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.7.tgz#56480d9ff3d3188970ee2b76527bd94a95567a42"
- integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==
+ version "4.1.8"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524"
+ integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==
resolve-cwd@^3.0.0:
version "3.0.0"
@@ -15289,16 +15825,16 @@ resolve.exports@^1.1.0:
integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==
resolve.exports@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.1.tgz#cee884cd4e3f355660e501fa3276b27d7ffe5a20"
- integrity sha512-OEJWVeimw8mgQuj3HfkNl4KqRevH7lzeQNaWRPfx0PPse7Jk6ozcsG4FKVgtzDsC1KUF+YlTHh17NcgHOPykLw==
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800"
+ integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==
-resolve@^1.1.7, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.1:
- version "1.22.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
- integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+resolve@^1.1.7, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.2:
+ version "1.22.2"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f"
+ integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
dependencies:
- is-core-module "^2.9.0"
+ is-core-module "^2.11.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
@@ -15401,9 +15937,9 @@ rn-fetch-blob@^0.12.0:
glob "7.0.6"
roarr@^7.0.4:
- version "7.14.3"
- resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.14.3.tgz#ff163bf9488222f327ee65cdee18018e790eb645"
- integrity sha512-AvUQY27C6/biXEAyYUXc8ONBtP1cA3MQM88e24Fmsl3LAqtNR309nMaWFALYk7ORTqgGrgrjBJ1vE20DZAc5qA==
+ version "7.15.0"
+ resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.0.tgz#09b792f0cd31b4a7f91030bb1c47550ceec98ee4"
+ integrity sha512-CV9WefQfUXTX6wr8CrEMhfNef3sjIt9wNhE/5PNu4tNWsaoDNDXqq+OGn/RW9A1UPb0qc7FQlswXRaJJJsqn8A==
dependencies:
boolean "^3.1.4"
fast-json-stringify "^2.7.10"
@@ -15430,15 +15966,20 @@ rollup@^2.43.1:
fsevents "~2.3.2"
rope-sequence@^1.3.0:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.3.tgz#3f67fc106288b84b71532b4a5fd9d4881e4457f0"
- integrity sha512-85aZYCxweiD5J8yTEbw+E6A27zSnLPNDL0WfPdw3YYodq7WjnTKo0q4dtyQ2gz23iPT8Q9CUyJtAaUNcTxRf5Q==
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
+ integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
rtl-detect@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.4.tgz#40ae0ea7302a150b96bc75af7d749607392ecac6"
integrity sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ==
+run-async@^2.2.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
+ integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
+
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@@ -15446,13 +15987,30 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
+rxjs@^6.4.0:
+ version "6.6.7"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+ integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+ dependencies:
+ tslib "^1.9.0"
+
rxjs@^7.5.2:
- version "7.8.0"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
- integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
+ version "7.8.1"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
+ integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"
+safe-array-concat@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060"
+ integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.2.0"
+ has-symbols "^1.0.3"
+ isarray "^2.0.5"
+
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -15558,24 +16116,24 @@ schema-utils@^2.6.5:
ajv "^6.12.4"
ajv-keywords "^3.5.2"
-schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
- integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
+schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99"
+ integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==
dependencies:
"@types/json-schema" "^7.0.8"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
schema-utils@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7"
- integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.1.tgz#eb2d042df8b01f4b5c276a2dfd41ba0faab72e8d"
+ integrity sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==
dependencies:
"@types/json-schema" "^7.0.9"
- ajv "^8.8.0"
+ ajv "^8.9.0"
ajv-formats "^2.1.1"
- ajv-keywords "^5.0.0"
+ ajv-keywords "^5.1.0"
select-hose@^2.0.0:
version "2.0.0"
@@ -15599,7 +16157,7 @@ semver@7.3.2:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
-semver@7.3.8, semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@~7.3.2:
+semver@7.3.8, semver@~7.3.2:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
@@ -15616,6 +16174,13 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8:
+ version "7.5.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec"
+ integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==
+ dependencies:
+ lru-cache "^6.0.0"
+
send@0.18.0, send@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
@@ -15635,6 +16200,19 @@ send@0.18.0, send@^0.18.0:
range-parser "~1.2.1"
statuses "2.0.1"
+sentry-expo@~6.1.0:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/sentry-expo/-/sentry-expo-6.1.1.tgz#e5cb74523ef09b7cdc185ba696333c69d87ed703"
+ integrity sha512-eNrWvvDY/Z6Yba+jjjYWX6s5Qk3jzCaSAs8I6EkXUFiXqEi3ONJ+LKanf9Wuy0pjjtWtxHyPHTrb+93Kn0cVmg==
+ dependencies:
+ "@expo/spawn-async" "^1.7.0"
+ "@sentry/integrations" "7.29.0"
+ "@sentry/react" "7.29.0"
+ "@sentry/react-native" "4.13.0"
+ "@sentry/types" "7.29.0"
+ mkdirp "^1.0.4"
+ rimraf "^3.0.2"
+
serialize-error@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-6.0.0.tgz#ccfb887a1dd1c48d6d52d7863b92544331fd752b"
@@ -15691,7 +16269,7 @@ serve-static@1.15.0, serve-static@^1.13.1:
parseurl "~1.3.3"
send "0.18.0"
-set-blocking@^2.0.0:
+set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
@@ -15776,11 +16354,16 @@ shell-quote@1.7.3:
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
-shell-quote@^1.6.1, shell-quote@^1.7.2, shell-quote@^1.7.3:
+shell-quote@1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba"
integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==
+shell-quote@^1.6.1, shell-quote@^1.7.2, shell-quote@^1.7.3:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
+ integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
+
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
@@ -15846,9 +16429,9 @@ slash@^4.0.0:
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
slash@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/slash/-/slash-5.0.0.tgz#8c18a871096b71ee0e002976a4fe3374991c3074"
- integrity sha512-n6KkmvKS0623igEVj3FF0OZs1gYYJ0o0Hj939yc1fyxl2xt+xYpLnzJB6xBSqOfV9ZFLEWodBBN/heZJahuIJQ==
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce"
+ integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==
slice-ansi@^2.0.0:
version "2.1.0"
@@ -15860,9 +16443,9 @@ slice-ansi@^2.0.0:
is-fullwidth-code-point "^2.0.0"
slugify@^1.3.4:
- version "1.6.5"
- resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.5.tgz#c8f5c072bf2135b80703589b39a3d41451fbe8c8"
- integrity sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ==
+ version "1.6.6"
+ resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b"
+ integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==
snapdragon-node@^2.0.1:
version "2.1.1"
@@ -15904,9 +16487,9 @@ sockjs@^0.3.24:
websocket-driver "^0.7.4"
sonic-boom@^3.1.0:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.1.tgz#972ceab831b5840a08a002fa95a672008bda1c38"
- integrity sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A==
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c"
+ integrity sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==
dependencies:
atomic-sleep "^1.0.0"
@@ -16029,9 +16612,9 @@ split-string@^3.0.1, split-string@^3.0.2:
extend-shallow "^3.0.0"
split2@^4.0.0, split2@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809"
- integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
+ integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
split@^1.0.1:
version "1.0.1"
@@ -16112,9 +16695,9 @@ stream-chain@^2.2.5:
integrity sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==
stream-json@^1.7.4:
- version "1.7.5"
- resolved "https://registry.yarnpkg.com/stream-json/-/stream-json-1.7.5.tgz#2ff0563011f22cea4f6a28dbfc0344a53c761fe4"
- integrity sha512-NSkoVduGakxZ8a+pTPUlcGEeAGQpWL9rKJhOFCV+J/QtdQUEU5vtBgVg6eJXn8JB8RZvpbJWZGvXkhz70MLWoA==
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/stream-json/-/stream-json-1.8.0.tgz#53f486b2e3b4496c506131f8d7260ba42def151c"
+ integrity sha512-HZfXngYHUAr1exT4fxlbc1IOce1RYxp2ldeaf97LYCOPSoOqY/1Psp7iGvpb+6JIOgkra9zDYnPX01hGAHzEPw==
dependencies:
stream-chain "^2.2.5"
@@ -16123,11 +16706,6 @@ strict-uri-encode@^2.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==
-string-hash-64@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322"
- integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==
-
string-length@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@@ -16154,7 +16732,16 @@ string-similarity@^4.0.1:
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+string-width@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -16163,6 +16750,14 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
+string-width@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3"
@@ -16227,7 +16822,21 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-strip-ansi@^5.0.0, strip-ansi@^5.2.0:
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
@@ -16242,9 +16851,9 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
- integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
+ integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
dependencies:
ansi-regex "^6.0.1"
@@ -16309,9 +16918,9 @@ structured-headers@^0.4.1:
integrity sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==
style-loader@^3.3.1:
- version "3.3.2"
- resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.2.tgz#eaebca714d9e462c19aa1e3599057bc363924899"
- integrity sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.3.tgz#bba8daac19930169c0c9c96706749a597ae3acff"
+ integrity sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==
stylehacks@^5.1.1:
version "5.1.1"
@@ -16326,11 +16935,12 @@ styleq@^0.1.2:
resolved "https://registry.yarnpkg.com/styleq/-/styleq-0.1.3.tgz#8efb2892debd51ce7b31dc09c227ad920decab71"
integrity sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==
-sucrase@^3.20.0:
- version "3.29.0"
- resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.29.0.tgz#3207c5bc1b980fdae1e539df3f8a8a518236da7d"
- integrity sha512-bZPAuGA5SdFHuzqIhTAqt9fvNEo9rESqXIG3oiKdF8K4UmkQxC4KlNL3lVyAErXp+mPvUqZ5l13qx6TrDIGf3A==
+sucrase@^3.20.0, sucrase@^3.32.0:
+ version "3.32.0"
+ resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.32.0.tgz#c4a95e0f1e18b6847127258a75cf360bc568d4a7"
+ integrity sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==
dependencies:
+ "@jridgewell/gen-mapping" "^0.3.2"
commander "^4.0.0"
glob "7.1.6"
lines-and-columns "^1.1.6"
@@ -16430,33 +17040,33 @@ symbol-tree@^3.2.4:
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
tailwindcss@^3.0.2:
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.7.tgz#5936dd08c250b05180f0944500c01dce19188c07"
- integrity sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.2.tgz#2f9e35d715fdf0bbf674d90147a0684d7054a2d3"
+ integrity sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==
dependencies:
+ "@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
chokidar "^3.5.3"
- color-name "^1.1.4"
- detective "^5.2.1"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.12"
glob-parent "^6.0.2"
is-glob "^4.0.3"
- lilconfig "^2.0.6"
+ jiti "^1.18.2"
+ lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
- postcss "^8.0.9"
- postcss-import "^14.1.0"
- postcss-js "^4.0.0"
- postcss-load-config "^3.1.4"
- postcss-nested "6.0.0"
+ postcss "^8.4.23"
+ postcss-import "^15.1.0"
+ postcss-js "^4.0.1"
+ postcss-load-config "^4.0.1"
+ postcss-nested "^6.0.1"
postcss-selector-parser "^6.0.11"
postcss-value-parser "^4.2.0"
- quick-lru "^5.1.1"
- resolve "^1.22.1"
+ resolve "^1.22.2"
+ sucrase "^3.32.0"
tapable@^1.0.0:
version "1.1.3"
@@ -16490,13 +17100,13 @@ tar-stream@^2.1.4:
readable-stream "^3.1.1"
tar@^6.0.2, tar@^6.0.5:
- version "6.1.13"
- resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b"
- integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==
+ version "6.1.15"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.15.tgz#c9738b0b98845a3b344d334b8fa3041aaba53a69"
+ integrity sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
- minipass "^4.0.0"
+ minipass "^5.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
@@ -16579,21 +17189,21 @@ terminal-link@^2.0.0, terminal-link@^2.1.1:
ansi-escapes "^4.2.1"
supports-hyperlinks "^2.0.0"
-terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.0:
- version "5.3.7"
- resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz#ef760632d24991760f339fe9290deb936ad1ffc7"
- integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==
+terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.0, terser-webpack-plugin@^5.3.7:
+ version "5.3.9"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1"
+ integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==
dependencies:
"@jridgewell/trace-mapping" "^0.3.17"
jest-worker "^27.4.5"
schema-utils "^3.1.1"
serialize-javascript "^6.0.1"
- terser "^5.16.5"
+ terser "^5.16.8"
-terser@^5.0.0, terser@^5.10.0, terser@^5.15.0, terser@^5.16.5:
- version "5.16.6"
- resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.6.tgz#f6c7a14a378ee0630fbe3ac8d1f41b4681109533"
- integrity sha512-IBZ+ZQIA9sMaXmRZCUMDjNH0D5AQQfdn4WUjHL0+1lF4TP1IHRJbrhb6fNaXWikrYQTSkb7SLxkeXAiy1p7mbg==
+terser@^5.0.0, terser@^5.10.0, terser@^5.15.0, terser@^5.16.8:
+ version "5.17.6"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.6.tgz#d810e75e1bb3350c799cd90ebefe19c9412c12de"
+ integrity sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ==
dependencies:
"@jridgewell/source-map" "^0.3.2"
acorn "^8.5.0"
@@ -16658,7 +17268,7 @@ through2@^2.0.1:
readable-stream "~2.3.6"
xtend "~4.0.1"
-through@2:
+through@2, through@^2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
@@ -16789,12 +17399,11 @@ tr46@~0.0.3:
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
trace-event-lib@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/trace-event-lib/-/trace-event-lib-1.3.1.tgz#8113146caa30778f45d0ec479d899f9eda94d594"
- integrity sha512-RO/TD5E9RNqU6MhOfi/njFWKYhrzOJCpRXlEQHgXwM+6boLSrQnOZ9xbHwOXzC+Luyixc7LNNSiTsqTVeF7I1g==
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/trace-event-lib/-/trace-event-lib-1.4.1.tgz#a749b8141650f56dcdecea760df4735f28d1ac6b"
+ integrity sha512-TOgFolKG8JFY+9d5EohGWMvwvteRafcyfPWWNIqcuD1W/FUvxWcy2MSCZ/beYHM63oYPHYHCd3tkbgCctHVP7w==
dependencies:
browser-process-hrtime "^1.0.0"
- lodash "^4.17.21"
traverse@~0.6.6:
version "0.6.7"
@@ -16847,15 +17456,15 @@ tsconfig-paths@^3.14.1:
minimist "^1.2.6"
strip-bom "^3.0.0"
-tslib@^1.8.1:
+tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
- integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338"
+ integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==
tsutils@^3.21.0:
version "3.21.0"
@@ -16920,15 +17529,15 @@ type-fest@^0.7.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48"
integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==
-type-fest@^2.0.0, type-fest@^2.3.3:
+type-fest@^2.19.0, type-fest@^2.3.3:
version "2.19.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
type-fest@^3.0.0:
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.6.1.tgz#cf8025edeebfd6cf48de73573a5e1423350b9993"
- integrity sha512-htXWckxlT6U4+ilVgweNliPqlsVSSucbxVexRYllyMVJDtf5rTjv6kF/s+qAd4QSL1BZcnJPEJavYBPQiWuZDA==
+ version "3.11.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.11.1.tgz#d8e62c7f42e14537d5b8796de5450d541f3a33a7"
+ integrity sha512-aCuRNRERRVh33lgQaJRlUxZqzfhzwTrsE98Mc3o3VXqmiaQdHacgUtJ0esp+7MvZ92qhtzKPeusaX6vIEcoreA==
type-is@~1.6.18:
version "1.6.18"
@@ -16954,7 +17563,7 @@ typed-emitter@^2.1.0:
optionalDependencies:
rxjs "^7.5.2"
-typedarray-to-buffer@^3.1.5:
+typedarray-to-buffer@^3.1.2, typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
@@ -16966,10 +17575,10 @@ typescript@^4.4.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
-ua-parser-js@^0.7.30:
- version "0.7.34"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.34.tgz#afb439e2e3e394bdc90080acb661a39c685b67d7"
- integrity sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ==
+ua-parser-js@^0.7.30, 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"
+ integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
@@ -17120,10 +17729,10 @@ upath@^1.2.0:
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
-update-browserslist-db@^1.0.10:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
- integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
+update-browserslist-db@^1.0.11:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
+ integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"
@@ -17171,9 +17780,9 @@ url-parse@^1.5.3, url-parse@^1.5.9:
requires-port "^1.0.0"
use-latest-callback@^0.1.5:
- version "0.1.5"
- resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51"
- integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ==
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.6.tgz#3fa6e7babbb5f9bfa24b5094b22939e1e92ebcf6"
+ integrity sha512-VO/P91A/PmKH9bcN9a7O3duSuxe6M14ZoYXgA6a8dab8doWNdhiIHzEkX/jFeTTRBsX0Ubk6nG4q2NIjNsj+bg==
use-sync-external-store@^1.0.0:
version "1.2.0"
@@ -17298,9 +17907,9 @@ w3c-hr-time@^1.0.2:
browser-process-hrtime "^1.0.0"
w3c-keyname@^2.2.0:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f"
- integrity sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.7.tgz#e29549e9ac97ac5cb2993c8222994e97922ef377"
+ integrity sha512-XB8aa62d4rrVfoZYQaYNy3fy+z4nrfy2ooea3/0BnBzXW0tSdZ+lRgjzBZhk0La0H6h8fVyYCxx/qkQcAIuvfg==
w3c-xmlserializer@^2.0.0:
version "2.0.0"
@@ -17376,16 +17985,16 @@ webidl-conversions@^7.0.0:
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
webpack-cli@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.0.1.tgz#95fc0495ac4065e9423a722dec9175560b6f2d9a"
- integrity sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A==
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.1.tgz#c211ac6d911e77c512978f7132f0d735d4a97ace"
+ integrity sha512-OLJwVMoXnXYH2ncNGU8gxVpUtm3ybvdioiTvHgUyBuyMLKiVvWy+QObzBsMtp5pH7qQoEuWgeEUQ/sU3ZJFzAw==
dependencies:
"@discoveryjs/json-ext" "^0.5.0"
- "@webpack-cli/configtest" "^2.0.1"
+ "@webpack-cli/configtest" "^2.1.0"
"@webpack-cli/info" "^2.0.1"
- "@webpack-cli/serve" "^2.0.1"
+ "@webpack-cli/serve" "^2.0.4"
colorette "^2.0.14"
- commander "^9.4.1"
+ commander "^10.0.1"
cross-spawn "^7.0.3"
envinfo "^7.7.3"
fastest-levenshtein "^1.0.12"
@@ -17406,9 +18015,9 @@ webpack-dev-middleware@^5.3.1:
schema-utils "^4.0.0"
webpack-dev-server@^4.11.1, webpack-dev-server@^4.6.0:
- version "4.13.1"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.13.1.tgz#6417a9b5d2f528e7644b68d6ed335e392dccffe8"
- integrity sha512-5tWg00bnWbYgkN+pd5yISQKDejRBYGEw15RaEEslH+zdbNDxxaZvEAO2WulaSaFKb5n3YG8JXsGaDsut1D0xdA==
+ version "4.15.0"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.0.tgz#87ba9006eca53c551607ea0d663f4ae88be7af21"
+ integrity sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ==
dependencies:
"@types/bonjour" "^3.5.9"
"@types/connect-history-api-fallback" "^1.3.5"
@@ -17450,9 +18059,9 @@ webpack-manifest-plugin@^4.0.2, webpack-manifest-plugin@^4.1.1:
webpack-sources "^2.2.0"
webpack-merge@^5.7.3:
- version "5.8.0"
- resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61"
- integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==
+ version "5.9.0"
+ resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826"
+ integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==
dependencies:
clone-deep "^4.0.1"
wildcard "^2.0.0"
@@ -17479,21 +18088,21 @@ webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.64.4, webpack@^5.75.0:
- version "5.76.2"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.2.tgz#6f80d1c1d1e3bf704db571b2504a0461fac80230"
- integrity sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==
+ version "5.84.1"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.84.1.tgz#d4493acdeca46b26ffc99d86d784cabfeb925a15"
+ integrity sha512-ZP4qaZ7vVn/K8WN/p990SGATmrL1qg4heP/MrVneczYtpDGJWlrgZv55vxaV2ul885Kz+25MP2kSXkPe3LZfmg==
dependencies:
"@types/eslint-scope" "^3.7.3"
- "@types/estree" "^0.0.51"
- "@webassemblyjs/ast" "1.11.1"
- "@webassemblyjs/wasm-edit" "1.11.1"
- "@webassemblyjs/wasm-parser" "1.11.1"
+ "@types/estree" "^1.0.0"
+ "@webassemblyjs/ast" "^1.11.5"
+ "@webassemblyjs/wasm-edit" "^1.11.5"
+ "@webassemblyjs/wasm-parser" "^1.11.5"
acorn "^8.7.1"
- acorn-import-assertions "^1.7.6"
+ acorn-import-assertions "^1.9.0"
browserslist "^4.14.5"
chrome-trace-event "^1.0.2"
- enhanced-resolve "^5.10.0"
- es-module-lexer "^0.9.0"
+ enhanced-resolve "^5.14.1"
+ es-module-lexer "^1.2.1"
eslint-scope "5.1.1"
events "^3.2.0"
glob-to-regexp "^0.4.1"
@@ -17502,9 +18111,9 @@ webpack@^5.64.4, webpack@^5.75.0:
loader-runner "^4.2.0"
mime-types "^2.1.27"
neo-async "^2.6.2"
- schema-utils "^3.1.0"
+ schema-utils "^3.1.2"
tapable "^2.1.1"
- terser-webpack-plugin "^5.1.3"
+ terser-webpack-plugin "^5.3.7"
watchpack "^2.4.0"
webpack-sources "^3.2.3"
@@ -17616,9 +18225,9 @@ which-collection@^1.0.1:
is-weakset "^2.0.1"
which-module@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
- integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
+ integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
which-typed-array@^1.1.9:
version "1.1.9"
@@ -17639,27 +18248,34 @@ which@^1.2.9, which@^1.3.1:
dependencies:
isexe "^2.0.0"
-which@^2.0.1:
+which@^2.0.1, which@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
+wide-align@^1.1.0:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
+ integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
+ dependencies:
+ string-width "^1.0.2 || 2 || 3 || 4"
+
wildcard@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
- integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
+ integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
wonka@^4.0.14:
version "4.0.15"
resolved "https://registry.yarnpkg.com/wonka/-/wonka-4.0.15.tgz#9aa42046efa424565ab8f8f451fcca955bf80b89"
integrity sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg==
-wonka@^6.1.2:
- version "6.2.5"
- resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.2.5.tgz#26e54a6827b96a6164b845106f4d925ede4089bb"
- integrity sha512-adhGYKm5xWIZYXRkzEqHbRbRl2gXHqOudjQJMXpRgSyboFmaKOjGm3RIThBk4tZdiZx1DXuKK0H9wKBgXHhzZg==
+wonka@^6.3.2:
+ version "6.3.2"
+ resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.3.2.tgz#6f32992b332251d7b696b038990f4dc284b3b33d"
+ integrity sha512-2xXbQ1LnwNS7egVm1HPhW2FyKrekolzhpM3mCwXdQr55gO+tAiY76rhb32OL9kKsW8taj++iP7C6hxlVzbnvrw==
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
@@ -17671,25 +18287,25 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
-workbox-background-sync@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz#3141afba3cc8aa2ae14c24d0f6811374ba8ff6a9"
- integrity sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==
+workbox-background-sync@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f"
+ integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg==
dependencies:
idb "^7.0.1"
- workbox-core "6.5.4"
+ workbox-core "6.6.1"
-workbox-broadcast-update@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz#8441cff5417cd41f384ba7633ca960a7ffe40f66"
- integrity sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==
+workbox-broadcast-update@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e"
+ integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ==
dependencies:
- workbox-core "6.5.4"
+ workbox-core "6.6.1"
-workbox-build@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.5.4.tgz#7d06d31eb28a878817e1c991c05c5b93409f0389"
- integrity sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==
+workbox-build@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0"
+ integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw==
dependencies:
"@apideck/better-ajv-errors" "^0.3.1"
"@babel/core" "^7.11.1"
@@ -17713,132 +18329,132 @@ workbox-build@6.5.4:
strip-comments "^2.0.1"
tempy "^0.6.0"
upath "^1.2.0"
- workbox-background-sync "6.5.4"
- workbox-broadcast-update "6.5.4"
- workbox-cacheable-response "6.5.4"
- workbox-core "6.5.4"
- workbox-expiration "6.5.4"
- workbox-google-analytics "6.5.4"
- workbox-navigation-preload "6.5.4"
- workbox-precaching "6.5.4"
- workbox-range-requests "6.5.4"
- workbox-recipes "6.5.4"
- workbox-routing "6.5.4"
- workbox-strategies "6.5.4"
- workbox-streams "6.5.4"
- workbox-sw "6.5.4"
- workbox-window "6.5.4"
+ workbox-background-sync "6.6.1"
+ workbox-broadcast-update "6.6.1"
+ workbox-cacheable-response "6.6.1"
+ workbox-core "6.6.1"
+ workbox-expiration "6.6.1"
+ workbox-google-analytics "6.6.1"
+ workbox-navigation-preload "6.6.1"
+ workbox-precaching "6.6.1"
+ workbox-range-requests "6.6.1"
+ workbox-recipes "6.6.1"
+ workbox-routing "6.6.1"
+ workbox-strategies "6.6.1"
+ workbox-streams "6.6.1"
+ workbox-sw "6.6.1"
+ workbox-window "6.6.1"
-workbox-cacheable-response@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz#a5c6ec0c6e2b6f037379198d4ef07d098f7cf137"
- integrity sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==
+workbox-cacheable-response@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9"
+ integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag==
dependencies:
- workbox-core "6.5.4"
+ workbox-core "6.6.1"
-workbox-core@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.5.4.tgz#df48bf44cd58bb1d1726c49b883fb1dffa24c9ba"
- integrity sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==
+workbox-core@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265"
+ integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw==
-workbox-expiration@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.5.4.tgz#501056f81e87e1d296c76570bb483ce5e29b4539"
- integrity sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==
+workbox-expiration@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739"
+ integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A==
dependencies:
idb "^7.0.1"
- workbox-core "6.5.4"
+ workbox-core "6.6.1"
-workbox-google-analytics@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz#c74327f80dfa4c1954cbba93cd7ea640fe7ece7d"
- integrity sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==
+workbox-google-analytics@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d"
+ integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA==
dependencies:
- workbox-background-sync "6.5.4"
- workbox-core "6.5.4"
- workbox-routing "6.5.4"
- workbox-strategies "6.5.4"
+ workbox-background-sync "6.6.1"
+ workbox-core "6.6.1"
+ workbox-routing "6.6.1"
+ workbox-strategies "6.6.1"
-workbox-navigation-preload@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz#ede56dd5f6fc9e860a7e45b2c1a8f87c1c793212"
- integrity sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==
+workbox-navigation-preload@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059"
+ integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA==
dependencies:
- workbox-core "6.5.4"
+ workbox-core "6.6.1"
-workbox-precaching@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.5.4.tgz#740e3561df92c6726ab5f7471e6aac89582cab72"
- integrity sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==
+workbox-precaching@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2"
+ integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A==
dependencies:
- workbox-core "6.5.4"
- workbox-routing "6.5.4"
- workbox-strategies "6.5.4"
+ workbox-core "6.6.1"
+ workbox-routing "6.6.1"
+ workbox-strategies "6.6.1"
-workbox-range-requests@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz#86b3d482e090433dab38d36ae031b2bb0bd74399"
- integrity sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==
+workbox-range-requests@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39"
+ integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g==
dependencies:
- workbox-core "6.5.4"
+ workbox-core "6.6.1"
-workbox-recipes@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.5.4.tgz#cca809ee63b98b158b2702dcfb741b5cc3e24acb"
- integrity sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==
+workbox-recipes@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae"
+ integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g==
dependencies:
- workbox-cacheable-response "6.5.4"
- workbox-core "6.5.4"
- workbox-expiration "6.5.4"
- workbox-precaching "6.5.4"
- workbox-routing "6.5.4"
- workbox-strategies "6.5.4"
+ workbox-cacheable-response "6.6.1"
+ workbox-core "6.6.1"
+ workbox-expiration "6.6.1"
+ workbox-precaching "6.6.1"
+ workbox-routing "6.6.1"
+ workbox-strategies "6.6.1"
-workbox-routing@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.5.4.tgz#6a7fbbd23f4ac801038d9a0298bc907ee26fe3da"
- integrity sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==
+workbox-routing@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581"
+ integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg==
dependencies:
- workbox-core "6.5.4"
+ workbox-core "6.6.1"
-workbox-strategies@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.5.4.tgz#4edda035b3c010fc7f6152918370699334cd204d"
- integrity sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==
+workbox-strategies@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.6.1.tgz#38d0f0fbdddba97bd92e0c6418d0b1a2ccd5b8bf"
+ integrity sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw==
dependencies:
- workbox-core "6.5.4"
+ workbox-core "6.6.1"
-workbox-streams@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.5.4.tgz#1cb3c168a6101df7b5269d0353c19e36668d7d69"
- integrity sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==
+workbox-streams@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26"
+ integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q==
dependencies:
- workbox-core "6.5.4"
- workbox-routing "6.5.4"
+ workbox-core "6.6.1"
+ workbox-routing "6.6.1"
-workbox-sw@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.5.4.tgz#d93e9c67924dd153a61367a4656ff4d2ae2ed736"
- integrity sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==
+workbox-sw@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c"
+ integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ==
workbox-webpack-plugin@^6.4.1:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.4.tgz#baf2d3f4b8f435f3469887cf4fba2b7fac3d0fd7"
- integrity sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531"
+ integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA==
dependencies:
fast-json-stable-stringify "^2.1.0"
pretty-bytes "^5.4.1"
upath "^1.2.0"
webpack-sources "^1.4.3"
- workbox-build "6.5.4"
+ workbox-build "6.6.1"
-workbox-window@6.5.4:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.5.4.tgz#d991bc0a94dff3c2dbb6b84558cff155ca878e91"
- integrity sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==
+workbox-window@6.6.1:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e"
+ integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ==
dependencies:
"@types/trusted-types" "^2.0.2"
- workbox-core "6.5.4"
+ workbox-core "6.6.1"
wrap-ansi@^6.2.0:
version "6.2.0"
@@ -17902,12 +18518,12 @@ ws@^7, ws@^7.0.0, ws@^7.4.6, ws@^7.5.1:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
-ws@^8.11.0, ws@^8.12.1, ws@^8.13.0:
+ws@^8.11.0, ws@^8.12.0, ws@^8.12.1, ws@^8.13.0:
version "8.13.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
-xcode@^3.0.0, xcode@^3.0.1:
+xcode@3.0.1, xcode@^3.0.0, xcode@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/xcode/-/xcode-3.0.1.tgz#3efb62aac641ab2c702458f9a0302696146aa53c"
integrity sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==
@@ -17965,7 +18581,7 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
-xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1:
+xtend@^4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
@@ -18000,6 +18616,11 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+yaml@^2.1.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
+ integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
+
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
@@ -18008,12 +18629,12 @@ yargs-parser@^18.1.2:
camelcase "^5.0.0"
decamelize "^1.2.0"
-yargs-parser@^20.2.2, yargs-parser@^20.2.9:
+yargs-parser@^20.2.2:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-yargs-parser@^21.1.1:
+yargs-parser@^21.0.0, yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
@@ -18045,7 +18666,7 @@ yargs@^15.1.0:
y18n "^4.0.0"
yargs-parser "^18.1.2"
-yargs@^16.0.3, yargs@^16.2.0:
+yargs@^16.2.0:
version "16.2.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
@@ -18058,10 +18679,10 @@ yargs@^16.0.3, yargs@^16.2.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
-yargs@^17.3.1, yargs@^17.5.1:
- version "17.7.1"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967"
- integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==
+yargs@^17.0.0, yargs@^17.3.1, yargs@^17.5.1:
+ version "17.7.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+ integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
dependencies:
cliui "^8.0.1"
escalade "^3.1.1"