Compare commits

...

51 Commits

Author SHA1 Message Date
Eric Bailey 3149f5234d Add back deprecated updateCurrentAccount and revert usages (#3540) 2024-04-13 13:40:53 -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
117 changed files with 2957 additions and 2310 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])
+8 -15
View File
@@ -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>
)
+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(),
+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 = {
+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')
+26 -3
View File
@@ -20,7 +20,7 @@ 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'
@@ -202,7 +202,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,10 +252,24 @@ export function useProfileFollowMutationQueue(
function useProfileFollowMutation(
logContext: LogEvents['profile:follow']['logContext'],
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
) {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
logEvent('profile:follow', {logContext})
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 getAgent().follow(did)
},
onSuccess(data, variables) {
@@ -530,3 +544,12 @@ export function* findAllProfilesInQueryData(
}
}
}
export function findProfileQueryData(
queryClient: QueryClient,
did: string,
): AppBskyActorDefs.ProfileViewDetailed | undefined {
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewDetailed>(
RQKEY(did),
)
}
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
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 = {
currentAgent: 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
/**
* @deprecated Use `refreshSession` instead.
*/
updateCurrentAccount: (
account: Partial<
Pick<SessionAccount, 'handle' | 'email' | 'emailConfirmed'>
>,
) => 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%']
+12 -11
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,
+21 -22
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%']
@@ -321,9 +322,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%']
+20 -19
View File
@@ -1,5 +1,4 @@
import React, {useState, useCallback} from 'react'
import * as Toast from '../util/Toast'
import React, {useCallback, useState} from 'react'
import {
ActivityIndicator,
KeyboardAvoidingView,
@@ -9,28 +8,30 @@ import {
TouchableOpacity,
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {AppBskyActorDefs} from '@atproto/api'
import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {s, colors, gradients} from 'lib/styles'
import {enforceLen} from 'lib/strings/helpers'
import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants'
import {compressIfNeeded} from 'lib/media/manip'
import {UserBanner} from '../util/UserBanner'
import {EditableUserAvatar} from '../util/UserAvatar'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {useAnalytics} from 'lib/analytics/analytics'
import {cleanError} from 'lib/strings/errors'
import Animated, {FadeOut} from 'react-native-reanimated'
import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro'
import {LinearGradient} from 'expo-linear-gradient'
import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useProfileUpdateMutation} from '#/state/queries/profile'
import {logger} from '#/logger'
import {useAnalytics} from 'lib/analytics/analytics'
import {MAX_DESCRIPTION, MAX_DISPLAY_NAME} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette'
import {compressIfNeeded} from 'lib/media/manip'
import {cleanError} from 'lib/strings/errors'
import {enforceLen} from 'lib/strings/helpers'
import {colors, gradients, s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {EditableUserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner'
const AnimatedTouchableOpacity =
Animated.createAnimatedComponent(TouchableOpacity)
-153
View File
@@ -1,153 +0,0 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {s, colors, gradients} from 'lib/styles'
import {Text} from '../util/text/Text'
import {ScrollView} from './util'
import {usePalette} from 'lib/hooks/usePalette'
import {
EmbedPlayerSource,
embedPlayerSources,
externalEmbedLabels,
} from '#/lib/strings/embed-player'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useSetExternalEmbedPref} from '#/state/preferences/external-embeds-prefs'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
export const snapPoints = [450]
export function Component({
onAccept,
source,
}: {
onAccept: () => void
source: EmbedPlayerSource
}) {
const pal = usePalette('default')
const {closeModal} = useModalControls()
const {_} = useLingui()
const setExternalEmbedPref = useSetExternalEmbedPref()
const {isMobile} = useWebMediaQueries()
const onShowAllPress = React.useCallback(() => {
for (const key of embedPlayerSources) {
setExternalEmbedPref(key, 'show')
}
onAccept()
closeModal()
}, [closeModal, onAccept, setExternalEmbedPref])
const onShowPress = React.useCallback(() => {
setExternalEmbedPref(source, 'show')
onAccept()
closeModal()
}, [closeModal, onAccept, setExternalEmbedPref, source])
const onHidePress = React.useCallback(() => {
setExternalEmbedPref(source, 'hide')
closeModal()
}, [closeModal, setExternalEmbedPref, source])
return (
<ScrollView
testID="embedConsentModal"
style={[
s.flex1,
pal.view,
isMobile
? {paddingHorizontal: 20, paddingTop: 10}
: {paddingHorizontal: 30},
]}>
<Text style={[pal.text, styles.title]}>
<Trans>External Media</Trans>
</Text>
<Text style={pal.text}>
<Trans>
This content is hosted by {externalEmbedLabels[source]}. Do you want
to enable external media?
</Trans>
</Text>
<View style={[s.mt10]} />
<Text style={pal.textLight}>
<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 style={[s.mt20]} />
<TouchableOpacity
testID="enableAllBtn"
onPress={onShowAllPress}
accessibilityRole="button"
accessibilityLabel={_(
msg`Show embeds from ${externalEmbedLabels[source]}`,
)}
accessibilityHint=""
onAccessibilityEscape={closeModal}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Enable External Media</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
<View style={[s.mt10]} />
<TouchableOpacity
testID="enableSourceBtn"
onPress={onShowPress}
accessibilityRole="button"
accessibilityLabel={_(
msg`Never load embeds from ${externalEmbedLabels[source]}`,
)}
accessibilityHint=""
onAccessibilityEscape={closeModal}>
<View style={[styles.btn, pal.btn]}>
<Text style={[pal.text, s.bold, s.f18]}>
<Trans>Enable {externalEmbedLabels[source]} only</Trans>
</Text>
</View>
</TouchableOpacity>
<View style={[s.mt10]} />
<TouchableOpacity
testID="disableSourceBtn"
onPress={onHidePress}
accessibilityRole="button"
accessibilityLabel={_(
msg`Never load embeds from ${externalEmbedLabels[source]}`,
)}
accessibilityHint=""
onAccessibilityEscape={closeModal}>
<View style={[styles.btn, pal.btn]}>
<Text style={[pal.text, s.bold, s.f18]}>
<Trans>No thanks</Trans>
</Text>
</View>
</TouchableOpacity>
</ScrollView>
)
}
const styles = StyleSheet.create({
title: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
borderRadius: 32,
padding: 14,
backgroundColor: colors.gray1,
},
})
+23 -22
View File
@@ -1,37 +1,38 @@
import React from 'react'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
ActivityIndicator,
} from 'react-native'
import {setStringAsync} from 'expo-clipboard'
import {ComAtprotoServerDefs} from '@atproto/api'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import Clipboard from '@react-native-clipboard/clipboard'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import * as Toast from '../util/Toast'
import {ScrollView} from './util'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans, msg} from '@lingui/macro'
import {cleanError} from 'lib/strings/errors'
import {useModalControls} from '#/state/modals'
import {useInvitesState, useInvitesAPI} from '#/state/invites'
import {UserInfoText} from '../util/UserInfoText'
import {makeProfileLink} from '#/lib/routes/links'
import {Link} from '../util/Link'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {
useInviteCodesQuery,
InviteCodesQueryResponse,
} from '#/state/queries/invites'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {makeProfileLink} from '#/lib/routes/links'
import {useInvitesAPI, useInvitesState} from '#/state/invites'
import {useModalControls} from '#/state/modals'
import {
InviteCodesQueryResponse,
useInviteCodesQuery,
} from '#/state/queries/invites'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
import {isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {UserInfoText} from '../util/UserInfoText'
import {ScrollView} from './util'
export const snapPoints = ['70%']
export function Component() {
@@ -148,7 +149,7 @@ function InviteCode({
const uses = invite.uses
const onPress = React.useCallback(() => {
Clipboard.setString(invite.code)
setStringAsync(invite.code)
Toast.show(_(msg`Copied to clipboard`))
setInviteCopied(invite.code)
}, [setInviteCopied, invite, _])
-4
View File
@@ -15,7 +15,6 @@ import * as ChangePasswordModal from './ChangePassword'
import * as CreateOrEditListModal from './CreateOrEditList'
import * as DeleteAccountModal from './DeleteAccount'
import * as EditProfileModal from './EditProfile'
import * as EmbedConsentModal from './EmbedConsent'
import * as InAppBrowserConsentModal from './InAppBrowserConsent'
import * as InviteCodesModal from './InviteCodes'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
@@ -116,9 +115,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'link-warning') {
snapPoints = LinkWarningModal.snapPoints
element = <LinkWarningModal.Component {...activeModal} />
} else if (activeModal?.name === 'embed-consent') {
snapPoints = EmbedConsentModal.snapPoints
element = <EmbedConsentModal.Component {...activeModal} />
} else if (activeModal?.name === 'in-app-browser-consent') {
snapPoints = InAppBrowserConsentModal.snapPoints
element = <InAppBrowserConsentModal.Component {...activeModal} />
+19 -22
View File
@@ -1,33 +1,32 @@
import React from 'react'
import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import type {Modal as ModalIface} from '#/state/modals'
import {useModalControls, useModals} from '#/state/modals'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import {useModals, useModalControls} from '#/state/modals'
import type {Modal as ModalIface} from '#/state/modals'
import * as EditProfileModal from './EditProfile'
import * as AddAppPassword from './AddAppPasswords'
import * as AltTextImageModal from './AltImage'
import * as ChangeEmailModal from './ChangeEmail'
import * as ChangeHandleModal from './ChangeHandle'
import * as ChangePasswordModal from './ChangePassword'
import * as CreateOrEditListModal from './CreateOrEditList'
import * as UserAddRemoveLists from './UserAddRemoveLists'
import * as ListAddUserModal from './ListAddRemoveUsers'
import * as CropImageModal from './crop-image/CropImage.web'
import * as DeleteAccountModal from './DeleteAccount'
import * as EditImageModal from './EditImage'
import * as EditProfileModal from './EditProfile'
import * as InviteCodesModal from './InviteCodes'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as LinkWarningModal from './LinkWarning'
import * as ListAddUserModal from './ListAddRemoveUsers'
import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel'
import * as ThreadgateModal from './Threadgate'
import * as CropImageModal from './crop-image/CropImage.web'
import * as AltTextImageModal from './AltImage'
import * as EditImageModal from './EditImage'
import * as ChangeHandleModal from './ChangeHandle'
import * as InviteCodesModal from './InviteCodes'
import * as AddAppPassword from './AddAppPasswords'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as UserAddRemoveLists from './UserAddRemoveLists'
import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail'
import * as ChangePasswordModal from './ChangePassword'
import * as LinkWarningModal from './LinkWarning'
import * as EmbedConsentModal from './EmbedConsent'
export function ModalsContainer() {
const {isModalActive, activeModals} = useModals()
@@ -112,8 +111,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <ChangePasswordModal.Component />
} else if (modal.name === 'link-warning') {
element = <LinkWarningModal.Component {...modal} />
} else if (modal.name === 'embed-consent') {
element = <EmbedConsentModal.Component {...modal} />
} else {
return null
}
+8 -7
View File
@@ -1,14 +1,15 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {s, colors, gradients} from 'lib/styles'
import {Text} from '../util/text/Text'
import {LinearGradient} from 'expo-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {usePalette} from 'lib/hooks/usePalette'
import {RepostIcon} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {colors, gradients, s} from 'lib/styles'
import {Text} from '../util/text/Text'
export const snapPoints = [250]
+14 -13
View File
@@ -6,23 +6,24 @@ import {
StyleSheet,
View,
} from 'react-native'
import {Svg, Circle, Path} from 'react-native-svg'
import {ScrollView, TextInput} from './util'
import {Circle, Path, Svg} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
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 {logger} from '#/logger'
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 {logger} from '#/logger'
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'
export const snapPoints = ['90%']
@@ -1,18 +1,19 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import ImageEditor from 'react-avatar-editor'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {LinearGradient} from 'expo-linear-gradient'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Slider} from '@miblanchard/react-native-slider'
import LinearGradient from 'react-native-linear-gradient'
import {Text} from 'view/com/util/text/Text'
import ImageEditor from 'react-avatar-editor'
import {useModalControls} from '#/state/modals'
import {usePalette} from 'lib/hooks/usePalette'
import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons'
import {Dimensions} from 'lib/media/types'
import {getDataUriSize} from 'lib/media/util'
import {s, gradients} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {gradients, s} from 'lib/styles'
import {Text} from 'view/com/util/text/Text'
enum AspectRatio {
Square = 'square',
@@ -1,11 +1,12 @@
import React from 'react'
import {StyleSheet, Text, View, Pressable} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {s, colors, gradients} from 'lib/styles'
import {Pressable, StyleSheet, Text, View} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {colors, gradients, s} from 'lib/styles'
export const ConfirmLanguagesButton = ({
onPress,
@@ -1,24 +1,25 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {AppBskyActorDefs} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast'
import {s} from 'lib/styles'
import {track} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles'
import {Shadow, useProfileShadow} from 'state/cache/profile-shadow'
import {track} from 'lib/analytics/analytics'
import {
useProfileFollowMutationQueue,
useProfileQuery,
} from 'state/queries/profile'
import {useRequireAuth} from 'state/session'
import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast'
export function PostThreadFollowBtn({did}: {did: string}) {
const {data: profile, isLoading} = useProfileQuery({did})
@@ -47,8 +48,10 @@ function PostThreadFollowBtnLoaded({
'PostThreadItem',
)
const requireAuth = useRequireAuth()
const showFollowBackLabel = useGate('show_follow_back_label')
const isFollowing = !!profile.viewer?.following
const isFollowedBy = !!profile.viewer?.followedBy
const [wasFollowing, setWasFollowing] = React.useState<boolean>(isFollowing)
// This prevents the button from disappearing as soon as we follow.
@@ -136,7 +139,15 @@ function PostThreadFollowBtnLoaded({
type="button"
style={[!isFollowing ? palInverted.text : pal.text, s.bold]}
numberOfLines={1}>
{!isFollowing ? <Trans>Follow</Trans> : <Trans>Following</Trans>}
{!isFollowing ? (
showFollowBackLabel && isFollowedBy ? (
<Trans>Follow Back</Trans>
) : (
<Trans>Follow</Trans>
)
) : (
<Trans>Following</Trans>
)}
</Text>
</TouchableOpacity>
</View>
+27 -23
View File
@@ -8,32 +8,33 @@ import {
View,
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {useAnalytics} from 'lib/analytics/analytics'
import {useTheme} from 'lib/ThemeContext'
import {logger} from '#/logger'
import {
RQKEY,
FeedDescriptor,
FeedParams,
usePostFeedQuery,
pollLatest,
} from '#/state/queries/post-feed'
import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events'
import {useSession} from '#/state/session'
import {STALE} from '#/state/queries'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {useQueryClient} from '@tanstack/react-query'
import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events'
import {STALE} from '#/state/queries'
import {
FeedDescriptor,
FeedParams,
pollLatest,
RQKEY,
usePostFeedQuery,
} from '#/state/queries/post-feed'
import {useSession} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {useTheme} from 'lib/ThemeContext'
import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice'
const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -217,6 +218,7 @@ let Feed = ({
track('Feed:onRefresh')
logEvent('feed:refresh', {
feedType: feedType,
feedUrl: feed,
reason: 'pull-to-refresh',
})
setIsPTRing(true)
@@ -227,13 +229,14 @@ let Feed = ({
logger.error('Failed to refresh posts feed', {message: err})
}
setIsPTRing(false)
}, [refetch, track, setIsPTRing, onHasNew, feedType])
}, [refetch, track, setIsPTRing, onHasNew, feed, feedType])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
logEvent('feed:endReached', {
feedType: feedType,
feedUrl: feed,
itemCount: feedItems.length,
})
track('Feed:onEndReached')
@@ -248,6 +251,7 @@ let Feed = ({
isError,
fetchNextPage,
track,
feed,
feedType,
feedItems.length,
])
-30
View File
@@ -1,30 +0,0 @@
import React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'
import {addStyle} from 'lib/styles'
type BlurViewProps = ViewProps & {
blurType?: 'dark' | 'light'
blurAmount?: number
}
export const BlurView = ({
style,
blurType,
...props
}: React.PropsWithChildren<BlurViewProps>) => {
if (blurType === 'dark') {
style = addStyle(style, styles.dark)
} else {
style = addStyle(style, styles.light)
}
return <View style={style} {...props} />
}
const styles = StyleSheet.create({
dark: {
backgroundColor: '#0008',
},
light: {
backgroundColor: '#fff8',
},
})
-1
View File
@@ -1 +0,0 @@
export {BlurView} from '@react-native-community/blur'
-35
View File
@@ -1,35 +0,0 @@
import React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'
import {addStyle} from 'lib/styles'
type BlurViewProps = ViewProps & {
blurType?: 'dark' | 'light'
blurAmount?: number
}
export const BlurView = ({
style,
blurType,
blurAmount,
...props
}: React.PropsWithChildren<BlurViewProps>) => {
// @ts-ignore using an RNW-specific attribute here -prf
let blur = `blur(${blurAmount || 10}px`
// @ts-ignore using an RNW-specific attribute here -prf
style = addStyle(style, {backdropFilter: blur, WebkitBackdropFilter: blur})
if (blurType === 'dark') {
style = addStyle(style, styles.dark)
} else {
style = addStyle(style, styles.light)
}
return <View style={style} {...props} />
}
const styles = StyleSheet.create({
dark: {
backgroundColor: '#0008',
},
light: {
backgroundColor: '#fff8',
},
})

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