Compare commits

..

51 Commits

Author SHA1 Message Date
Eric Bailey d5e0653f62 WIP 2024-04-11 21:15:41 -05:00
Eric Bailey 73fec7fb3c Cleanups 2024-04-11 19:29:26 -05:00
Eric Bailey e07269cca2 Double check and clarify persist handling 2024-04-11 17:04:19 -05:00
Eric Bailey c6e77961fc Re-organize for better clarity 2024-04-11 16:58:08 -05:00
Eric Bailey 9721bbaeb3 Cleanup, feedback 2024-04-11 16:25:00 -05:00
Eric Bailey fd085fd437 Remove todo 2024-04-09 10:56:48 -05:00
Eric Bailey 0d88a212ee Memoize persist handler 2024-04-09 10:56:48 -05:00
Eric Bailey d9d9032456 Remove logs, add user alerts, add TODO 2024-04-09 10:56:48 -05:00
Eric Bailey 1f9679b920 Handle thrown errors much better 2024-04-09 10:56:48 -05:00
Eric Bailey 896e67120c Remove too-strong fallback 2024-04-09 10:56:48 -05:00
Eric Bailey d0ffb90fb3 Pare back persisted state to clarify source of truth 2024-04-09 10:56:48 -05:00
Eric Bailey 5be41c967b Add back local web debug, useful 2024-04-09 10:56:48 -05:00
Eric Bailey dfb0aae97a Better handle init failure 2024-04-09 10:56:48 -05:00
Eric Bailey fd001836ed Some comments 2024-04-09 10:56:48 -05:00
Eric Bailey cbac0a91cf Clean up persistSession 2024-04-09 10:56:48 -05:00
Eric Bailey ef7e2a981e Remove global state manipulation in favor of refreshing the agent 2024-04-09 10:56:47 -05:00
Eric Bailey b8ecb06322 Use resumeSession to get email confirmation data 2024-04-09 10:56:03 -05:00
Eric Bailey 57c2d61931 Use BskyAgent as the source of truth 2024-04-09 10:56:03 -05:00
Eric Bailey 5d9e48308d Remove redundant call 2024-04-09 10:56:03 -05:00
Eric Bailey e4e5bfb4c9 Cleanup 2024-04-09 10:56:03 -05:00
Eric Bailey de370bd2cc Align on what happens when expired 2024-04-09 10:56:03 -05:00
Eric Bailey 0ba76deb0c Apply expired logic 2024-04-09 10:56:03 -05:00
Eric Bailey cd5b555b08 Strip back 2024-04-09 10:56:03 -05:00
Samuel Newman a49a5a351d Use ALF for the embed consent modal (#3336) 2024-04-09 00:58:18 +01:00
Eric Bailey 2bc20b1752 Onboarding tweaks (#3447)
* Remove feed

* Follow bsky.app
2024-04-08 14:32:00 -05:00
dan 6d3f9397f4 [Experiment] Ignore the persisted tab (#3442) 2024-04-08 18:39:32 +01:00
dan f03390e4b2 [Experiment] Show "Follow Back" label (#3439) 2024-04-08 18:38:59 +01:00
dan 887fedabea [Statsig] Track like/follow metadata (#3435)
* Track becoming mutuals

* Track poster/liker status

* Track post and followee clout

* Track follower and liker clout

* Extract utility
2024-04-08 18:38:51 +01:00
dan 8188f61e7d [Experiment] Turn off interval polling for Discover (#3431)
* Include feed URL in feed events

* [Experiment] Turn off polling interval for Discover
2024-04-08 17:21:44 +01:00
dan 1150007d37 Auto-expand follow suggestions on web (#3432) 2024-04-08 17:20:58 +01:00
dan ed5647d434 [Experiment] Disable min shell on foregrounding (#3430) 2024-04-08 17:20:41 +01:00
Hailey 731d94b973 Implement back button fix for all places in the app (#3434) 2024-04-08 09:09:40 -07:00
Hailey a1ac84687f Keep each deployment job in a separate concurrency group (#3446) 2024-04-08 08:54:50 -07:00
Hailey 00076ad1ff Bump async-storage to comply with App Store guidelines (#3436) 2024-04-08 08:24:59 -07:00
Hailey a6babaceaf adjust app.config.js to prevent development manifest error (#3413) 2024-04-05 13:29:54 -07:00
Hailey cd29dba761 fix dialog back button for android (#3428)
* fix types

* remove unused async

* add try/catch
2024-04-05 12:52:53 -07:00
Hailey c80dcc565e only run labelers for core team (#3426)
* use `pull_request_target` instead of `pull_request`

* adjust

* only run for core team
2024-04-05 08:58:28 -07:00
Hailey 443dfb5c3e prevent deployment race conditions (#3416) 2024-04-05 08:27:22 -07:00
dan 46c112edfd Enforce that text is wrapped in <Text>, remaining cases (#3421)
* Toggle.Button -> Toggle.ButtonWithText

* Simplify Prompt.Cancel/Action

* Move lines down for better diff

* Remove ButtonWithText

* Simplify types

* Enforce Button/ButtonText nesting

* Add suggested wrapper in linter error

* Check <Trans> ancestry too

* Also check literals

* Rm ts-ignore
2024-04-05 15:09:35 +01:00
dan 49266c355e Remove special cases for some buttons with text (#3412)
* Toggle.Button -> Toggle.ButtonWithText

* Simplify Prompt.Cancel/Action

* Move lines down for better diff

* Remove ButtonWithText

* Simplify types
2024-04-05 14:57:53 +01:00
Hailey 9b087b721d Lint and check types before building (#3424) 2024-04-04 23:15:59 -07:00
Hailey 40ccb574b8 properly import linear gradient (#3423) 2024-04-05 06:03:14 +01:00
Hailey 9bcb0a7a81 migrate to expo-linear-gradient (#3420)
* replace packages

* rm useless alias from webpack config

* fix accidental double commit

* replace all usages
2024-04-04 21:22:13 -07:00
Hailey 4e59914d2a migrate to 'expo-haptics' (#3418)
* migrate to 'expo-haptics'

* run yarn install twice if necessary
2024-04-04 21:19:38 -07:00
Hailey cddbbc4bd4 migrate to expo-application (#3417)
* migrate to `expo-application`

* replace additional usage
2024-04-04 21:19:28 -07:00
Hailey 0433f8ad68 migrate to expo-clipboard (#3419)
* replace package

* replace usages
2024-04-04 21:19:15 -07:00
Hailey 101d1589bf Bump some Expo package, remove unused packages to prepare for migration (#3415)
* bump some package versions

* remove completely unused packages

* remove blurview

* update yarn.lock
2024-04-04 18:30:32 -07:00
Samuel Newman 93b606e55a Use CSS keyframe animation for spinner on web (#3411)
* Use css keyframe animation for spinner on web

* duplicate css to index.html
2024-04-04 23:18:06 +01:00
dan 575f390d7b Fix false positive DEV check (#3410) 2024-04-04 22:54:18 +01:00
Hailey 2e048706e9 fix comments on prs (#3406)
* fix comments on prs

* remove labels

* remove test
2024-04-04 13:52:38 -07:00
dan 3915bb4316 Enforce Text suffix for Text-rendering components (#3407)
* Rm unused

* Add Text suffix to Title/Description

* Add Text suffix to text components

* Add Text suffix to props

* Validate Text components returns
2024-04-04 21:34:55 +01:00
131 changed files with 3070 additions and 2460 deletions
+6 -27
View File
@@ -1,5 +1,3 @@
const bskyEslint = require('./eslint')
module.exports = {
root: true,
extends: [
@@ -25,31 +23,12 @@ module.exports = {
'bsky-internal/avoid-unwrapped-text': [
'error',
{
impliedTextComponents: [
'Button', // TODO: Not always safe.
'ButtonText',
'DateField.Label',
'Description',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'InlineLink',
'Label',
'P',
'Prompt.Title',
'Prompt.Description',
'Prompt.Cancel', // TODO: Not always safe.
'Prompt.Action', // TODO: Not always safe.
'TextField.Label',
'TextField.Suffix',
'Title',
'Toggle.Label',
'ToggleButton.Button', // TODO: Not always safe.
],
impliedTextProps: ['FormContainer title'],
impliedTextComponents: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'],
impliedTextProps: [],
suggestedTextWrappers: {
Button: 'ButtonText',
'ToggleButton.Button': 'ToggleButton.ButtonText',
},
},
],
'simple-import-sort/imports': [
+39 -9
View File
@@ -22,6 +22,9 @@ jobs:
bundleDeploy:
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
cancel-in-progress: true
outputs:
fingerprint-is-different: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different }}
@@ -44,7 +47,18 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 100
fetch-depth: 0
- name: ⬇️ Get last successful deployment commit from the cache
id: get-base-commit
uses: actions/cache@v4
with:
path: last-successful-commit-hash.txt
key: last-successful-deployment-commit-${{ github.ref_name }}
- name: Add the last successful deployment commit to the output
id: last-successful-commit
run: echo base-commit=$(cat last-successful-commit-hash.txt) >> "$GITHUB_OUTPUT"
- name: ⬇️ Fetch commits from base branch
if: ${{ github.ref != 'refs/heads/main' }}
@@ -57,12 +71,12 @@ jobs:
if [ -z "${{ inputs.channel == 'production' }}" ]; then
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
else
echo base-commit=$(git log -n 1 --skip 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
echo base-commit=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
fi
- name: ✓ Make sure we found a base commit
run: |
if [ -z "${{ steps.base-commit.outputs.base-commit }}" ]; then
if [ -z "${{ steps.base-commit.outputs.base-commit }}" && ${{ inputs.channel == 'production' }} ]; then
echo "Could not find a base commit for this release. Exiting."
exit 1
fi
@@ -82,7 +96,6 @@ jobs:
uses: expo/expo-github-action/fingerprint@main
with:
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
args:
- name: 👀 Debug fingerprint
id: fingerprint-debug
@@ -90,13 +103,26 @@ jobs:
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
fingerprintDiff="${{ steps.fingerprint.outputs.fingerprint-diff }}"
fingerprintDiff='$(echo "${{ steps.fingerprint.outputs.fingerprint-diff }}")'
if [[ $fingerprintDiff =~ "bareRncliAutolinking" || $fingerprintDiff =~ "expoAutolinkingAndroid" || $fingerprintDiff =~ "expoAutolinkingIos" ]]; then
echo fingerprint-is-different="true" >> "$GITHUB_OUTPUT"
else
echo fingerprint-is-different="false" >> "$GITHUB_OUTPUT"
fi
- name: Lint check
run: yarn lint
- name: Prettier check
run: yarn prettier --check .
- name: Check & compile i18n
run: yarn intl:build
- name: Type check
run: yarn typecheck
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
@@ -113,10 +139,6 @@ jobs:
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
uses: dcarbone/install-jq-action@v2
- name: 🔤 Compile Translations
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn intl:build
- name: ✏️ Write environment variables
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: |
@@ -136,11 +158,16 @@ jobs:
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
- name: Save successful deployment commit hash
run: echo ${{ steps.fingerprint.outputs.current-git-commit }} > last-successful-commit-hash.txt
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
name: Build and Submit iOS
runs-on: macos-14
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
cancel-in-progress: false
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
@@ -206,6 +233,9 @@ jobs:
buildIfNecessaryAndroid:
name: Build and Submit Android
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-android
cancel-in-progress: false
needs: [ bundleDeploy ]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
+26 -164
View File
@@ -13,12 +13,15 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
permissions:
pull-requests: write
actions: write
contents: read
jobs:
webpack-analyzer:
runs-on: ubuntu-22.04
if: ${{ github.event_name == 'pull_request' }}
permissions:
pull-requests: write
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
@@ -31,9 +34,6 @@ jobs:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
- name: Ensure tracking relevant branches and checkout base
run: |
git checkout ${{ github.head_ref }}
@@ -49,6 +49,7 @@ jobs:
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git merge --no-edit ${{ github.head_ref }}
yarn install
- name: 🔦 Generate stats file for PR
run: |
@@ -71,6 +72,7 @@ jobs:
- name: 🔦 Generate stats file from base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
run: |
yarn install
yarn generate-webpack-stats-file
mv stats.json stats-base.json
@@ -82,62 +84,20 @@ jobs:
pr_path: '../stats-new.json'
excluded_assets: '(.+).chunk.js|(.+).js.map|(.+).json|(.+).png'
- name: 🔍 Find old comment if it exists
uses: peter-evans/find-comment@v2
if: ${{ github.event_name == 'pull_request' }}
id: old_comment
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: <!-- webpack-analyzer comment -->
- name: 💬 Add comment with diff
uses: actions/github-script@v6
if: ${{ steps.old_comment.outputs.comment-id == '' }}
with:
script: |
const body = `<!-- webpack-analyzer comment -->
header: bundle-diff
message: |
| Old size | New size | Diff |
|----------|----------|-----------------------|
| ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) |
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: 💬 Update comment with fingerprint
if: ${{ steps.old_comment.outputs.comment-id != '' }}
uses: actions/github-script@v6
with:
script: |
const body = `<!-- webpack-analyzer comment -->
| Old size | New size | Diff |
|----------|----------|-----------------------|
| ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) |
`;
github.rest.issues.updateComment({
issue_number: context.issue.number,
comment_id: '${{ steps.old_comment.outputs.comment-id }}',
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
---
test-suite-fingerprint:
runs-on: ubuntu-22.04
if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
# REQUIRED: limit concurrency when pushing main(default) branch to prevent conflict for this action to update its fingerprint database
if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }}
permissions:
# REQUIRED: Allow comments of PRs
pull-requests: write
# REQUIRED: Allow updating fingerprint in acton caches
actions: write
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
@@ -172,126 +132,28 @@ jobs:
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
- name: 🏷️ Labeling PR
uses: actions/github-script@v6
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }}
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: ['bot: fingerprint changed']
})
} catch (e) {
if (e.status != 404) {
throw e;
}
}
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['bot: fingerprint compatible']
})
- name: 🏷️ Labeling PR
uses: actions/github-script@v6
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }}
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: ['bot: fingerprint compatible']
})
} catch (e) {
if (e.status != 404) {
throw e;
}
}
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['bot: fingerprint changed']
})
- name: 🔍 Find old comment if it exists
uses: peter-evans/find-comment@v2
if: ${{ github.event_name == 'pull_request' }}
id: old_comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: <!-- pr-labeler comment -->
- name: 💬 Add comment with fingerprint
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id == '' }}
uses: actions/github-script@v6
with:
script: |
const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2);
const body = `<!-- pr-labeler comment -->
header: fingerprint-diff
message: |
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
<details><summary>Fingerprint diff</summary>
\`\`\`json
${diff}
\`\`\`
```json
${{ steps.fingerprint.outputs.fingerprint-diff }}
```
</details>
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: 💬 Update comment with fingerprint
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id != '' }}
uses: actions/github-script@v6
- name: 💬 Delete comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }}
with:
script: |
const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2);
const body = `<!-- pr-labeler comment -->
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
<details><summary>Fingerprint diff</summary>
header: fingerprint-diff
delete: true
\`\`\`json
${diff}
\`\`\`
</details>
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
`;
github.rest.issues.updateComment({
issue_number: context.issue.number,
comment_id: '${{ steps.old_comment.outputs.comment-id }}',
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: 💬 Delete comment with fingerprint
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' && steps.old_comment.outputs.comment-id != '' }}
uses: actions/github-script@v6
with:
script: |
github.rest.issues.deleteComment({
issue_number: context.issue.number,
comment_id: '${{ steps.old_comment.outputs.comment-id }}',
owner: context.repo.owner,
repo: context.repo.repo,
});
+9 -5
View File
@@ -130,11 +130,15 @@ module.exports = function (config) {
// TestFlight builds
enabled: IS_TESTFLIGHT,
fallbackToCacheTimeout: 30000,
codeSigningCertificate: './code-signing/certificate.pem',
codeSigningMetadata: {
keyid: 'main',
alg: 'rsa-v1_5-sha256',
},
codeSigningCertificate: IS_TESTFLIGHT
? './code-signing/certificate.pem'
: undefined,
codeSigningMetadata: IS_TESTFLIGHT
? {
keyid: 'main',
alg: 'rsa-v1_5-sha256',
}
: undefined,
checkAutomatically: 'NEVER',
channel: UPDATES_CHANNEL,
},
+15
View File
@@ -220,6 +220,21 @@
.nativeDropdown-item:focus {
outline: none;
}
/* Spinner component */
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.rotate-500ms {
position: absolute;
inset:0;
animation: rotate 500ms linear infinite;
}
</style>
{% include "scripts.html" %}
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
+403 -1
View File
@@ -199,7 +199,7 @@ describe('avoid-unwrapped-text', () => {
{
code: `
<View prop={
<View propText={
<Trans><Text>foo</Text></Trans>
}>
<Bar />
@@ -246,6 +246,205 @@ describe('avoid-unwrapped-text', () => {
</Foo>
`,
},
{
code: `
function Stuff() {
return <Text>foo</Text>
}
`,
},
{
code: `
function Stuff({ foo }) {
return <View>{foo}</View>
}
`,
},
{
code: `
function MyText() {
return <Text>foo</Text>
}
`,
},
{
code: `
function MyText({ foo }) {
if (foo) {
return <Text>foo</Text>
}
return <Text>foo</Text>
}
`,
},
{
code: `
<View>
<Text>{'foo'}</Text>
</View>
`,
},
{
code: `
<View>
<Text>{foo + 'foo'}</Text>
</View>
`,
},
{
code: `
<View>
<Text><Trans>{'foo'}</Trans></Text>
</View>
`,
},
{
code: `
<View>
{foo['bar'] && <Bar />}
</View>
`,
},
{
code: `
<View>
{(foo === 'bar') && <Bar />}
</View>
`,
},
{
code: `
<View>
{(foo !== 'bar') && <Bar />}
</View>
`,
},
{
code: `
<View>
<Text>{\`foo\`}</Text>
</View>
`,
},
{
code: `
<View>
<Text><Trans>{\`foo\`}</Trans></Text>
</View>
`,
},
{
code: `
<View>
<Text>{_(msg\`foo\`)}</Text>
</View>
`,
},
{
code: `
<View>
<Text><Trans>{_(msg\`foo\`)}</Trans></Text>
</View>
`,
},
{
code: `
<Foo>
<View prop={stuff('foo')}>
<Bar />
</View>
</Foo>
`,
},
{
code: `
<Foo>
<View onClick={() => stuff('foo')}>
<Bar />
</View>
</Foo>
`,
},
{
code: `
<View>
{renderItem('foo')}
</View>
`,
},
{
code: `
<View>
{foo === 'foo' && <Bar />}
</View>
`,
},
{
code: `
<View>
{foo['foo'] && <Bar />}
</View>
`,
},
{
code: `
<View>
{check('foo') && <Bar />}
</View>
`,
},
{
code: `
<View>
{foo.bar && <Bar />}
</View>
`,
},
{
code: `
<Text>
<Trans>{renderItem('foo')}</Trans>
</Text>
`,
},
{
code: `
<View>
{null}
</View>
`,
},
{
code: `
<Text>
<Trans>{null}</Trans>
</Text>
`,
},
],
invalid: [
@@ -390,6 +589,209 @@ describe('avoid-unwrapped-text', () => {
`,
errors: 1,
},
{
code: `
function MyText() {
return <Foo />
}
`,
errors: 1,
},
{
code: `
function MyText({ foo }) {
return <Foo>{foo}</Foo>
}
`,
errors: 1,
},
{
code: `
function MyText({ foo }) {
if (foo) {
return <Foo>{foo}</Foo>
}
return <Text>foo</Text>
}
`,
errors: 1,
},
{
code: `
<View>
{'foo'}
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo && 'foo'}
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{'foo'}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo && <Trans>{'foo'}</Trans>}
</View>
`,
errors: 1,
},
{
code: `
<View>
{10}
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{10}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{foo + 10}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
{\`foo\`}
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{\`foo\`}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{foo + \`foo\`}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
{_(msg\`foo\`)}
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo + _(msg\`foo\`)}
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{_(msg\`foo\`)}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{foo + _(msg\`foo\`)}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>foo</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans><Trans>foo</Trans></Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{foo}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>{'foo'}</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View prop={
<Trans><Text>foo</Text></Trans>
}>
<Bar />
</View>
`,
errors: 1,
},
],
}
+226 -6
View File
@@ -33,8 +33,14 @@ exports.create = function create(context) {
const options = context.options[0] || {}
const impliedTextProps = options.impliedTextProps ?? []
const impliedTextComponents = options.impliedTextComponents ?? []
const suggestedTextWrappers = options.suggestedTextWrappers ?? {}
const textProps = [...impliedTextProps]
const textComponents = ['Text', ...impliedTextComponents]
function isTextComponent(tagName) {
return textComponents.includes(tagName) || tagName.endsWith('Text')
}
return {
JSXText(node) {
if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) {
@@ -44,18 +50,18 @@ exports.create = function create(context) {
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (textComponents.includes(tagName) || tagName.endsWith('Text')) {
if (isTextComponent(tagName)) {
// We're good.
return
}
if (tagName === 'Trans') {
// Skip over it and check above.
// Exit and rely on the traversal for <Trans> JSXElement (code below).
// TODO: Maybe validate that it's present.
parent = parent.parent
continue
return
}
let message = 'Wrap this string in <Text>.'
if (tagName !== 'View') {
const suggestedWrapper = suggestedTextWrappers[tagName]
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
if (tagName !== 'View' && !suggestedWrapper) {
message +=
' If <' +
tagName +
@@ -107,5 +113,219 @@ exports.create = function create(context) {
continue
}
},
Literal(node) {
if (typeof node.value !== 'string' && typeof node.value !== 'number') {
return
}
let parent = node.parent
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (isTextComponent(tagName)) {
// We're good.
return
}
if (tagName === 'Trans') {
// Exit and rely on the traversal for <Trans> JSXElement (code below).
// TODO: Maybe validate that it's present.
return
}
const suggestedWrapper = suggestedTextWrappers[tagName]
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
if (tagName !== 'View' && !suggestedWrapper) {
message +=
' If <' +
tagName +
'> is guaranteed to render <Text>, ' +
'rename it to <' +
tagName +
'Text> or add it to impliedTextComponents.'
}
context.report({
node,
message,
})
return
}
if (parent.type === 'BinaryExpression' && parent.operator === '+') {
parent = parent.parent
continue
}
if (
parent.type === 'JSXExpressionContainer' ||
parent.type === 'LogicalExpression'
) {
parent = parent.parent
continue
}
// Be conservative for other types.
return
}
},
TemplateLiteral(node) {
let parent = node.parent
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (isTextComponent(tagName)) {
// We're good.
return
}
if (tagName === 'Trans') {
// Exit and rely on the traversal for <Trans> JSXElement (code below).
// TODO: Maybe validate that it's present.
return
}
const suggestedWrapper = suggestedTextWrappers[tagName]
let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.`
if (tagName !== 'View' && !suggestedWrapper) {
message +=
' If <' +
tagName +
'> is guaranteed to render <Text>, ' +
'rename it to <' +
tagName +
'Text> or add it to impliedTextComponents.'
}
context.report({
node,
message,
})
return
}
if (
parent.type === 'CallExpression' &&
parent.callee.type === 'Identifier' &&
parent.callee.name === '_'
) {
// This is a user-facing string, keep going up.
parent = parent.parent
continue
}
if (parent.type === 'BinaryExpression' && parent.operator === '+') {
parent = parent.parent
continue
}
if (
parent.type === 'JSXExpressionContainer' ||
parent.type === 'LogicalExpression' ||
parent.type === 'TaggedTemplateExpression'
) {
parent = parent.parent
continue
}
// Be conservative for other types.
return
}
},
JSXElement(node) {
if (getTagName(node) !== 'Trans') {
return
}
let parent = node.parent
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (isTextComponent(tagName)) {
// We're good.
return
}
if (tagName === 'Trans') {
// Exit and rely on the traversal for this JSXElement.
// TODO: Should nested <Trans> even be allowed?
return
}
const suggestedWrapper = suggestedTextWrappers[tagName]
let message = `Wrap this <Trans> in <${suggestedWrapper ?? 'Text'}>.`
if (tagName !== 'View' && !suggestedWrapper) {
message +=
' If <' +
tagName +
'> is guaranteed to render <Text>, ' +
'rename it to <' +
tagName +
'Text> or add it to impliedTextComponents.'
}
context.report({
node,
message,
})
return
}
if (
parent.type === 'JSXAttribute' &&
parent.name.type === 'JSXIdentifier' &&
parent.parent.type === 'JSXOpeningElement' &&
parent.parent.parent.type === 'JSXElement'
) {
const tagName = getTagName(parent.parent.parent)
const propName = parent.name.name
if (
textProps.includes(tagName + ' ' + propName) ||
propName === 'text' ||
propName.endsWith('Text')
) {
// We're good.
return
}
const message =
'Wrap this <Trans> in <Text>.' +
' If `' +
propName +
'` is guaranteed to be wrapped in <Text>, ' +
'rename it to `' +
propName +
'Text' +
'` or add it to impliedTextProps.'
context.report({
node,
message,
})
return
}
parent = parent.parent
continue
}
},
ReturnStatement(node) {
let fnScope = context.getScope()
while (fnScope && fnScope.type !== 'function') {
fnScope = fnScope.upper
}
if (!fnScope) {
return
}
const fn = fnScope.block
if (!fn.id || fn.id.type !== 'Identifier' || !fn.id.name) {
return
}
if (!/^[A-Z]\w*Text$/.test(fn.id.name)) {
return
}
if (!node.argument || node.argument.type !== 'JSXElement') {
return
}
const openingEl = node.argument.openingElement
if (openingEl.name.type !== 'JSXIdentifier') {
return
}
const returnedComponentName = openingEl.name.name
if (!isTextComponent(returnedComponentName)) {
context.report({
node,
message:
'Components ending with *Text must return <Text> or <SomeText>.',
})
}
},
}
}
+20 -28
View File
@@ -35,7 +35,7 @@
"e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all",
"perf:test": "NODE_ENV=test maestro test",
"perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml",
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure",
"intl:build": "yarn intl:extract && yarn intl:compile",
@@ -64,10 +64,7 @@
"@mattermost/react-native-paste-input": "^0.6.4",
"@miblanchard/react-native-slider": "^2.3.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@react-native-async-storage/async-storage": "1.21.0",
"@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-async-storage/async-storage": "1.23.1",
"@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^0.8.0",
"@react-native-picker/picker": "2.6.1",
@@ -108,27 +105,30 @@
"email-validator": "^2.0.4",
"emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1",
"expo": "^50.0.0-preview.10",
"expo-application": "~5.8.2",
"expo-build-properties": "^0.11.0",
"expo-camera": "~14.0.1",
"expo-constants": "~15.4.3",
"expo-dev-client": "~3.3.5",
"expo-device": "~5.9.2",
"expo-image": "~1.10.3",
"expo": "^50.0.8",
"expo-application": "^5.8.3",
"expo-build-properties": "^0.11.1",
"expo-camera": "~14.0.4",
"expo-clipboard": "^5.0.1",
"expo-constants": "~15.4.5",
"expo-dev-client": "~3.3.8",
"expo-device": "~5.9.3",
"expo-haptics": "^12.8.1",
"expo-image": "~1.10.6",
"expo-image-manipulator": "^11.8.0",
"expo-image-picker": "~14.7.1",
"expo-linear-gradient": "^12.7.2",
"expo-linking": "^6.2.2",
"expo-localization": "~14.8.2",
"expo-localization": "~14.8.3",
"expo-media-library": "~15.9.1",
"expo-notifications": "~0.27.3",
"expo-notifications": "~0.27.6",
"expo-sharing": "^11.10.0",
"expo-splash-screen": "~0.26.2",
"expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.3",
"expo-task-manager": "~11.7.0",
"expo-updates": "~0.24.7",
"expo-web-browser": "~12.8.1",
"expo-task-manager": "~11.7.2",
"expo-updates": "~0.24.10",
"expo-web-browser": "~12.8.2",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"js-sha256": "^0.9.0",
@@ -143,7 +143,6 @@
"lodash.samplesize": "^4.2.0",
"lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0",
"lru_map": "^0.4.1",
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",
"mobx-utils": "^6.0.6",
@@ -154,20 +153,16 @@
"psl": "^1.9.0",
"react": "18.2.0",
"react-avatar-editor": "^13.0.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.73.2",
"react-native-appstate-hook": "^1.0.6",
"react-native-date-picker": "^4.4.0",
"react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.14.0",
"react-native-get-random-values": "~1.8.0",
"react-native-haptic-feedback": "^1.14.0",
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "^0.38.1",
"react-native-ios-context-menu": "^1.15.3",
"react-native-linear-gradient": "^2.6.2",
"react-native-pager-view": "6.2.3",
"react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress",
@@ -179,9 +174,7 @@
"react-native-uitextview": "^1.1.6",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.1",
"react-native-version-number": "^0.3.6",
"react-native-web": "~0.19.6",
"react-native-web-linear-gradient": "^1.1.2",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "13.6.4",
"react-responsive": "^9.0.2",
@@ -190,7 +183,6 @@
"statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"use-deep-compare": "^1.1.0",
"zeego": "^1.6.2",
"zod": "^3.20.2"
},
+7 -7
View File
@@ -16,8 +16,13 @@ import {useQueryClient} from '@tanstack/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from '#/state/session'
import {readLastActiveAccount} from '#/state/session/util'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
import {useNotificationsListener} from 'lib/notifications/notifications'
@@ -31,11 +36,6 @@ import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from 'state/session'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
@@ -66,7 +66,7 @@ function InnerApp() {
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
})
const account = persisted.get('session').currentAccount
const account = readLastActiveAccount()
resumeSession(account)
}, [resumeSession, _])
+7 -7
View File
@@ -7,8 +7,13 @@ import {SafeAreaProvider} from 'react-native-safe-area-context'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from '#/state/session'
import {readLastActiveAccount} from '#/state/session/util'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {QueryProvider} from 'lib/react-query'
import {ThemeProvider} from 'lib/ThemeContext'
@@ -19,11 +24,6 @@ import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from 'state/session'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
@@ -42,7 +42,7 @@ function InnerApp() {
// init
useEffect(() => {
const account = persisted.get('session').currentAccount
const account = readLastActiveAccount()
resumeSession(account)
}, [resumeSession])
@@ -11,8 +11,7 @@ import {
View,
ViewStyle,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {Trans} from '@lingui/macro'
import {LinearGradient} from 'expo-linear-gradient'
import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
@@ -59,6 +58,10 @@ export type ButtonState = {
export type ButtonContext = VariantProps & ButtonState
type NonTextElements =
| React.ReactElement
| Iterable<React.ReactElement | null | undefined | boolean>
export type ButtonProps = Pick<
PressableProps,
'disabled' | 'onPress' | 'testID'
@@ -68,11 +71,9 @@ export type ButtonProps = Pick<
testID?: string
label: string
style?: StyleProp<ViewStyle>
children:
| React.ReactNode
| string
| ((context: ButtonContext) => React.ReactNode | string)
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
}
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
const Context = React.createContext<VariantProps & ButtonState>({
@@ -404,15 +405,7 @@ export function Button({
</View>
)}
<Context.Provider value={context}>
{/* @ts-ignore */}
{typeof children === 'string' || children?.type === Trans ? (
/* @ts-ignore */
<ButtonText>{children}</ButtonText>
) : typeof children === 'function' ? (
children(context)
) : (
children
)}
{typeof children === 'function' ? children(context) : children}
</Context.Provider>
</Pressable>
)
-61
View File
@@ -1,61 +0,0 @@
# Button
`Button` is intended to be very flexible and can be used to create consistent
button styles, as seen in the Storybook, or can be used to create custom
buttons with totally independent styles.
## Usage
Typical usage looks like this, where the `variant`, `size`, `color`, and/or
`shape` are defined. Normal usage also should make use of the `ButtonText` and
`ButtonIcon` components so that they inherit styles from the top level props
like `color`.
```tsx
<Button
label='Typical button'
onPress={() => {}}
size='large'
variant='solid'
color='primary'
>
<ButtonIcon icon={Plus} position='left' />
<ButtonText>Hello world</ButtonText>
</Button>
```
Or:
```tsx
<Button
label='Typical button'
onPress={() => {}}
size='large'
variant='solid'
color='primary'
>
<ButtonText>Hello world</ButtonText>
<ButtonIcon icon={Plus} position='right' />
</Button>
```
Each of those top-level props props is optional in order to allow for "custom"
buttons, like this:
```tsx
<Button label='Custom button' onPress={() => {}}>
{({ hovered, focused, pressed, disabled }) => (
<View style={[
t.atoms.bg_contrast_25,
hovered && t.atoms.bg_contrast_50,
]}>
<Text>{disabled ? 'Disabled' : 'Click me!'}</Text>
</View>
))}
</Button>
```
In the custom button case, you have the _option_ to use `ButtonIcon` and
`ButtonText`, but without top-level props like `color`, they serve little
purpose, and it may be perferrable to define your own components for these
purposes.
+2 -1
View File
@@ -39,7 +39,8 @@ export function useDialogControl(): DialogOuterProps['control'] {
control.current.open()
},
close: cb => {
control.current.close(cb)
control.current.close()
cb?.()
},
}),
[id, control],
+37 -16
View File
@@ -1,20 +1,24 @@
import React, {useImperativeHandle} from 'react'
import {View, TouchableWithoutFeedback} from 'react-native'
import {FocusScope} from '@tamagui/focus-scope'
import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
import {TouchableWithoutFeedback, View} from 'react-native'
import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FocusScope} from '@tamagui/focus-scope'
import {useTheme, atoms as a, useBreakpoints, web, flatten} from '#/alf'
import {logger} from '#/logger'
import {useDialogStateControlContext} from '#/state/dialogs'
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {Context} from '#/components/Dialog/context'
import {
DialogControlProps,
DialogInnerProps,
DialogOuterProps,
} from '#/components/Dialog/types'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Portal} from '#/components/Portal'
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {useDialogStateControlContext} from '#/state/dialogs'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
export {Input} from '#/components/forms/TextField'
@@ -37,14 +41,31 @@ export function Outer({
setDialogIsOpen(control.id, true)
}, [setIsOpen, setDialogIsOpen, control.id])
const close = React.useCallback(async () => {
const onCloseInner = React.useCallback(async () => {
setIsVisible(false)
await new Promise(resolve => setTimeout(resolve, 150))
setIsOpen(false)
setIsVisible(true)
setDialogIsOpen(control.id, false)
onClose?.()
}, [onClose, setIsOpen, setDialogIsOpen, control.id])
}, [control.id, onClose, setDialogIsOpen])
const close = React.useCallback<DialogControlProps['close']>(
cb => {
try {
if (cb && typeof cb === 'function') {
cb()
}
} catch (e: any) {
logger.error(`Dialog closeCallback failed`, {
message: e.message,
})
} finally {
onCloseInner()
}
},
[onCloseInner],
)
useImperativeHandle(
control.ref,
@@ -52,7 +73,7 @@ export function Outer({
open,
close,
}),
[open, close],
[close, open],
)
React.useEffect(() => {
@@ -65,7 +86,7 @@ export function Outer({
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [isOpen, close])
}, [close, isOpen])
const context = React.useMemo(
() => ({
@@ -82,7 +103,7 @@ export function Outer({
<TouchableWithoutFeedback
accessibilityHint={undefined}
accessibilityLabel={_(msg`Close active dialog`)}
onPress={close}>
onPress={onCloseInner}>
<View
style={[
web(a.fixed),
+1 -1
View File
@@ -1,5 +1,5 @@
import React from 'react'
import LinearGradient from 'react-native-linear-gradient'
import {LinearGradient} from 'expo-linear-gradient'
import {atoms as a, tokens} from '#/alf'
+1 -1
View File
@@ -250,7 +250,7 @@ export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
>
export function InlineLink({
export function InlineLinkText({
children,
to,
action = 'push',
+4 -2
View File
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
import {cleanError} from 'lib/strings/errors'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Button, ButtonText} from '#/components/Button'
import {Error} from '#/components/Error'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
@@ -87,7 +87,9 @@ function ListFooterMaybeError({
a.py_sm,
]}
onPress={onRetry}>
<Trans>Retry</Trans>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
</View>
</View>
+2 -2
View File
@@ -1,13 +1,13 @@
import React from 'react'
import Animated, {
Easing,
useSharedValue,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated'
import {atoms as a, useTheme, flatten} from '#/alf'
import {atoms as a, flatten, useTheme} from '#/alf'
import {Props, useCommonSVGProps} from '#/components/icons/common'
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
+34
View File
@@ -0,0 +1,34 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, flatten, useTheme} from '#/alf'
import {Props, useCommonSVGProps} from '#/components/icons/common'
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
export function Loader(props: Props) {
const t = useTheme()
const common = useCommonSVGProps(props)
return (
<View
style={[
a.relative,
a.justify_center,
a.align_center,
{width: common.size, height: common.size},
]}>
{/* css rotation animation - /bskyweb/templates/base.html */}
<div className="rotate-500ms">
<Icon
{...props}
style={[
a.absolute,
a.inset_0,
t.atoms.text_contrast_high,
flatten(props.style),
]}
/>
</div>
</View>
)
}
+13 -18
View File
@@ -51,7 +51,7 @@ export function Outer({
)
}
export function Title({children}: React.PropsWithChildren<{}>) {
export function TitleText({children}: React.PropsWithChildren<{}>) {
const {titleId} = React.useContext(Context)
return (
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
@@ -60,7 +60,7 @@ export function Title({children}: React.PropsWithChildren<{}>) {
)
}
export function Description({children}: React.PropsWithChildren<{}>) {
export function DescriptionText({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {descriptionId} = React.useContext(Context)
return (
@@ -91,15 +91,13 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
}
export function Cancel({
children,
cta,
}: React.PropsWithChildren<{
}: {
/**
* Optional i18n string, used in lieu of `children` for simple buttons. If
* undefined (and `children` is undefined), it will default to "Cancel".
* Optional i18n string. If undefined, it will default to "Cancel".
*/
cta?: string
}>) {
}) {
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext()
@@ -114,33 +112,30 @@ export function Cancel({
size={gtMobile ? 'small' : 'medium'}
label={cta || _(msg`Cancel`)}
onPress={onPress}>
{children ? children : <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>}
<ButtonText>{cta || _(msg`Cancel`)}</ButtonText>
</Button>
)
}
export function Action({
children,
onPress,
color = 'primary',
cta,
testID,
}: React.PropsWithChildren<{
}: {
onPress: () => void
color?: ButtonColor
/**
* Optional i18n string, used in lieu of `children` for simple buttons. If
* undefined (and `children` is undefined), it will default to "Confirm".
* Optional i18n string. If undefined, it will default to "Confirm".
*/
cta?: string
testID?: string
}>) {
}) {
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext()
const handleOnPress = React.useCallback(() => {
close()
onPress()
close(onPress)
}, [close, onPress])
return (
@@ -151,7 +146,7 @@ export function Action({
label={cta || _(msg`Confirm`)}
onPress={handleOnPress}
testID={testID}>
{children ? children : <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>}
<ButtonText>{cta || _(msg`Confirm`)}</ButtonText>
</Button>
)
}
@@ -175,8 +170,8 @@ export function Basic({
}>) {
return (
<Outer control={control} testID="confirmModal">
<Title>{title}</Title>
<Description>{description}</Description>
<TitleText>{title}</TitleText>
<DescriptionText>{description}</DescriptionText>
<Actions>
<Action
cta={confirmButtonCta}
+5 -5
View File
@@ -7,7 +7,7 @@ import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLink} from '#/components/Link'
import {InlineLinkText} from '#/components/Link'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {Text, TextProps} from '#/components/Typography'
@@ -84,7 +84,7 @@ export function RichText({
!disableLinks
) {
els.push(
<InlineLink
<InlineLinkText
selectable={selectable}
key={key}
to={`/profile/${mention.did}`}
@@ -92,14 +92,14 @@ export function RichText({
// @ts-ignore TODO
dataSet={WORD_WRAP}>
{segment.text}
</InlineLink>,
</InlineLinkText>,
)
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
if (disableLinks) {
els.push(toShortUrl(segment.text))
} else {
els.push(
<InlineLink
<InlineLinkText
selectable={selectable}
key={key}
to={link.uri}
@@ -108,7 +108,7 @@ export function RichText({
dataSet={WORD_WRAP}
shareOnLongPress>
{toShortUrl(segment.text)}
</InlineLink>,
</InlineLinkText>,
)
}
} else if (
+119
View File
@@ -0,0 +1,119 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
type EmbedPlayerSource,
embedPlayerSources,
externalEmbedLabels,
} from '#/lib/strings/embed-player'
import {useSetExternalEmbedPref} from '#/state/preferences'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Button, ButtonText} from '../Button'
import {Text} from '../Typography'
export function EmbedConsentDialog({
control,
source,
onAccept,
}: {
control: Dialog.DialogControlProps
source: EmbedPlayerSource
onAccept: () => void
}) {
const {_} = useLingui()
const t = useTheme()
const setExternalEmbedPref = useSetExternalEmbedPref()
const {gtMobile} = useBreakpoints()
const onShowAllPress = useCallback(() => {
for (const key of embedPlayerSources) {
setExternalEmbedPref(key, 'show')
}
onAccept()
control.close()
}, [control, onAccept, setExternalEmbedPref])
const onShowPress = useCallback(() => {
setExternalEmbedPref(source, 'show')
onAccept()
control.close()
}, [control, onAccept, setExternalEmbedPref, source])
const onHidePress = useCallback(() => {
setExternalEmbedPref(source, 'hide')
control.close()
}, [control, setExternalEmbedPref, source])
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`External Media`)}
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
<View style={a.gap_sm}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>External Media</Trans>
</Text>
<View style={[a.mt_sm, a.mb_2xl, a.gap_lg]}>
<Text>
<Trans>
This content is hosted by {externalEmbedLabels[source]}. Do you
want to enable external media?
</Trans>
</Text>
<Text style={t.atoms.text_contrast_medium}>
<Trans>
External media may allow websites to collect information about
you and your device. No information is sent or requested until
you press the "play" button.
</Trans>
</Text>
</View>
</View>
<View style={a.gap_md}>
<Button
style={gtMobile && a.flex_1}
label={_(msg`Enable external media`)}
onPress={onShowAllPress}
onAccessibilityEscape={control.close}
color="primary"
size="medium"
variant="solid">
<ButtonText>
<Trans>Enable external media</Trans>
</ButtonText>
</Button>
<Button
style={gtMobile && a.flex_1}
label={_(msg`Enable this source only`)}
onPress={onShowPress}
onAccessibilityEscape={control.close}
color="secondary"
size="medium"
variant="solid">
<ButtonText>
<Trans>Enable {externalEmbedLabels[source]} only</Trans>
</ButtonText>
</Button>
<Button
label={_(msg`No thanks`)}
onAccessibilityEscape={control.close}
onPress={onHidePress}
color="secondary"
size="medium"
variant="ghost">
<ButtonText>
<Trans>No thanks</Trans>
</ButtonText>
</Button>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+20 -21
View File
@@ -1,37 +1,36 @@
import React from 'react'
import {Keyboard, View} from 'react-native'
import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
import {
usePreferencesQuery,
useUpsertMutedWordsMutation,
useRemoveMutedWordMutation,
} from '#/state/queries/preferences'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {
usePreferencesQuery,
useRemoveMutedWordMutation,
useUpsertMutedWordsMutation,
} from '#/state/queries/preferences'
import {
atoms as a,
useTheme,
native,
useBreakpoints,
useTheme,
ViewStyleProp,
web,
native,
} from '#/alf'
import {Text} from '#/components/Typography'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as Dialog from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText'
import {Divider} from '#/components/Divider'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
import {logger} from '#/logger'
import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import * as Prompt from '#/components/Prompt'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Text} from '#/components/Typography'
export function MutedWordsDialog() {
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
@@ -130,9 +129,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
<TargetToggle>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Radio />
<Toggle.Label>
<Toggle.LabelText>
<Trans>Mute in text & tags</Trans>
</Toggle.Label>
</Toggle.LabelText>
</View>
<PageText size="sm" />
</TargetToggle>
@@ -145,9 +144,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
<TargetToggle>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Radio />
<Toggle.Label>
<Toggle.LabelText>
<Trans>Mute in tags only</Trans>
</Toggle.Label>
</Toggle.LabelText>
</View>
<Hashtag size="sm" />
</TargetToggle>
@@ -8,7 +8,7 @@ import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export const LabelText = TextField.LabelText
export function DateField({
value,
+1 -1
View File
@@ -13,7 +13,7 @@ import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export const LabelText = TextField.LabelText
/**
* Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date
+1 -1
View File
@@ -9,7 +9,7 @@ import * as TextField from '#/components/forms/TextField'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
export const LabelText = TextField.LabelText
const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>(
({style, ...props}, ref) => {
+2 -2
View File
@@ -225,7 +225,7 @@ export function createInput(Component: typeof TextInput) {
export const Input = createInput(TextInput)
export function Label({
export function LabelText({
nativeID,
children,
}: React.PropsWithChildren<{nativeID?: string}>) {
@@ -288,7 +288,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
)
}
export function Suffix({
export function SuffixText({
children,
label,
accessibilityHint,
+5 -5
View File
@@ -3,16 +3,16 @@ import {Pressable, View, ViewStyle} from 'react-native'
import {HITSLOP_10} from 'lib/constants'
import {
useTheme,
atoms as a,
native,
flatten,
ViewStyleProp,
native,
TextStyleProp,
useTheme,
ViewStyleProp,
} from '#/alf'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
import {Text} from '#/components/Typography'
export type ItemState = {
name: string
@@ -234,7 +234,7 @@ export function Item({
)
}
export function Label({
export function LabelText({
children,
style,
}: React.PropsWithChildren<TextStyleProp>) {
+67 -57
View File
@@ -1,16 +1,15 @@
import React from 'react'
import {View, AccessibilityProps, TextStyle, ViewStyle} from 'react-native'
import {AccessibilityProps, TextStyle, View, ViewStyle} from 'react-native'
import {atoms as a, useTheme, native} from '#/alf'
import {atoms as a, native, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
AccessibilityProps &
React.PropsWithChildren<{
type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
AccessibilityProps & {
children: React.ReactElement
testID?: string
}>
}
export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & {
multiple?: boolean
@@ -47,49 +46,42 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const state = Toggle.useItemContext()
const {baseStyles, hoverStyles, activeStyles, textStyles} =
React.useMemo(() => {
const base: ViewStyle[] = []
const hover: ViewStyle[] = []
const active: ViewStyle[] = []
const text: TextStyle[] = []
const {baseStyles, hoverStyles, activeStyles} = React.useMemo(() => {
const base: ViewStyle[] = []
const hover: ViewStyle[] = []
const active: ViewStyle[] = []
hover.push(
t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25,
)
hover.push(
t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25,
)
if (state.selected) {
active.push({
backgroundColor: t.palette.contrast_800,
})
text.push(t.atoms.text_inverted)
hover.push({
backgroundColor: t.palette.contrast_800,
})
if (state.disabled) {
active.push({
backgroundColor: t.palette.contrast_500,
})
}
}
if (state.selected) {
active.push({
backgroundColor: t.palette.contrast_800,
})
hover.push({
backgroundColor: t.palette.contrast_800,
})
if (state.disabled) {
base.push({
backgroundColor: t.palette.contrast_100,
})
text.push({
opacity: 0.5,
active.push({
backgroundColor: t.palette.contrast_500,
})
}
}
return {
baseStyles: base,
hoverStyles: hover,
activeStyles: active,
textStyles: text,
}
}, [t, state])
if (state.disabled) {
base.push({
backgroundColor: t.palette.contrast_100,
})
}
return {
baseStyles: base,
hoverStyles: hover,
activeStyles: active,
}
}, [t, state])
return (
<View
@@ -110,19 +102,37 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
activeStyles,
(state.hovered || state.pressed) && hoverStyles,
]}>
{typeof children === 'string' ? (
<Text
style={[
a.text_center,
a.font_bold,
t.atoms.text_contrast_medium,
textStyles,
]}>
{children}
</Text>
) : (
children
)}
{children}
</View>
)
}
export function ButtonText({children}: {children: React.ReactNode}) {
const t = useTheme()
const state = Toggle.useItemContext()
const textStyles = React.useMemo(() => {
const text: TextStyle[] = []
if (state.selected) {
text.push(t.atoms.text_inverted)
}
if (state.disabled) {
text.push({
opacity: 0.5,
})
}
return text
}, [t, state])
return (
<Text
style={[
a.text_center,
a.font_bold,
t.atoms.text_contrast_medium,
textStyles,
]}>
{children}
</Text>
)
}
@@ -13,7 +13,7 @@ import {
} from '#/state/queries/preferences'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {InlineLink} from '#/components/Link'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
@@ -84,17 +84,17 @@ export function Buttons({
onChange={onChange}>
{ignoreLabel && (
<ToggleButton.Button name="ignore" label={ignoreLabel}>
{ignoreLabel}
<ToggleButton.ButtonText>{ignoreLabel}</ToggleButton.ButtonText>
</ToggleButton.Button>
)}
{warnLabel && (
<ToggleButton.Button name="warn" label={warnLabel}>
{warnLabel}
<ToggleButton.ButtonText>{warnLabel}</ToggleButton.ButtonText>
</ToggleButton.Button>
)}
{hideLabel && (
<ToggleButton.Button name="hide" label={hideLabel}>
{hideLabel}
<ToggleButton.ButtonText>{hideLabel}</ToggleButton.ButtonText>
</ToggleButton.Button>
)}
</ToggleButton.Group>
@@ -243,9 +243,9 @@ export function LabelerLabelPreference({
) : isGlobalLabel ? (
<Trans>
Configured in{' '}
<InlineLink to="/moderation" style={a.text_sm}>
<InlineLinkText to="/moderation" style={a.text_sm}>
moderation settings
</InlineLink>
</InlineLinkText>
.
</Trans>
) : null}
+12 -13
View File
@@ -1,20 +1,19 @@
import React from 'react'
import {View} from 'react-native'
import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
import {getAgent} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {Button, ButtonText} from '#/components/Button'
import {InlineLink} from '#/components/Link'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {Divider} from '../Divider'
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
@@ -145,13 +144,13 @@ function Label({
<View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>Source:</Trans>{' '}
<InlineLink
<InlineLinkText
to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''},
)}
onPress={() => control.close()}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
</InlineLink>
</InlineLinkText>
</Text>
</View>
</View>
@@ -204,14 +203,14 @@ function AppealForm({
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
This appeal will be sent to{' '}
<InlineLink
<InlineLinkText
to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''},
)}
onPress={() => control.close()}
style={[a.text_md, a.leading_snug]}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
</InlineLink>
</InlineLinkText>
.
</Trans>
</Text>
@@ -245,7 +244,7 @@ function AppealForm({
size="medium"
onPress={onPressBack}
label={_(msg`Back`)}>
{_(msg`Back`)}
<ButtonText>{_(msg`Back`)}</ButtonText>
</Button>
<Button
testID="submitBtn"
@@ -254,7 +253,7 @@ function AppealForm({
size="medium"
onPress={onSubmit}
label={_(msg`Submit`)}>
{_(msg`Submit`)}
<ButtonText>{_(msg`Submit`)}</ButtonText>
</Button>
</View>
</>
@@ -1,19 +1,18 @@
import React from 'react'
import {View} from 'react-native'
import {ModerationCause} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ModerationCause} from '@atproto/api'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {makeProfileLink} from '#/lib/routes/links'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
import {atoms as a, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {InlineLink} from '#/components/Link'
import {Divider} from '#/components/Divider'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog'
@@ -55,9 +54,9 @@ function ModerationDetailsDialogInner({
description = (
<Trans>
This user is included in the{' '}
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}>
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
{list.name}
</InlineLink>{' '}
</InlineLinkText>{' '}
list which you have blocked.
</Trans>
)
@@ -84,9 +83,9 @@ function ModerationDetailsDialogInner({
description = (
<Trans>
This user is included in the{' '}
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}>
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
{list.name}
</InlineLink>{' '}
</InlineLinkText>{' '}
list which you have muted.
</Trans>
)
@@ -127,12 +126,12 @@ function ModerationDetailsDialogInner({
{modcause.source.type === 'user' ? (
<Trans>the author</Trans>
) : (
<InlineLink
<InlineLinkText
to={makeProfileLink({did: modcause.label.src, handle: ''})}
onPress={() => control.close()}
style={a.text_md}>
{desc.source}
</InlineLink>
</InlineLinkText>
)}
.
</Trans>
+4 -4
View File
@@ -1,9 +1,9 @@
import VersionNumber from 'react-native-version-number'
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
export const appVersion = `${VersionNumber.appVersion} (${
VersionNumber.buildVersion
}, ${IS_DEV ? 'development' : UPDATES_CHANNEL})`
export const appVersion = `${nativeApplicationVersion} (${nativeBuildVersion}, ${
IS_DEV ? 'development' : UPDATES_CHANNEL
})`
+4 -1
View File
@@ -4,6 +4,7 @@ export const LOCAL_DEV_SERVICE =
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
export const STAGING_SERVICE = 'https://staging.bsky.dev'
export const BSKY_SERVICE = 'https://bsky.social'
export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'
export const DEFAULT_SERVICE = BSKY_SERVICE
const HELP_DESK_LANG = 'en-us'
export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}`
@@ -80,8 +81,10 @@ export const HITSLOP_30 = createHitslop(30)
export const BACK_HITSLOP = HITSLOP_30
export const MAX_POST_LINES = 25
export const BSKY_APP_ACCOUNT_DID = 'did:plc:z72i7hdynmk6r22z27h6tvur'
export const BSKY_FEED_OWNER_DIDS = [
'did:plc:z72i7hdynmk6r22z27h6tvur',
BSKY_APP_ACCOUNT_DID,
'did:plc:vpkhqolt662uhesyj6nxm7ys',
'did:plc:q6gjnaw2blty4crticxkmujt',
]
+19 -12
View File
@@ -1,28 +1,35 @@
import {isIOS, isWeb} from 'platform/detection'
import ReactNativeHapticFeedback, {
HapticFeedbackTypes,
} from 'react-native-haptic-feedback'
import {
impactAsync,
ImpactFeedbackStyle,
notificationAsync,
NotificationFeedbackType,
selectionAsync,
} from 'expo-haptics'
const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537s
import {isIOS, isWeb} from 'platform/detection'
const hapticImpact: ImpactFeedbackStyle = isIOS
? ImpactFeedbackStyle.Medium
: ImpactFeedbackStyle.Light // Users said the medium impact was too strong on Android; see APP-537s
export class Haptics {
static default() {
if (isWeb) {
return
}
ReactNativeHapticFeedback.trigger(hapticImpact)
impactAsync(hapticImpact)
}
static impact(type: HapticFeedbackTypes = hapticImpact) {
static impact(type: ImpactFeedbackStyle = hapticImpact) {
if (isWeb) {
return
}
ReactNativeHapticFeedback.trigger(type)
impactAsync(type)
}
static selection() {
if (isWeb) {
return
}
ReactNativeHapticFeedback.trigger('selection')
selectionAsync()
}
static notification = (type: 'success' | 'warning' | 'error') => {
if (isWeb) {
@@ -30,11 +37,11 @@ export class Haptics {
}
switch (type) {
case 'success':
return ReactNativeHapticFeedback.trigger('notificationSuccess')
return notificationAsync(NotificationFeedbackType.Success)
case 'warning':
return ReactNativeHapticFeedback.trigger('notificationWarning')
return notificationAsync(NotificationFeedbackType.Warning)
case 'error':
return ReactNativeHapticFeedback.trigger('notificationError')
return notificationAsync(NotificationFeedbackType.Error)
}
}
}
+12 -6
View File
@@ -1,11 +1,12 @@
import {useCallback} from 'react'
import {isWeb} from '#/platform/detection'
import {useAnalytics} from '#/lib/analytics/analytics'
import {useSessionApi, SessionAccount} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {useCloseAllActiveElements} from '#/state/util'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {SessionAccount, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import * as Toast from '#/view/com/util/Toast'
import {LogEvents} from '../statsig/statsig'
export function useAccountSwitcher() {
@@ -44,9 +45,14 @@ export function useAccountSwitcher() {
'circle-exclamation',
)
}
} catch (e) {
Toast.show('Sorry! We need you to enter your password.')
} catch (e: any) {
logger.error(`switch account: selectAccount failed`, {
message: e.message,
})
clearCurrentAccount() // back user out to login
setTimeout(() => {
Toast.show('Sorry! We need you to enter your password.')
}, 100)
}
},
[
+2 -2
View File
@@ -1,6 +1,6 @@
import React from 'react'
import {Alert, AppState, AppStateStatus} from 'react-native'
import app from 'react-native-version-number'
import {nativeBuildVersion} from 'expo-application'
import {
checkForUpdateAsync,
fetchUpdateAsync,
@@ -21,7 +21,7 @@ async function setExtraParams() {
isIOS ? 'ios-build-number' : 'android-build-number',
// Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
// This just ensures it gets passed as a string
`${app.buildVersion}`,
`${nativeBuildVersion}`,
)
await setExtraParamAsync(
'channel',
+3 -3
View File
@@ -4,7 +4,7 @@
*/
import {Platform} from 'react-native'
import app from 'react-native-version-number'
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
import * as info from 'expo-updates'
import {init} from 'sentry-expo'
@@ -21,7 +21,7 @@ const buildChannel = (info.channel || 'development') as
* - `dev`
* - `1.57.0`
*/
const release = app.appVersion ?? 'dev'
const release = nativeApplicationVersion ?? 'dev'
/**
* Examples:
@@ -33,7 +33,7 @@ const release = app.appVersion ?? 'dev'
* - `android.1.57.0.46`
*/
const dist = `${Platform.OS}.${release}${
app.buildVersion ? `.${app.buildVersion}` : ''
nativeBuildVersion ? `.${nativeBuildVersion}` : ''
}`
init({
+2 -2
View File
@@ -1,6 +1,6 @@
import {Share} from 'react-native'
// import * as Sharing from 'expo-sharing'
import Clipboard from '@react-native-clipboard/clipboard'
import {setStringAsync} from 'expo-clipboard'
import {isAndroid, isIOS} from 'platform/detection'
import * as Toast from '#/view/com/util/Toast'
@@ -19,7 +19,7 @@ export async function shareUrl(url: string) {
} 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)
setStringAsync(url)
Toast.show('Copied to clipboard')
}
}
+9
View File
@@ -45,10 +45,12 @@ export type LogEvents = {
'onboarding:moderation:nextPressed': {}
'onboarding:finished:nextPressed': {}
'feed:endReached': {
feedUrl: string
feedType: string
itemCount: number
}
'feed:refresh': {
feedUrl: string
feedType: string
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
}
@@ -65,6 +67,10 @@ export type LogEvents = {
logContext: 'Composer'
}
'post:like': {
doesLikerFollowPoster: boolean | undefined
doesPosterFollowLiker: boolean | undefined
likerClout: number | undefined
postClout: number | undefined
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:repost': {
@@ -77,6 +83,9 @@ export type LogEvents = {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'profile:follow': {
didBecomeMutual: boolean | undefined
followeeClout: number | undefined
followerClout: number | undefined
logContext:
| 'RecommendedFollowsItem'
| 'PostThreadItem'
+12
View File
@@ -43,6 +43,14 @@ export function attachRouteToLogEvents(
getCurrentRouteName = getRouteName
}
export function toClout(n: number | null | undefined): number | undefined {
if (n == null) {
return undefined
} else {
return Math.max(0, Math.round(Math.log(n)))
}
}
export function logEvent<E extends keyof LogEvents>(
eventName: E & string,
rawMetadata: LogEvents[E] & FlatJSONRecord,
@@ -78,6 +86,10 @@ function toStatsigUser(did: string | undefined) {
return {
userID,
platform: Platform.OS,
custom: {
// Need to specify here too for gating.
platform: Platform.OS,
},
}
}
+8
View File
@@ -19,6 +19,14 @@ if (process.env.NODE_ENV !== 'production') {
typeof msgOrError === 'string' &&
msgOrError.startsWith('Unexpected text node')
) {
if (
msgOrError ===
'Unexpected text node: . A text node cannot be a child of a <View>.'
) {
// This is due to a stray empty string.
// React already handles this fine, so RNW warning is a false positive. Ignore.
return
}
const err = new Error(msgOrError)
thrownErrors.add(err)
throw err
+22 -14
View File
@@ -5,12 +5,13 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf'
import {AccountList} from '#/components/AccountList'
import {Button} from '#/components/Button'
import {Button, ButtonText} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {FormContainer} from './FormContainer'
@@ -38,15 +39,22 @@ export const ChooseAccountForm = ({
setShowLoggedOut(false)
Toast.show(_(msg`Already signed in as @${account.handle}`))
} else {
await initSession(account)
logEvent('account:loggedIn', {
logContext: 'ChooseAccountForm',
withPassword: false,
})
track('Sign In', {resumedSession: true})
setTimeout(() => {
Toast.show(_(msg`Signed in as @${account.handle}`))
}, 100)
try {
await initSession(account)
logEvent('account:loggedIn', {
logContext: 'ChooseAccountForm',
withPassword: false,
})
track('Sign In', {resumedSession: true})
setTimeout(() => {
Toast.show(_(msg`Signed in as @${account.handle}`))
}, 100)
} catch (e: any) {
logger.error('choose account: initSession failed', {
message: e.message,
})
onSelectAccount(account)
}
}
} else {
onSelectAccount(account)
@@ -58,11 +66,11 @@ export const ChooseAccountForm = ({
return (
<FormContainer
testID="chooseAccountForm"
title={<Trans>Select account</Trans>}>
titleText={<Trans>Select account</Trans>}>
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Sign in as...</Trans>
</TextField.Label>
</TextField.LabelText>
<AccountList
onSelectAccount={onSelect}
onSelectOther={() => onSelectAccount()}
@@ -75,7 +83,7 @@ export const ChooseAccountForm = ({
color="secondary"
size="medium"
onPress={onPressBack}>
{_(msg`Back`)}
<ButtonText>{_(msg`Back`)}</ButtonText>
</Button>
<View style={[a.flex_1]} />
</View>
+5 -5
View File
@@ -83,11 +83,11 @@ export const ForgotPasswordForm = ({
return (
<FormContainer
testID="forgotPasswordForm"
title={<Trans>Reset password</Trans>}>
titleText={<Trans>Reset password</Trans>}>
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Hosting provider</Trans>
</TextField.Label>
</TextField.LabelText>
<HostingProvider
serviceUrl={serviceUrl}
onSelectServiceUrl={setServiceUrl}
@@ -95,9 +95,9 @@ export const ForgotPasswordForm = ({
/>
</View>
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Email address</Trans>
</TextField.Label>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={At} />
<TextField.Input
+4 -4
View File
@@ -6,12 +6,12 @@ import {Text} from '#/components/Typography'
export function FormContainer({
testID,
title,
titleText,
children,
style,
}: {
testID?: string
title?: React.ReactNode
titleText?: React.ReactNode
children: React.ReactNode
style?: StyleProp<ViewStyle>
}) {
@@ -21,9 +21,9 @@ export function FormContainer({
<View
testID={testID}
style={[a.gap_md, a.flex_1, !gtMobile && [a.px_lg, a.py_md], style]}>
{title && !gtMobile && (
{titleText && !gtMobile && (
<Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}>
{title}
{titleText}
</Text>
)}
{children}
+8 -6
View File
@@ -128,11 +128,11 @@ export const LoginForm = ({
const isReady = !!serviceDescription && !!identifier && !!password
return (
<FormContainer testID="loginForm" title={<Trans>Sign in</Trans>}>
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Hosting provider</Trans>
</TextField.Label>
</TextField.LabelText>
<HostingProvider
serviceUrl={serviceUrl}
onSelectServiceUrl={setServiceUrl}
@@ -140,9 +140,9 @@ export const LoginForm = ({
/>
</View>
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Account</Trans>
</TextField.Label>
</TextField.LabelText>
<View style={[a.gap_sm]}>
<TextField.Root>
<TextField.Icon icon={At} />
@@ -237,7 +237,9 @@ export const LoginForm = ({
color="secondary"
size="medium"
onPress={onPressRetryConnect}>
{_(msg`Retry`)}
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
) : !serviceDescription ? (
<>
+3 -3
View File
@@ -99,7 +99,7 @@ export const SetNewPasswordForm = ({
return (
<FormContainer
testID="setNewPasswordForm"
title={<Trans>Set new password</Trans>}>
titleText={<Trans>Set new password</Trans>}>
<Text style={[a.leading_snug, a.mb_sm]}>
<Trans>
You will receive an email with a "reset code." Enter that code here,
@@ -108,7 +108,7 @@ export const SetNewPasswordForm = ({
</Text>
<View>
<TextField.Label>Reset code</TextField.Label>
<TextField.LabelText>Reset code</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Ticket} />
<TextField.Input
@@ -131,7 +131,7 @@ export const SetNewPasswordForm = ({
</View>
<View>
<TextField.Label>New password</TextField.Label>
<TextField.LabelText>New password</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Lock} />
<TextField.Input
+5 -5
View File
@@ -40,7 +40,7 @@ import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filte
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import * as LabelingService from '#/components/LabelingServiceCard'
import {InlineLink, Link} from '#/components/Link'
import {InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
import {Text} from '#/components/Typography'
@@ -518,11 +518,11 @@ function PwiOptOut() {
msg`Discourage apps from showing my account to logged-out users`,
)}>
<Toggle.Switch />
<Toggle.Label style={[a.text_md, a.flex_1]}>
<Toggle.LabelText style={[a.text_md, a.flex_1]}>
<Trans>
Discourage apps from showing my account to logged-out users
</Trans>
</Toggle.Label>
</Toggle.LabelText>
</Toggle.Item>
{updateProfile.isPending && <Loader />}
@@ -545,9 +545,9 @@ function PwiOptOut() {
</Trans>
</Text>
<InlineLink to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
<InlineLinkText to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
<Trans>Learn more about what is public on Bluesky.</Trans>
</InlineLink>
</InlineLinkText>
</View>
</View>
)
+18 -20
View File
@@ -1,29 +1,27 @@
import React from 'react'
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {IS_DEV} from '#/env'
import {isWeb} from '#/platform/detection'
import {useOnboardingDispatch} from '#/state/shell'
import {
useTheme,
atoms as a,
useBreakpoints,
web,
native,
flatten,
TextStyleProp,
} from '#/alf'
import {P, leading, Text} from '#/components/Typography'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Button, ButtonIcon} from '#/components/Button'
import {ScrollView} from '#/view/com/util/Views'
import {createPortalGroup} from '#/components/Portal'
import {Context} from '#/screens/Onboarding/state'
import {
atoms as a,
flatten,
native,
TextStyleProp,
useBreakpoints,
useTheme,
web,
} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {createPortalGroup} from '#/components/Portal'
import {leading, P, Text} from '#/components/Typography'
import {IS_DEV} from '#/env'
const COL_WIDTH = 500
@@ -75,7 +73,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
onPress={() => onboardDispatch({type: 'skip'})}
// DEV ONLY
label="Clear onboarding state">
Clear
<ButtonText>Clear</ButtonText>
</Button>
</View>
)}
@@ -204,7 +202,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
)
}
export function Title({
export function TitleText({
children,
style,
}: React.PropsWithChildren<TextStyleProp>) {
@@ -224,7 +222,7 @@ export function Title({
)
}
export function Description({
export function DescriptionText({
children,
style,
}: React.PropsWithChildren<TextStyleProp>) {
@@ -1,18 +1,17 @@
import React from 'react'
import {View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {Image} from 'expo-image'
import {useLingui} from '@lingui/react'
import {LinearGradient} from 'expo-linear-gradient'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useTheme, atoms as a} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
import {Text} from '#/components/Typography'
import {RichText} from '#/components/RichText'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import {FeedConfig} from '#/screens/Onboarding/StepAlgoFeeds'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
function PrimaryFeedCardInner({
feed,
+6 -21
View File
@@ -6,9 +6,9 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
@@ -34,11 +34,6 @@ export const PRIMARY_FEEDS: FeedConfig[] = [
uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot',
gradient: tokens.gradients.midnight,
},
{
default: IS_PROD, // these feeds are only available in prod
uri: 'at://did:plc:wqowuobffl66jv3kpsvo7ak4/app.bsky.feed.generator/the-algorithm',
gradient: tokens.gradients.midnight,
},
]
const SECONDARY_FEEDS: FeedConfig[] = [
@@ -105,15 +100,15 @@ export function StepAlgoFeeds() {
<View style={[a.align_start]}>
<IconCircle icon={ListSparkle} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>Choose your main feeds</Trans>
</Title>
<Description>
</TitleText>
<DescriptionText>
<Trans>
Custom feeds built by the community bring you new experiences and help
you find the content you love.
</Trans>
</Description>
</DescriptionText>
<View style={[a.w_full, a.pb_2xl]}>
<Toggle.Group
@@ -130,16 +125,6 @@ export function StepAlgoFeeds() {
<Trans>We recommend our "Discover" feed:</Trans>
</Text>
<FeedCard config={PRIMARY_FEEDS[0]} />
<Text
style={[
a.text_md,
a.pt_4xl,
a.pb_lg,
t.atoms.text_contrast_medium,
]}>
<Trans>We also think you'll like "For You" by Skygaze:</Trans>
</Text>
<FeedCard config={PRIMARY_FEEDS[1]} />
</Toggle.Group>
<Toggle.Group
+10 -7
View File
@@ -4,15 +4,16 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useSetSaveFeedsMutation} from '#/state/queries/preferences'
import {getAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {
@@ -55,7 +56,9 @@ export function StepFinished() {
try {
await Promise.all([
bulkWriteFollows(suggestedAccountsStepResults.accountDids),
bulkWriteFollows(
suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID),
),
// these must be serial
(async () => {
await getAgent().setInterestsPref({tags: selectedInterests})
@@ -87,12 +90,12 @@ export function StepFinished() {
<View style={[a.align_start]}>
<IconCircle icon={Check} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>You're ready to go!</Trans>
</Title>
<Description>
</TitleText>
<DescriptionText>
<Trans>We hope you have a wonderful time. Remember, Bluesky is:</Trans>
</Description>
</DescriptionText>
<View style={[a.pt_5xl, a.gap_3xl]}>
<View style={[a.flex_row, a.align_center, a.w_full, a.gap_lg]}>
+8 -8
View File
@@ -10,9 +10,9 @@ import {
useSetFeedViewPreferencesMutation,
} from 'state/queries/preferences'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {atoms as a} from '#/alf'
@@ -58,12 +58,12 @@ export function StepFollowingFeed() {
<View style={[a.align_start]}>
<IconCircle icon={FilterTimeline} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>Your default feed is "Following"</Trans>
</Title>
<Description style={[a.mb_md]}>
</TitleText>
<DescriptionText style={[a.mb_md]}>
<Trans>It shows posts from the people you follow as they happen.</Trans>
</Description>
</DescriptionText>
<View style={[a.w_full]}>
<Toggle.Item
@@ -139,9 +139,9 @@ export function StepFollowingFeed() {
</Toggle.Item>
</View>
<Description style={[a.mt_lg]}>
<DescriptionText style={[a.mt_lg]}>
<Trans>You can change these settings later.</Trans>
</Description>
</DescriptionText>
<OnboardingControls.Portal>
<Button
@@ -11,9 +11,9 @@ import {logger} from '#/logger'
import {getAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {ApiResponseMap, Context} from '#/screens/Onboarding/state'
import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton'
@@ -163,8 +163,8 @@ export function StepInterests() {
]}
/>
<Title>{title}</Title>
<Description>{description}</Description>
<TitleText>{title}</TitleText>
<DescriptionText>{description}</DescriptionText>
<View style={[a.w_full, a.pt_2xl]}>
{isLoading ? (
@@ -113,15 +113,15 @@ export function AdultContentEnabledPref({
)}
<Prompt.Outer control={prompt}>
<Prompt.Title>
<Prompt.TitleText>
<Trans>Adult Content</Trans>
</Prompt.Title>
<Prompt.Description>
</Prompt.TitleText>
<Prompt.DescriptionText>
<Trans>
Due to Apple policies, adult content can only be enabled on the web
after completing sign up.
</Trans>
</Prompt.Description>
</Prompt.DescriptionText>
<Prompt.Actions>
<Prompt.Action onPress={() => prompt.close()} cta={_(msg`OK`)} />
</Prompt.Actions>
@@ -1,17 +1,17 @@
import React from 'react'
import {View} from 'react-native'
import {LabelPreference, InterpretedLabelValueDefinition} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {
usePreferencesQuery,
usePreferencesSetContentLabelMutation,
} from '#/state/queries/preferences'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {Text} from '#/components/Typography'
export function ModerationOption({
labelValueDefinition,
@@ -83,13 +83,13 @@ export function ModerationOption({
values={[visibility ?? 'hide']}
onChange={onChange}>
<ToggleButton.Button name="ignore" label={labels.show}>
{labels.show}
<ToggleButton.ButtonText>{labels.show}</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="warn" label={labels.warn}>
{labels.warn}
<ToggleButton.ButtonText>{labels.warn}</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="hide" label={labels.hide}>
{labels.hide}
<ToggleButton.ButtonText>{labels.hide}</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
)}
@@ -9,9 +9,9 @@ import {logEvent} from '#/lib/statsig/statsig'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {usePreferencesSetAdultContentMutation} from 'state/queries/preferences'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {AdultContentEnabledPref} from '#/screens/Onboarding/StepModeration/AdultContentEnabledPref'
@@ -56,14 +56,14 @@ export function StepModeration() {
<View style={[a.align_start]}>
<IconCircle icon={EyeSlash} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>You're in control</Trans>
</Title>
<Description style={[a.mb_xl]}>
</TitleText>
<DescriptionText style={[a.mb_xl]}>
<Trans>
Select what you want to see (or not see), and well handle the rest.
</Trans>
</Description>
</DescriptionText>
{!preferences ? (
<View style={[a.pt_md]}>
@@ -10,9 +10,9 @@ import {capitalize} from '#/lib/strings/capitalize'
import {useModerationOpts} from '#/state/queries/preferences'
import {useProfilesQuery} from '#/state/queries/profile'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {
@@ -136,16 +136,16 @@ export function StepSuggestedAccounts() {
<View style={[a.align_start]}>
<IconCircle icon={At} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>Here are some accounts for you to follow</Trans>
</Title>
<Description>
</TitleText>
<DescriptionText>
{state.interestsStepResults.selectedInterests.length ? (
<Trans>Based on your interest in {interestsText}</Trans>
) : (
<Trans>These are popular accounts you might like:</Trans>
)}
</Description>
</DescriptionText>
<View style={[a.w_full, a.pt_xl]}>
{isLoading ? (
+6 -6
View File
@@ -9,9 +9,9 @@ import {capitalize} from '#/lib/strings/capitalize'
import {IS_TEST_USER} from 'lib/constants'
import {useSession} from 'state/session'
import {
Description,
DescriptionText,
OnboardingControls,
Title,
TitleText,
} from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state'
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
@@ -76,10 +76,10 @@ export function StepTopicalFeeds() {
<View style={[a.align_start]}>
<IconCircle icon={ListMagnifyingGlass} style={[a.mb_2xl]} />
<Title>
<TitleText>
<Trans>Feeds can be topical as well!</Trans>
</Title>
<Description>
</TitleText>
<DescriptionText>
{state.interestsStepResults.selectedInterests.length ? (
<Trans>
Here are some topical feeds based on your interests: {interestsText}
@@ -91,7 +91,7 @@ export function StepTopicalFeeds() {
many as you like.
</Trans>
)}
</Description>
</DescriptionText>
<View style={[a.w_full, a.pb_2xl, a.pt_2xl]}>
<Toggle.Group
+7 -8
View File
@@ -1,17 +1,16 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Shadow} from '#/state/cache/types'
import {pluralize} from '#/lib/strings/helpers'
import {Shadow} from '#/state/cache/types'
import {makeProfileLink} from 'lib/routes/links'
import {formatCount} from 'view/com/util/numeric/format'
import {atoms as a, useTheme} from '#/alf'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {InlineLink} from '#/components/Link'
export function ProfileHeaderMetrics({
profile,
@@ -28,7 +27,7 @@ export function ProfileHeaderMetrics({
<View
style={[a.flex_row, a.gap_sm, a.align_center, a.pb_md]}
pointerEvents="box-none">
<InlineLink
<InlineLinkText
testID="profileHeaderFollowersButton"
style={[a.flex_row, t.atoms.text]}
to={makeProfileLink(profile, 'followers')}
@@ -37,8 +36,8 @@ export function ProfileHeaderMetrics({
<Text style={[t.atoms.text_contrast_medium, a.text_md]}>
{pluralizedFollowers}
</Text>
</InlineLink>
<InlineLink
</InlineLinkText>
<InlineLinkText
testID="profileHeaderFollowsButton"
style={[a.flex_row, t.atoms.text]}
to={makeProfileLink(profile, 'follows')}
@@ -49,7 +48,7 @@ export function ProfileHeaderMetrics({
following
</Text>
</Trans>
</InlineLink>
</InlineLinkText>
<Text style={[a.font_bold, t.atoms.text, a.text_md]}>
{formatCount(profile.postsCount || 0)}{' '}
<Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
@@ -316,13 +316,13 @@ function CantSubscribePrompt({
const {_} = useLingui()
return (
<Prompt.Outer control={control}>
<Prompt.Title>Unable to subscribe</Prompt.Title>
<Prompt.Description>
<Prompt.TitleText>Unable to subscribe</Prompt.TitleText>
<Prompt.DescriptionText>
<Trans>
We're sorry! You can only subscribe to ten labelers, and you've
reached your limit of ten.
</Trans>
</Prompt.Description>
</Prompt.DescriptionText>
<Prompt.Actions>
<Prompt.Action onPress={control.close} cta={_(msg`OK`)} />
</Prompt.Actions>
@@ -10,7 +10,9 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {Shadow} from '#/state/cache/types'
import {useModalControls} from '#/state/modals'
import {
@@ -78,6 +80,9 @@ let ProfileHeaderStandard = ({
})
}, [track, openModal, profile])
const autoExpandSuggestionsOnProfileFollow = useGate(
'autoexpand_suggestions_on_profile_follow',
)
const onPressFollow = () => {
requireAuth(async () => {
try {
@@ -91,6 +96,9 @@ let ProfileHeaderStandard = ({
)}`,
),
)
if (isWeb && autoExpandSuggestionsOnProfileFollow) {
setShowSuggestedFollows(true)
}
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to follow', {message: String(e)})
+11 -14
View File
@@ -1,23 +1,22 @@
import React, {memo} from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NavigationProp} from 'lib/routes/types'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {BACK_HITSLOP} from 'lib/constants'
import {useSession} from '#/state/session'
import {Shadow} from '#/state/cache/types'
import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
import {useNavigation} from '@react-navigation/native'
import {atoms as a, useTheme} from '#/alf'
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
import {BlurView} from 'view/com/util/BlurView'
import {Shadow} from '#/state/cache/types'
import {ProfileImageLightbox, useLightboxControls} from '#/state/lightbox'
import {useSession} from '#/state/session'
import {BACK_HITSLOP} from 'lib/constants'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types'
import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {UserBanner} from 'view/com/util/UserBanner'
import {atoms as a, useTheme} from '#/alf'
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
interface Props {
@@ -97,9 +96,7 @@ let ProfileHeaderShell = ({
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<View style={styles.backBtnWrapper}>
<BlurView style={styles.backBtn} blurType="dark">
<FontAwesomeIcon size={18} icon="angle-left" color="white" />
</BlurView>
<FontAwesomeIcon size={18} icon="angle-left" color="white" />
</View>
</TouchableWithoutFeedback>
)}
+5 -5
View File
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {InlineLink} from '#/components/Link'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
export const Policies = ({
@@ -45,16 +45,16 @@ export const Policies = ({
const els = []
if (tos) {
els.push(
<InlineLink key="tos" to={tos}>
<InlineLinkText key="tos" to={tos}>
{_(msg`Terms of Service`)}
</InlineLink>,
</InlineLinkText>,
)
}
if (pp) {
els.push(
<InlineLink key="pp" to={pp}>
<InlineLinkText key="pp" to={pp}>
{_(msg`Privacy Policy`)}
</InlineLink>,
</InlineLinkText>,
)
}
if (els.length === 2) {
+10 -10
View File
@@ -36,9 +36,9 @@ export function StepInfo() {
<View style={[a.gap_md]}>
<FormError error={state.error} />
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Hosting provider</Trans>
</TextField.Label>
</TextField.LabelText>
<HostingProvider
serviceUrl={state.serviceUrl}
onSelectServiceUrl={v =>
@@ -54,9 +54,9 @@ export function StepInfo() {
<>
{state.serviceDescription.inviteCodeRequired && (
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Invite code</Trans>
</TextField.Label>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Ticket} />
<TextField.Input
@@ -76,9 +76,9 @@ export function StepInfo() {
</View>
)}
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Email</Trans>
</TextField.Label>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Envelope} />
<TextField.Input
@@ -97,9 +97,9 @@ export function StepInfo() {
</TextField.Root>
</View>
<View>
<TextField.Label>
<TextField.LabelText>
<Trans>Password</Trans>
</TextField.Label>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Lock} />
<TextField.Input
@@ -117,9 +117,9 @@ export function StepInfo() {
</TextField.Root>
</View>
<View>
<DateField.Label>
<DateField.LabelText>
<Trans>Your birth date</Trans>
</DateField.Label>
</DateField.LabelText>
<DateField.DateField
testID="date"
value={DateField.utils.toSimpleDateString(state.dateOfBirth)}
+3 -3
View File
@@ -24,7 +24,7 @@ import {StepInfo} from '#/screens/Signup/StepInfo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {InlineLink} from '#/components/Link'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
export function Signup({onPressBack}: {onPressBack: () => void}) {
@@ -215,9 +215,9 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
<View style={[a.w_full, a.py_lg]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>Having trouble?</Trans>{' '}
<InlineLink to={FEEDBACK_FORM_URL({email: state.email})}>
<InlineLinkText to={FEEDBACK_FORM_URL({email: state.email})}>
<Trans>Contact support</Trans>
</InlineLink>
</InlineLinkText>
</Text>
</View>
</View>
-8
View File
@@ -3,7 +3,6 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {EmbedPlayerSource} from '#/lib/strings/embed-player'
import {GalleryModel} from '#/state/models/media/gallery'
import {ImageModel} from '#/state/models/media/image'
import {ThreadgateSetting} from '../queries/threadgate'
@@ -125,12 +124,6 @@ export interface LinkWarningModal {
share?: boolean
}
export interface EmbedConsentModal {
name: 'embed-consent'
source: EmbedPlayerSource
onAccept: () => void
}
export interface InAppBrowserConsentModal {
name: 'in-app-browser-consent'
href: string
@@ -169,7 +162,6 @@ export type Modal =
// Generic
| LinkWarningModal
| EmbedConsentModal
| InAppBrowserConsentModal
const ModalContext = React.createContext<{
+7 -6
View File
@@ -1,11 +1,12 @@
import EventEmitter from 'eventemitter3'
import {logger} from '#/logger'
import {defaults, Schema} from '#/state/persisted/schema'
import {migrate} from '#/state/persisted/legacy'
import * as store from '#/state/persisted/store'
import BroadcastChannel from '#/lib/broadcast'
export type {Schema, PersistedAccount} from '#/state/persisted/schema'
import BroadcastChannel from '#/lib/broadcast'
import {logger} from '#/logger'
import {migrate} from '#/state/persisted/legacy'
import {defaults, Schema} from '#/state/persisted/schema'
import * as store from '#/state/persisted/store'
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
export {defaults} from '#/state/persisted/schema'
const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL')
+19 -2
View File
@@ -1,9 +1,13 @@
import {z} from 'zod'
import {deviceLocales} from '#/platform/detection'
const externalEmbedOptions = ['show', 'hide'] as const
// only data needed for rendering account page
/**
* A account persisted to storage. Stored in the `accounts[]` array. Contains
* base account info and access tokens.
*/
const accountSchema = z.object({
service: z.string(),
did: z.string(),
@@ -16,12 +20,25 @@ const accountSchema = z.object({
})
export type PersistedAccount = z.infer<typeof accountSchema>
/**
* The current account. Stored in the `currentAccount` field.
*
* In previous versions, this included tokens and other info. Now, it's used
* only to reference the `did` field, and all other fields are marked as
* optional. They should be considered deprecated and not used, but are kept
* here for backwards compat.
*/
const currentAccountScheme = accountSchema.extend({
service: z.string().optional(),
handle: z.string().optional(),
})
export const schema = z.object({
colorMode: z.enum(['system', 'light', 'dark']),
darkTheme: z.enum(['dim', 'dark']).optional(),
session: z.object({
accounts: z.array(accountSchema),
currentAccount: accountSchema.optional(),
currentAccount: currentAccountScheme.optional(),
}),
reminders: z.object({
lastEmailConfirm: z.string().optional(),
+14 -7
View File
@@ -2,22 +2,29 @@ import {AppBskyActorDefs} from '@atproto/api'
import {QueryClient, useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'actor-search'
export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
export function useActorSearch(prefix: string) {
export function useActorSearch({
query,
enabled,
}: {
query: string
enabled?: boolean
}) {
const agent = useAgent()
return useQuery<AppBskyActorDefs.ProfileView[]>({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(prefix || ''),
queryKey: RQKEY(query || ''),
async queryFn() {
const res = await getAgent().searchActors({
q: prefix,
const res = await agent.searchActors({
q: query,
})
return res.data.actors
},
enabled: !!prefix,
enabled: enabled && !!query,
})
}
+3 -1
View File
@@ -1,7 +1,9 @@
import {BskyAgent} from '@atproto/api'
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
export const PUBLIC_BSKY_AGENT = new BskyAgent({
service: 'https://public.api.bsky.app',
service: PUBLIC_BSKY_SERVICE,
})
export const STALE = {
+10 -6
View File
@@ -5,7 +5,7 @@ import {z} from 'zod'
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
import {STALE} from '#/state/queries'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const labelerInfoQueryKeyRoot = 'labeler-info'
export const labelerInfoQueryKey = (did: string) => [
@@ -31,11 +31,12 @@ export function useLabelerInfoQuery({
did?: string
enabled?: boolean
}) {
const agent = useAgent()
return useQuery({
enabled: !!did && enabled !== false,
queryKey: labelerInfoQueryKey(did as string),
queryFn: async () => {
const res = await getAgent().app.bsky.labeler.getServices({
const res = await agent.app.bsky.labeler.getServices({
dids: [did as string],
detailed: true,
})
@@ -45,24 +46,26 @@ export function useLabelerInfoQuery({
}
export function useLabelersInfoQuery({dids}: {dids: string[]}) {
const agent = useAgent()
return useQuery({
enabled: !!dids.length,
queryKey: labelersInfoQueryKey(dids),
queryFn: async () => {
const res = await getAgent().app.bsky.labeler.getServices({dids})
const res = await agent.app.bsky.labeler.getServices({dids})
return res.data.views as AppBskyLabelerDefs.LabelerView[]
},
})
}
export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
const agent = useAgent()
return useQuery({
enabled: !!dids.length,
queryKey: labelersDetailedInfoQueryKey(dids),
gcTime: 1000 * 60 * 60 * 6, // 6 hours
staleTime: STALE.MINUTES.ONE,
queryFn: async () => {
const res = await getAgent().app.bsky.labeler.getServices({
const res = await agent.app.bsky.labeler.getServices({
dids,
detailed: true,
})
@@ -73,6 +76,7 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
export function useLabelerSubscriptionMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) {
@@ -83,9 +87,9 @@ export function useLabelerSubscriptionMutation() {
}).parse({did, subscribe})
if (subscribe) {
await getAgent().addLabeler(did)
await agent.addLabeler(did)
} else {
await getAgent().removeLabeler(did)
await agent.removeLabeler(did)
}
},
onSuccess() {
+3 -2
View File
@@ -7,7 +7,7 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'list-members'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export function useListMembersQuery(uri: string) {
const agent = useAgent()
return useInfiniteQuery<
AppBskyGraphGetList.OutputSchema,
Error,
@@ -26,7 +27,7 @@ export function useListMembersQuery(uri: string) {
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(uri),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getList({
const res = await agent.app.bsky.graph.getList({
list: uri,
limit: PAGE_SIZE,
cursor: pageParam,
+3 -2
View File
@@ -6,13 +6,14 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'my-blocked-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyBlockedAccountsQuery() {
const agent = useAgent()
return useInfiniteQuery<
AppBskyGraphGetBlocks.OutputSchema,
Error,
@@ -22,7 +23,7 @@ export function useMyBlockedAccountsQuery() {
>({
queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getBlocks({
const res = await agent.app.bsky.graph.getBlocks({
limit: 30,
cursor: pageParam,
})
+3 -2
View File
@@ -6,13 +6,14 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'my-muted-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyMutedAccountsQuery() {
const agent = useAgent()
return useInfiniteQuery<
AppBskyGraphGetMutes.OutputSchema,
Error,
@@ -22,7 +23,7 @@ export function useMyMutedAccountsQuery() {
>({
queryKey: RQKEY(),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getMutes({
const res = await agent.app.bsky.graph.getMutes({
limit: 30,
cursor: pageParam,
})
+3 -2
View File
@@ -19,7 +19,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {AuthorFeedAPI} from 'lib/api/feed/author'
import {CustomFeedAPI} from 'lib/api/feed/custom'
import {FollowingFeedAPI} from 'lib/api/feed/following'
@@ -111,6 +111,7 @@ export function usePostFeedQuery(
result: InfiniteData<FeedPage>
} | null>(null)
const lastPageCountRef = useRef(0)
const agent = useAgent()
// Make sure this doesn't invalidate unless really needed.
const selectArgs = React.useMemo(
@@ -153,7 +154,7 @@ export function usePostFeedQuery(
* moderations happen later, which results in some posts being shown and
* some not.
*/
if (!getAgent().session) {
if (!agent.session) {
assertSomePostsPassModeration(res.feed)
}
+3 -2
View File
@@ -6,7 +6,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'liked-by'
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function useLikedByQuery(resolvedUri: string | undefined) {
const agent = useAgent()
return useInfiniteQuery<
AppBskyFeedGetLikes.OutputSchema,
Error,
@@ -25,7 +26,7 @@ export function useLikedByQuery(resolvedUri: string | undefined) {
>({
queryKey: RQKEY(resolvedUri || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().getLikes({
const res = await agent.getLikes({
uri: resolvedUri || '',
limit: PAGE_SIZE,
cursor: pageParam,
+3 -2
View File
@@ -6,7 +6,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -16,6 +16,7 @@ const RQKEY_ROOT = 'post-reposted-by'
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
export function usePostRepostedByQuery(resolvedUri: string | undefined) {
const agent = useAgent()
return useInfiniteQuery<
AppBskyFeedGetRepostedBy.OutputSchema,
Error,
@@ -25,7 +26,7 @@ export function usePostRepostedByQuery(resolvedUri: string | undefined) {
>({
queryKey: RQKEY(resolvedUri || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().getRepostedBy({
const res = await agent.getRepostedBy({
uri: resolvedUri || '',
limit: PAGE_SIZE,
cursor: pageParam,
+3 -2
View File
@@ -7,7 +7,7 @@ import {
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
import {precacheThreadPostProfiles} from './profile'
@@ -65,11 +65,12 @@ export type ThreadNode =
export function usePostThreadQuery(uri: string | undefined) {
const queryClient = useQueryClient()
const agent = useAgent()
return useQuery<ThreadNode, Error>({
gcTime: 0,
queryKey: RQKEY(uri || ''),
async queryFn() {
const res = await getAgent().getPostThread({uri: uri!})
const res = await agent.getPostThread({uri: uri!})
if (res.success) {
const nodes = responseToThreadNodes(res.data.thread)
precacheThreadPostProfiles(queryClient, nodes)
+34 -8
View File
@@ -1,13 +1,14 @@
import {useCallback} from 'react'
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {track} from '#/lib/analytics/analytics'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {Shadow} from '#/state/cache/types'
import {getAgent} from '#/state/session'
import {getAgent, useSession} from '#/state/session'
import {findProfileQueryData} from './profile'
const RQKEY_ROOT = 'post'
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
@@ -68,7 +69,7 @@ export function usePostLikeMutationQueue(
const postUri = post.uri
const postCid = post.cid
const initialLikeUri = post.viewer?.like
const likeMutation = usePostLikeMutation(logContext)
const likeMutation = usePostLikeMutation(logContext, post)
const unlikeMutation = usePostUnlikeMutation(logContext)
const queueToggle = useToggleMutationQueue({
@@ -117,15 +118,40 @@ export function usePostLikeMutationQueue(
return [queueLike, queueUnlike]
}
function usePostLikeMutation(logContext: LogEvents['post:like']['logContext']) {
function usePostLikeMutation(
logContext: LogEvents['post:like']['logContext'],
post: Shadow<AppBskyFeedDefs.PostView>,
) {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
const postAuthor = post.author
return useMutation<
{uri: string}, // responds with the uri of the like
Error,
{uri: string; cid: string} // the post's uri and cid
>({
mutationFn: post => {
logEvent('post:like', {logContext})
return getAgent().like(post.uri, post.cid)
mutationFn: ({uri, cid}) => {
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
if (currentAccount) {
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
logEvent('post:like', {
logContext,
doesPosterFollowLiker: postAuthor.viewer
? Boolean(postAuthor.viewer.followedBy)
: undefined,
doesLikerFollowPoster: postAuthor.viewer
? Boolean(postAuthor.viewer.following)
: undefined,
likerClout: toClout(ownProfile?.followersCount),
postClout:
post.likeCount != null &&
post.repostCount != null &&
post.replyCount != null
? toClout(post.likeCount + post.repostCount + post.replyCount)
: undefined,
})
return getAgent().like(uri, cid)
},
onSuccess() {
track('Post:Like')
+32 -18
View File
@@ -22,7 +22,7 @@ import {
ThreadViewPreferences,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
import {saveLabelers} from '#/state/session/agent-config'
export * from '#/state/queries/preferences/const'
@@ -33,14 +33,13 @@ const preferencesQueryKeyRoot = 'getPreferences'
export const preferencesQueryKey = [preferencesQueryKeyRoot]
export function usePreferencesQuery() {
const agent = useAgent()
return useQuery({
staleTime: STALE.SECONDS.FIFTEEN,
structuralSharing: true,
refetchOnWindowFocus: true,
queryKey: preferencesQueryKey,
queryFn: async () => {
const agent = getAgent()
if (agent.session?.did === undefined) {
return DEFAULT_LOGGED_OUT_PREFERENCES
} else {
@@ -118,10 +117,11 @@ export function useModerationOpts() {
export function useClearPreferencesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async () => {
await getAgent().app.bsky.actor.putPreferences({preferences: []})
await agent.app.bsky.actor.putPreferences({preferences: []})
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -132,6 +132,7 @@ export function useClearPreferencesMutation() {
export function usePreferencesSetContentLabelMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<
void,
@@ -139,7 +140,7 @@ export function usePreferencesSetContentLabelMutation() {
{label: string; visibility: LabelPreference; labelerDid: string | undefined}
>({
mutationFn: async ({label, visibility, labelerDid}) => {
await getAgent().setContentLabelPref(label, visibility, labelerDid)
await agent.setContentLabelPref(label, visibility, labelerDid)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -150,6 +151,7 @@ export function usePreferencesSetContentLabelMutation() {
export function useSetContentLabelMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async ({
@@ -161,7 +163,7 @@ export function useSetContentLabelMutation() {
visibility: LabelPreference
labelerDid?: string
}) => {
await getAgent().setContentLabelPref(label, visibility, labelerDid)
await agent.setContentLabelPref(label, visibility, labelerDid)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -172,10 +174,11 @@ export function useSetContentLabelMutation() {
export function usePreferencesSetAdultContentMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, {enabled: boolean}>({
mutationFn: async ({enabled}) => {
await getAgent().setAdultContentEnabled(enabled)
await agent.setAdultContentEnabled(enabled)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -186,10 +189,11 @@ export function usePreferencesSetAdultContentMutation() {
export function usePreferencesSetBirthDateMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, {birthDate: Date}>({
mutationFn: async ({birthDate}: {birthDate: Date}) => {
await getAgent().setPersonalDetails({birthDate: birthDate.toISOString()})
await agent.setPersonalDetails({birthDate: birthDate.toISOString()})
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -200,10 +204,11 @@ export function usePreferencesSetBirthDateMutation() {
export function useSetFeedViewPreferencesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
mutationFn: async prefs => {
await getAgent().setFeedViewPrefs('home', prefs)
await agent.setFeedViewPrefs('home', prefs)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -214,10 +219,11 @@ export function useSetFeedViewPreferencesMutation() {
export function useSetThreadViewPreferencesMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, Partial<ThreadViewPreferences>>({
mutationFn: async prefs => {
await getAgent().setThreadViewPrefs(prefs)
await agent.setThreadViewPrefs(prefs)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -228,6 +234,7 @@ export function useSetThreadViewPreferencesMutation() {
export function useSetSaveFeedsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<
void,
@@ -235,7 +242,7 @@ export function useSetSaveFeedsMutation() {
Pick<UsePreferencesQueryResponse['feeds'], 'saved' | 'pinned'>
>({
mutationFn: async ({saved, pinned}) => {
await getAgent().setSavedFeeds(saved, pinned)
await agent.setSavedFeeds(saved, pinned)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -246,10 +253,11 @@ export function useSetSaveFeedsMutation() {
export function useSaveFeedMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, {uri: string}>({
mutationFn: async ({uri}) => {
await getAgent().addSavedFeed(uri)
await agent.addSavedFeed(uri)
track('CustomFeed:Save')
// triggers a refetch
await queryClient.invalidateQueries({
@@ -261,10 +269,11 @@ export function useSaveFeedMutation() {
export function useRemoveFeedMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, {uri: string}>({
mutationFn: async ({uri}) => {
await getAgent().removeSavedFeed(uri)
await agent.removeSavedFeed(uri)
track('CustomFeed:Unsave')
// triggers a refetch
await queryClient.invalidateQueries({
@@ -276,10 +285,11 @@ export function useRemoveFeedMutation() {
export function usePinFeedMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, {uri: string}>({
mutationFn: async ({uri}) => {
await getAgent().addPinnedFeed(uri)
await agent.addPinnedFeed(uri)
track('CustomFeed:Pin', {uri})
// triggers a refetch
await queryClient.invalidateQueries({
@@ -291,10 +301,11 @@ export function usePinFeedMutation() {
export function useUnpinFeedMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, unknown, {uri: string}>({
mutationFn: async ({uri}) => {
await getAgent().removePinnedFeed(uri)
await agent.removePinnedFeed(uri)
track('CustomFeed:Unpin', {uri})
// triggers a refetch
await queryClient.invalidateQueries({
@@ -306,10 +317,11 @@ export function useUnpinFeedMutation() {
export function useUpsertMutedWordsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
await getAgent().upsertMutedWords(mutedWords)
await agent.upsertMutedWords(mutedWords)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -320,10 +332,11 @@ export function useUpsertMutedWordsMutation() {
export function useUpdateMutedWordMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
await getAgent().updateMutedWord(mutedWord)
await agent.updateMutedWord(mutedWord)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -334,10 +347,11 @@ export function useUpdateMutedWordMutation() {
export function useRemoveMutedWordMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation({
mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
await getAgent().removeMutedWord(mutedWord)
await agent.removeMutedWord(mutedWord)
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
+3 -2
View File
@@ -6,7 +6,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -15,6 +15,7 @@ const RQKEY_ROOT = 'profile-followers'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileFollowersQuery(did: string | undefined) {
const agent = useAgent()
return useInfiniteQuery<
AppBskyGraphGetFollowers.OutputSchema,
Error,
@@ -24,7 +25,7 @@ export function useProfileFollowersQuery(did: string | undefined) {
>({
queryKey: RQKEY(did || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getFollowers({
const res = await agent.app.bsky.graph.getFollowers({
actor: did || '',
limit: PAGE_SIZE,
cursor: pageParam,
+3 -2
View File
@@ -7,7 +7,7 @@ import {
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
@@ -17,6 +17,7 @@ const RQKEY_ROOT = 'profile-follows'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileFollowsQuery(did: string | undefined) {
const agent = useAgent()
return useInfiniteQuery<
AppBskyGraphGetFollows.OutputSchema,
Error,
@@ -27,7 +28,7 @@ export function useProfileFollowsQuery(did: string | undefined) {
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY(did || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await getAgent().app.bsky.graph.getFollows({
const res = await agent.app.bsky.graph.getFollows({
actor: did || '',
limit: PAGE_SIZE,
cursor: pageParam,
+47 -17
View File
@@ -20,12 +20,12 @@ import {track} from '#/lib/analytics/analytics'
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {Shadow} from '#/state/cache/types'
import {STALE} from '#/state/queries'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {updateProfileShadow} from '../cache/profile-shadow'
import {getAgent, useSession} from '../session'
import {getAgent, useAgent, useSession} from '../session'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
import {ThreadNode} from './post-thread'
@@ -53,6 +53,7 @@ export function useProfileQuery({
staleTime?: number
}) {
const queryClient = useQueryClient()
const agent = useAgent()
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
// WARNING
// this staleTime is load-bearing
@@ -62,7 +63,7 @@ export function useProfileQuery({
refetchOnWindowFocus: true,
queryKey: RQKEY(did ?? ''),
queryFn: async () => {
const res = await getAgent().getProfile({actor: did ?? ''})
const res = await agent.getProfile({actor: did ?? ''})
return res.data
},
placeholderData: () => {
@@ -77,11 +78,12 @@ export function useProfileQuery({
}
export function useProfilesQuery({handles}: {handles: string[]}) {
const agent = useAgent()
return useQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: profilesQueryKey(handles),
queryFn: async () => {
const res = await getAgent().getProfiles({actors: handles})
const res = await agent.getProfiles({actors: handles})
return res.data
},
})
@@ -89,17 +91,18 @@ export function useProfilesQuery({handles}: {handles: string[]}) {
export function usePrefetchProfileQuery() {
const queryClient = useQueryClient()
const agent = useAgent()
const prefetchProfileQuery = useCallback(
(did: string) => {
queryClient.prefetchQuery({
queryKey: RQKEY(did),
queryFn: async () => {
const res = await getAgent().getProfile({actor: did || ''})
const res = await agent.getProfile({actor: did || ''})
return res.data
},
})
},
[queryClient],
[queryClient, agent],
)
return prefetchProfileQuery
}
@@ -115,6 +118,7 @@ interface ProfileUpdateParams {
}
export function useProfileUpdateMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, Error, ProfileUpdateParams>({
mutationFn: async ({
profile,
@@ -123,7 +127,7 @@ export function useProfileUpdateMutation() {
newUserBanner,
checkCommitted,
}) => {
await getAgent().upsertProfile(async existing => {
await agent.upsertProfile(async existing => {
existing = existing || {}
if (typeof updates === 'function') {
existing = updates(existing)
@@ -202,7 +206,7 @@ export function useProfileFollowMutationQueue(
const queryClient = useQueryClient()
const did = profile.did
const initialFollowingUri = profile.viewer?.following
const followMutation = useProfileFollowMutation(logContext)
const followMutation = useProfileFollowMutation(logContext, profile)
const unfollowMutation = useProfileUnfollowMutation(logContext)
const queueToggle = useToggleMutationQueue({
@@ -252,11 +256,25 @@ export function useProfileFollowMutationQueue(
function useProfileFollowMutation(
logContext: LogEvents['profile:follow']['logContext'],
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
) {
const {currentAccount, agent} = useSession()
const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
logEvent('profile:follow', {logContext})
return await getAgent().follow(did)
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
if (currentAccount) {
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
logEvent('profile:follow', {
logContext,
didBecomeMutual: profile.viewer
? Boolean(profile.viewer.followedBy)
: undefined,
followeeClout: toClout(profile.followersCount),
followerClout: toClout(ownProfile?.followersCount),
})
return await agent.follow(did)
},
onSuccess(data, variables) {
track('Profile:Follow', {username: variables.did})
@@ -267,11 +285,12 @@ function useProfileFollowMutation(
function useProfileUnfollowMutation(
logContext: LogEvents['profile:unfollow']['logContext'],
) {
const agent = useAgent()
return useMutation<void, Error, {did: string; followUri: string}>({
mutationFn: async ({followUri}) => {
logEvent('profile:unfollow', {logContext})
track('Profile:Unfollow', {username: followUri})
return await getAgent().deleteFollow(followUri)
return await agent.deleteFollow(followUri)
},
})
}
@@ -327,9 +346,10 @@ export function useProfileMuteMutationQueue(
function useProfileMuteMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => {
await getAgent().mute(did)
await agent.mute(did)
},
onSuccess() {
queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
@@ -339,9 +359,10 @@ function useProfileMuteMutation() {
function useProfileUnmuteMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => {
await getAgent().unmute(did)
await agent.unmute(did)
},
onSuccess() {
queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
@@ -404,14 +425,14 @@ export function useProfileBlockMutationQueue(
}
function useProfileBlockMutation() {
const {currentAccount} = useSession()
const {currentAccount, agent} = useSession()
const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
if (!currentAccount) {
throw new Error('Not signed in')
}
return await getAgent().app.bsky.graph.block.create(
return await agent.app.bsky.graph.block.create(
{repo: currentAccount.did},
{subject: did, createdAt: new Date().toISOString()},
)
@@ -424,7 +445,7 @@ function useProfileBlockMutation() {
}
function useProfileUnblockMutation() {
const {currentAccount} = useSession()
const {currentAccount, agent} = useSession()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string; blockUri: string}>({
mutationFn: async ({blockUri}) => {
@@ -432,7 +453,7 @@ function useProfileUnblockMutation() {
throw new Error('Not signed in')
}
const {rkey} = new AtUri(blockUri)
await getAgent().app.bsky.graph.block.delete({
await agent.app.bsky.graph.block.delete({
repo: currentAccount.did,
rkey,
})
@@ -530,3 +551,12 @@ export function* findAllProfilesInQueryData(
}
}
}
export function findProfileQueryData(
queryClient: QueryClient,
did: string,
): AppBskyActorDefs.ProfileViewDetailed | undefined {
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewDetailed>(
RQKEY(did),
)
}
+10 -9
View File
@@ -16,7 +16,7 @@ import {
import {STALE} from '#/state/queries'
import {useModerationOpts} from '#/state/queries/preferences'
import {getAgent, useSession} from '#/state/session'
import {useAgent, useSession} from '#/state/session'
const suggestedFollowsQueryKeyRoot = 'suggested-follows'
const suggestedFollowsQueryKey = [suggestedFollowsQueryKeyRoot]
@@ -28,7 +28,7 @@ const suggestedFollowsByActorQueryKey = (did: string) => [
]
export function useSuggestedFollowsQuery() {
const {currentAccount} = useSession()
const {agent, currentAccount} = useSession()
const moderationOpts = useModerationOpts()
return useInfiniteQuery<
@@ -42,7 +42,7 @@ export function useSuggestedFollowsQuery() {
staleTime: STALE.HOURS.ONE,
queryKey: suggestedFollowsQueryKey,
queryFn: async ({pageParam}) => {
const res = await getAgent().app.bsky.actor.getSuggestions({
const res = await agent.app.bsky.actor.getSuggestions({
limit: 25,
cursor: pageParam,
})
@@ -79,10 +79,11 @@ export function useSuggestedFollowsQuery() {
}
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
const agent = useAgent()
return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
queryKey: suggestedFollowsByActorQueryKey(did),
queryFn: async () => {
const res = await getAgent().app.bsky.graph.getSuggestedFollowsByActor({
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
actor: did,
})
return res.data
@@ -92,6 +93,7 @@ export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
export function useGetSuggestedFollowersByActor() {
const queryClient = useQueryClient()
const agent = useAgent()
return React.useCallback(
async (actor: string) => {
@@ -99,17 +101,16 @@ export function useGetSuggestedFollowersByActor() {
staleTime: STALE.MINUTES.ONE,
queryKey: suggestedFollowsByActorQueryKey(actor),
queryFn: async () => {
const res =
await getAgent().app.bsky.graph.getSuggestedFollowsByActor({
actor: actor,
})
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
actor: actor,
})
return res.data
},
})
return res
},
[queryClient],
[queryClient, agent],
)
}
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
import {BskyAgent} from '@atproto/api'
import {LogEvents} from '#/lib/statsig/statsig'
import {PersistedAccount} from '#/state/persisted'
/**
* Alias for `PersistedAccount` from persisted storage.
*/
export type SessionAccount = PersistedAccount
/**
* Subset of `SessionAccount` that excludes tokens.
*/
export type CurrentAccount = Omit<SessionAccount, 'accessJwt' | 'refreshJwt'>
/**
* Context shape returned from `useSession()`
*/
export type SessionStateContext = {
agent: BskyAgent
isInitialLoad: boolean
isSwitchingAccounts: boolean
hasSession: boolean
accounts: SessionAccount[]
/**
* Contains the full account object persisted to storage, minus access
* tokens.
*/
currentAccount: CurrentAccount | undefined
}
/**
* Context shape returned from `useSessionApi()`
*/
export type SessionApiContext = {
createAccount: (props: {
service: string
email: string
password: string
handle: string
inviteCode?: string
verificationPhone?: string
verificationCode?: string
}) => Promise<void>
login: (
props: {
service: string
identifier: string
password: string
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* A full logout. Clears the `currentAccount` from session, AND removes
* access tokens from all accounts, so that returning as any user will
* require a full login.
*/
logout: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => Promise<void>
/**
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
* clear access tokens from accounts, allowing the user to return to their
* other accounts without logging in.
*
* Used when adding a new account, deleting an account.
*/
clearCurrentAccount: () => void
initSession: (account: SessionAccount) => Promise<void>
resumeSession: (account?: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
selectAccount: (
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* Refreshes the BskyAgent's session and derive a fresh `currentAccount`
*/
refreshSession: () => void
}
+179
View File
@@ -0,0 +1,179 @@
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
import {jwtDecode} from 'jwt-decode'
import {IS_TEST_USER} from '#/lib/constants'
import {hasProp} from '#/lib/type-guards'
import {logger} from '#/logger'
import * as persisted from '#/state/persisted'
import {readLabelers} from '#/state/session/agent-config'
import {SessionAccount, SessionApiContext} from '#/state/session/types'
export function isSessionDeactivated(accessJwt: string | undefined) {
if (accessJwt) {
const sessData = jwtDecode(accessJwt)
return (
hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated'
)
}
return false
}
export function readLastActiveAccount() {
const {currentAccount, accounts} = persisted.get('session')
return accounts.find(a => a.did === currentAccount?.did)
}
export function agentToSessionAccount(
agent: BskyAgent,
): SessionAccount | undefined {
if (!agent.session) return undefined
return {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed,
deactivated: isSessionDeactivated(agent.session.accessJwt),
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
}
}
export function sessionAccountToAgentSession(
account: SessionAccount,
): BskyAgent['session'] {
return {
did: account.did,
handle: account.handle,
email: account.email,
emailConfirmed: account.emailConfirmed,
accessJwt: account.accessJwt || '',
refreshJwt: account.refreshJwt || '',
}
}
export async function configureModeration(
agent: BskyAgent,
account?: SessionAccount,
) {
if (account) {
if (IS_TEST_USER(account.handle)) {
const did = (
await agent
.resolveHandle({handle: 'mod-authority.test'})
.catch(_ => undefined)
)?.data.did
if (did) {
console.warn('USING TEST ENV MODERATION')
BskyAgent.configure({appLabelers: [did]})
}
} else {
BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]})
if (account) {
const labelerDids = await readLabelers(account.did).catch(_ => {})
if (labelerDids) {
agent.configureLabelersHeader(
labelerDids.filter(did => did !== BSKY_LABELER_DID),
)
}
}
}
} else {
BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]})
}
}
export function isSessionExpired(account: SessionAccount) {
let canReusePrevSession = false
try {
if (account.accessJwt) {
const decoded = jwtDecode(account.accessJwt)
if (decoded.exp) {
const didExpire = Date.now() >= decoded.exp * 1000
if (!didExpire) {
canReusePrevSession = true
}
}
}
} catch (e) {
logger.error(`session: could not decode jwt`)
}
return !canReusePrevSession
}
export async function createAgentAndLogin({
service,
identifier,
password,
}: {
service: string
identifier: string
password: string
}) {
const agent = new BskyAgent({service})
await agent.login({identifier, password})
if (!agent.session) {
throw new Error(`session: login failed to establish a session`)
}
const account = agentToSessionAccount(agent)!
await configureModeration(agent, account)
return {
agent,
account,
}
}
export async function createAgentAndCreateAccount({
service,
email,
password,
handle,
inviteCode,
verificationPhone,
verificationCode,
}: Parameters<SessionApiContext['createAccount']>[0]) {
const agent = new BskyAgent({service})
await agent.createAccount({
handle,
password,
email,
inviteCode,
verificationPhone,
verificationCode,
})
if (!agent.session) {
throw new Error(`session: createAccount failed to establish a session`)
}
const deactivated = isSessionDeactivated(agent.session.accessJwt)
if (!deactivated) {
/*dont await*/ agent.upsertProfile(_existing => {
return {
displayName: '',
// HACKFIX
// creating a bunch of identical profile objects is breaking the relay
// tossing this unspecced field onto it to reduce the size of the problem
// -prf
createdAt: new Date().toISOString(),
}
})
}
const account = agentToSessionAccount(agent)!
await configureModeration(agent, account)
return {
agent,
account,
}
}
+14 -7
View File
@@ -1,6 +1,8 @@
import React from 'react'
import * as persisted from '#/state/persisted'
import {useGate} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted'
type StateContext = string
type SetContext = (v: string) => void
@@ -8,7 +10,7 @@ type SetContext = (v: string) => void
const stateContext = React.createContext<StateContext>('home')
const setContext = React.createContext<SetContext>((_: string) => {})
function getInitialFeed() {
function getInitialFeed(startSessionWithFollowing: boolean) {
if (isWeb) {
if (window.location.pathname === '/') {
const params = new URLSearchParams(window.location.search)
@@ -24,16 +26,21 @@ function getInitialFeed() {
return feedFromSession
}
}
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
if (feedFromPersisted) {
// Fall back to the last chosen one across all tabs.
return feedFromPersisted
if (!startSessionWithFollowing) {
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
if (feedFromPersisted) {
// Fall back to the last chosen one across all tabs.
return feedFromPersisted
}
}
return 'home'
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(getInitialFeed)
const startSessionWithFollowing = useGate('start_session_with_following')
const [state, setState] = React.useState(() =>
getInitialFeed(startSessionWithFollowing),
)
const saveState = React.useCallback((feed: string) => {
setState(feed)
+7 -7
View File
@@ -14,7 +14,7 @@ import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {InlineLink} from '#/components/Link'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {CenteredView} from '../util/Views'
@@ -162,15 +162,15 @@ function Footer() {
a.flex_1,
t.atoms.border_contrast_medium,
]}>
<InlineLink to="https://bsky.social">
<InlineLinkText to="https://bsky.social">
<Trans>Business</Trans>
</InlineLink>
<InlineLink to="https://bsky.social/about/blog">
</InlineLinkText>
<InlineLinkText to="https://bsky.social/about/blog">
<Trans>Blog</Trans>
</InlineLink>
<InlineLink to="https://bsky.social/about/join">
</InlineLinkText>
<InlineLinkText to="https://bsky.social/about/join">
<Trans>Jobs</Trans>
</InlineLink>
</InlineLinkText>
<View style={a.flex_1} />
+15 -11
View File
@@ -1,17 +1,17 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {BSKY_SERVICE} from 'lib/constants'
import * as persisted from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {BSKY_SERVICE} from 'lib/constants'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Text, P} from '#/components/Typography'
import {Button, ButtonText} from '#/components/Button'
import * as ToggleButton from '#/components/forms/ToggleButton'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {P, Text} from '#/components/Typography'
export function ServerInputDialog({
control,
@@ -87,13 +87,17 @@ export function ServerInputDialog({
values={fixedOption}
onChange={setFixedOption}>
<ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}>
{_(msg`Bluesky`)}
<ToggleButton.ButtonText>
{_(msg`Bluesky`)}
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button
testID="customSelectBtn"
name="custom"
label={_(msg`Custom`)}>
{_(msg`Custom`)}
<ToggleButton.ButtonText>
{_(msg`Custom`)}
</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
@@ -106,9 +110,9 @@ export function ServerInputDialog({
a.px_md,
a.py_md,
]}>
<TextField.Label nativeID="address-input-label">
<TextField.LabelText nativeID="address-input-label">
<Trans>Server address</Trans>
</TextField.Label>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Globe} />
<Dialog.Input
@@ -163,7 +167,7 @@ export function ServerInputDialog({
size="small"
onPress={() => control.close()}
label={_(msg`Done`)}>
{_(msg`Done`)}
<ButtonText>{_(msg`Done`)}</ButtonText>
</Button>
</View>
</View>
+2 -6
View File
@@ -11,8 +11,8 @@ import {
TouchableOpacity,
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient'
import {RichText} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
@@ -508,11 +508,7 @@ export const ComposePost = observer(function ComposePost({
title={_(msg`Discard draft?`)}
description={_(msg`Are you sure you'd like to discard this draft?`)}
onConfirm={() => {
if (isWeb) {
onClose()
} else {
discardPromptControl.close(onClose)
}
discardPromptControl.close(onClose)
}}
confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative"
+36 -22
View File
@@ -1,28 +1,29 @@
import React from 'react'
import {useNavigation} from '@react-navigation/native'
import {useAnalytics} from 'lib/analytics/analytics'
import {useQueryClient} from '@tanstack/react-query'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {MainScrollProvider} from '../util/MainScrollProvider'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useSetMinimalShellMode} from '#/state/shell'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {ComposeIcon2} from 'lib/icons'
import {s} from 'lib/styles'
import {View, useWindowDimensions} from 'react-native'
import {ListMethods} from '../util/List'
import {Feed} from '../posts/Feed'
import {FAB} from '../util/fab/FAB'
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
import {useWindowDimensions, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {listenSoftReset} from '#/state/events'
import {truncateAndInvalidate} from '#/state/queries/util'
import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection'
import {logEvent} from '#/lib/statsig/statsig'
import {listenSoftReset} from '#/state/events'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ComposeIcon2} from 'lib/icons'
import {s} from 'lib/styles'
import {Feed} from '../posts/Feed'
import {FAB} from '../util/fab/FAB'
import {ListMethods} from '../util/List'
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
import {MainScrollProvider} from '../util/MainScrollProvider'
const POLL_FREQ = 60e3 // 60sec
@@ -71,6 +72,7 @@ export function FeedPage({
setHasNew(false)
logEvent('feed:refresh', {
feedType: feed.split('|')[0],
feedUrl: feed,
reason: 'soft-reset',
})
}
@@ -96,10 +98,22 @@ export function FeedPage({
setHasNew(false)
logEvent('feed:refresh', {
feedType: feed.split('|')[0],
feedUrl: feed,
reason: 'load-latest',
})
}, [scrollToTop, feed, queryClient, setHasNew])
let feedPollInterval
if (
useGate('disable_poll_on_discover') &&
feed === // Discover
'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
) {
feedPollInterval = undefined
} else {
feedPollInterval = POLL_FREQ
}
return (
<View testID={testID} style={s.h100pct}>
<MainScrollProvider>
@@ -108,7 +122,7 @@ export function FeedPage({
enabled={isPageFocused}
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
pollInterval={feedPollInterval}
disablePoll={hasNew}
scrollElRef={scrollElRef}
onScrolledDownChange={setIsScrolledDown}
+13 -12
View File
@@ -1,24 +1,25 @@
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 {usePalette} from 'lib/hooks/usePalette'
import {isNative} from 'platform/detection'
import {StyleSheet, TextInput, TouchableOpacity, View} from 'react-native'
import {setStringAsync} from 'expo-clipboard'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import Clipboard from '@react-native-clipboard/clipboard'
import * as Toast from '../util/Toast'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {
useAppPasswordsQuery,
useAppPasswordCreateMutation,
useAppPasswordsQuery,
} from '#/state/queries/app-passwords'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
import {isNative} from 'platform/detection'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
export const snapPoints = ['70%']
@@ -72,7 +73,7 @@ export function Component({}: {}) {
const onCopy = React.useCallback(() => {
if (appPassword) {
Clipboard.setString(appPassword)
setStringAsync(appPassword)
Toast.show(_(msg`Copied to clipboard`))
setWasCopied(true)
}
+17 -16
View File
@@ -1,28 +1,29 @@
import React, {useMemo, useCallback, useState} from 'react'
import React, {useCallback, useMemo, useState} from 'react'
import {
ImageStyle,
StyleSheet,
TouchableOpacity,
View,
TextInput as RNTextInput,
useWindowDimensions,
ScrollView as RNScrollView,
StyleSheet,
TextInput as RNTextInput,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native'
import {ScrollView, TextInput} from './util'
import {Image} from 'expo-image'
import {usePalette} from 'lib/hooks/usePalette'
import {gradients, s} from 'lib/styles'
import {enforceLen} from 'lib/strings/helpers'
import {LinearGradient} from 'expo-linear-gradient'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {MAX_ALT_TEXT} from 'lib/constants'
import {useTheme} from 'lib/ThemeContext'
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
import {Text} from '../util/text/Text'
import LinearGradient from 'react-native-linear-gradient'
import {usePalette} from 'lib/hooks/usePalette'
import {enforceLen} from 'lib/strings/helpers'
import {gradients, s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {isWeb} from 'platform/detection'
import {ImageModel} from 'state/models/media/image'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {Text} from '../util/text/Text'
import {ScrollView, TextInput} from './util'
export const snapPoints = ['100%']
+15 -20
View File
@@ -1,19 +1,20 @@
import React, {useState} from 'react'
import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native'
import {ScrollView, TextInput} from './util'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {ErrorMessage} from '../util/error/ErrorMessage'
import * as Toast from '../util/Toast'
import {s, colors} from 'lib/styles'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useSession, useSessionApi, getAgent} from '#/state/session'
import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {ScrollView, TextInput} from './util'
enum Stages {
InputEmail,
@@ -26,7 +27,7 @@ export const snapPoints = ['90%']
export function Component() {
const pal = usePalette('default')
const {currentAccount} = useSession()
const {updateCurrentAccount} = useSessionApi()
const {refreshSession} = useSessionApi()
const {_} = useLingui()
const [stage, setStage] = useState<Stages>(Stages.InputEmail)
const [email, setEmail] = useState<string>(currentAccount?.email || '')
@@ -49,10 +50,7 @@ export function Component() {
setStage(Stages.ConfirmCode)
} else {
await getAgent().com.atproto.server.updateEmail({email: email.trim()})
updateCurrentAccount({
email: email.trim(),
emailConfirmed: false,
})
refreshSession()
Toast.show(_(msg`Email updated`))
setStage(Stages.Done)
}
@@ -81,10 +79,7 @@ export function Component() {
email: email.trim(),
token: confirmationCode.trim(),
})
updateCurrentAccount({
email: email.trim(),
emailConfirmed: false,
})
refreshSession()
Toast.show(_(msg`Email updated`))
setStage(Stages.Done)
} catch (e) {
+24 -27
View File
@@ -1,37 +1,38 @@
import React, {useState} from 'react'
import Clipboard from '@react-native-clipboard/clipboard'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import * as Toast from '../util/Toast'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {setStringAsync} from 'expo-clipboard'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {ScrollView, TextInput} from './util'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {SelectableBtn} from '../util/forms/SelectableBtn'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {s} from 'lib/styles'
import {createFullHandle, makeValidHandle} from 'lib/strings/handles'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {useAnalytics} from 'lib/analytics/analytics'
import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
import {useServiceQuery} from '#/state/queries/service'
import {useUpdateHandleMutation, useFetchDid} from '#/state/queries/handle'
import {
getAgent,
SessionAccount,
useSession,
useSessionApi,
SessionAccount,
getAgent,
} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {cleanError} from 'lib/strings/errors'
import {createFullHandle, makeValidHandle} from 'lib/strings/handles'
import {s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {SelectableBtn} from '../util/forms/SelectableBtn'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {ScrollView, TextInput} from './util'
export const snapPoints = ['100%']
@@ -71,7 +72,7 @@ export function Inner({
const {_} = useLingui()
const pal = usePalette('default')
const {track} = useAnalytics()
const {updateCurrentAccount} = useSessionApi()
const {refreshSession} = useSessionApi()
const {closeModal} = useModalControls()
const {mutateAsync: updateHandle, isPending: isUpdateHandlePending} =
useUpdateHandleMutation()
@@ -114,9 +115,7 @@ export function Inner({
await updateHandle({
handle: newHandle,
})
updateCurrentAccount({
handle: newHandle,
})
refreshSession()
closeModal()
onChanged()
} catch (err: any) {
@@ -132,7 +131,7 @@ export function Inner({
onChanged,
track,
closeModal,
updateCurrentAccount,
refreshSession,
updateHandle,
serviceInfo,
])
@@ -321,9 +320,7 @@ function CustomHandleForm({
// events
// =
const onPressCopy = React.useCallback(() => {
Clipboard.setString(
isDNSForm ? `did=${currentAccount.did}` : currentAccount.did,
)
setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did)
Toast.show(_(msg`Copied to clipboard`))
}, [currentAccount, isDNSForm, _])
const onChangeHandle = React.useCallback(
+19 -18
View File
@@ -1,4 +1,4 @@
import React, {useState, useCallback, useMemo} from 'react'
import React, {useCallback, useMemo, useState} from 'react'
import {
ActivityIndicator,
KeyboardAvoidingView,
@@ -8,35 +8,36 @@ import {
TouchableOpacity,
View,
} from 'react-native'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {LinearGradient} from 'expo-linear-gradient'
import {
AppBskyGraphDefs,
AppBskyRichtextFacet,
RichText as RichTextAPI,
} from '@atproto/api'
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 * as Toast from '../util/Toast'
import {s, colors, gradients} from 'lib/styles'
import {enforceLen} from 'lib/strings/helpers'
import {compressIfNeeded} from 'lib/media/manip'
import {EditableUserAvatar} from '../util/UserAvatar'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {useAnalytics} from 'lib/analytics/analytics'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError, isNetworkError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {shortenLinks} from '#/lib/strings/rich-text-manip'
import {useModalControls} from '#/state/modals'
import {
useListCreateMutation,
useListMetadataMutation,
} from '#/state/queries/list'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {shortenLinks} from '#/lib/strings/rich-text-manip'
import {getAgent} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {compressIfNeeded} from 'lib/media/manip'
import {cleanError, isNetworkError} from 'lib/strings/errors'
import {enforceLen} from 'lib/strings/helpers'
import {colors, gradients, s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {EditableUserAvatar} from '../util/UserAvatar'
const MAX_NAME = 64 // todo
const MAX_DESCRIPTION = 300 // todo
+1 -1
View File
@@ -6,7 +6,7 @@ import {
TouchableOpacity,
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {LinearGradient} from 'expo-linear-gradient'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+17 -16
View File
@@ -1,26 +1,27 @@
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 {LinearGradient} from 'expo-linear-gradient'
import {MaterialIcons} from '@expo/vector-icons'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Slider} from '@miblanchard/react-native-slider'
import {observer} from 'mobx-react-lite'
import ImageEditor, {Position} from 'react-avatar-editor'
import {useModalControls} from '#/state/modals'
import {MAX_ALT_TEXT} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons'
import {enforceLen} from 'lib/strings/helpers'
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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
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 {getKeys} from 'lib/type-assertions'
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 {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {Text} from '../util/text/Text'
import {TextInput} from './util'
export const snapPoints = ['80%']

Some files were not shown because too many files have changed in this diff Show More